tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

changes.js (8807B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 loader.lazyRequireGetter(
      7  this,
      8  "getTabPrefs",
      9  "resource://devtools/shared/indentation.js",
     10  true
     11 );
     12 
     13 const {
     14  getSourceForDisplay,
     15 } = require("resource://devtools/client/inspector/changes/utils/changes-utils.js");
     16 
     17 /**
     18 * In the Redux state, changed CSS rules are grouped by source (stylesheet) and stored in
     19 * a single level array, regardless of nesting.
     20 * This method returns a nested tree structure of the changed CSS rules so the React
     21 * consumer components can traverse it easier when rendering the nested CSS rules view.
     22 * Keeping this interface updated allows the Redux state structure to change without
     23 * affecting the consumer components.
     24 *
     25 * @param {object} state
     26 *        Redux slice for tracked changes.
     27 * @param {object} filter
     28 *        Object with optional filters to use. Has the following properties:
     29 *        - sourceIds: {Array}
     30 *          Use only subtrees of sources matching source ids from this array.
     31 *        - ruleIds: {Array}
     32 *          Use only rules matching rule ids from this array. If the array includes ids
     33 *          of ancestor rules (@media, @supports), their nested rules will be included.
     34 * @return {object}
     35 */
     36 function getChangesTree(state, filter = {}) {
     37  // Use or assign defaults of sourceId and ruleId arrays by which to filter the tree.
     38  const { sourceIds: sourceIdsFilter = [], ruleIds: rulesIdsFilter = [] } =
     39    filter;
     40  /**
     41   * Recursively replace a rule's array of child rule ids with the referenced child rules.
     42   * Mark visited rules so as not to handle them (and their children) again.
     43   *
     44   * Returns the rule object with expanded children or null if previously visited.
     45   *
     46   * @param  {string} ruleId
     47   * @param  {object} rule
     48   * @param  {Array} rules
     49   * @param  {Set} visitedRules
     50   * @return {object | null}
     51   */
     52  function expandRuleChildren(ruleId, rule, rules, visitedRules) {
     53    if (visitedRules.has(ruleId)) {
     54      return null;
     55    }
     56 
     57    visitedRules.add(ruleId);
     58 
     59    return {
     60      ...rule,
     61      children: rule.children.map(childRuleId =>
     62        expandRuleChildren(childRuleId, rules[childRuleId], rules, visitedRules)
     63      ),
     64    };
     65  }
     66 
     67  return Object.entries(state)
     68    .filter(([sourceId]) => {
     69      // Use only matching sources if an array to filter by was provided.
     70      if (sourceIdsFilter.length) {
     71        return sourceIdsFilter.includes(sourceId);
     72      }
     73 
     74      return true;
     75    })
     76    .reduce((sourcesObj, [sourceId, source]) => {
     77      const { rules } = source;
     78      // Log of visited rules in this source. Helps avoid duplication when traversing the
     79      // descendant rule tree. This Set is unique per source. It will be passed down to
     80      // be populated with ids of rules once visited. This ensures that only visited rules
     81      // unique to this source will be skipped and prevents skipping identical rules from
     82      // other sources (ex: rules with the same selector and the same index).
     83      const visitedRules = new Set();
     84 
     85      // Build a new collection of sources keyed by source id.
     86      sourcesObj[sourceId] = {
     87        ...source,
     88        // Build a new collection of rules keyed by rule id.
     89        rules: Object.entries(rules)
     90          .filter(([ruleId]) => {
     91            // Use only matching rules if an array to filter by was provided.
     92            if (rulesIdsFilter.length) {
     93              return rulesIdsFilter.includes(ruleId);
     94            }
     95 
     96            return true;
     97          })
     98          .reduce((rulesObj, [ruleId, rule]) => {
     99            // Expand the rule's array of child rule ids with the referenced child rules.
    100            // Skip exposing null values which mean the rule was previously visited
    101            // as part of an ancestor descendant tree.
    102            const expandedRule = expandRuleChildren(
    103              ruleId,
    104              rule,
    105              rules,
    106              visitedRules
    107            );
    108            if (expandedRule !== null) {
    109              rulesObj[ruleId] = expandedRule;
    110            }
    111 
    112            return rulesObj;
    113          }, {}),
    114      };
    115 
    116      return sourcesObj;
    117    }, {});
    118 }
    119 
    120 /**
    121 * Build the CSS text of a stylesheet with the changes aggregated in the Redux state.
    122 * If filters for rule id or source id are provided, restrict the changes to the matching
    123 * sources and rules.
    124 *
    125 * Code comments with the source origin are put above of the CSS rule (or group of
    126 * rules). Removed CSS declarations are written commented out. Added CSS declarations are
    127 * written as-is.
    128 *
    129 * @param  {object} state
    130 *         Redux slice for tracked changes.
    131 * @param  {object} filter
    132 *         Object with optional source and rule filters. See getChangesTree()
    133 * @return {string}
    134 *         CSS stylesheet text.
    135 */
    136 
    137 // For stylesheet sources, the stylesheet filename and full path are used:
    138 //
    139 // /* styles.css | https://example.com/styles.css */
    140 //
    141 // .selector {
    142 //  /* property: oldvalue; */
    143 //  property: value;
    144 // }
    145 
    146 // For inline stylesheet sources, the stylesheet index and host document URL are used:
    147 //
    148 // /* Inline | https://example.com */
    149 //
    150 // .selector {
    151 //  /* property: oldvalue; */
    152 //  property: value;
    153 // }
    154 
    155 // For element style attribute sources, the unique selector generated for the element
    156 // and the host document URL are used:
    157 //
    158 // /* Element (div) | https://example.com */
    159 //
    160 // div:nth-child(1) {
    161 //  /* property: oldvalue; */
    162 //  property: value;
    163 // }
    164 function getChangesStylesheet(state, filter) {
    165  const changeTree = getChangesTree(state, filter);
    166  // Get user prefs about indentation style.
    167  const { indentUnit, indentWithTabs } = getTabPrefs();
    168  const indentChar = indentWithTabs
    169    ? "\t".repeat(indentUnit)
    170    : " ".repeat(indentUnit);
    171 
    172  /**
    173   * If the rule has just one item in its array of selector versions, return it as-is.
    174   * If it has more than one, build a string using the first selector commented-out
    175   * and the last selector as-is. This indicates that a rule's selector has changed.
    176   *
    177   * @param  {Array} selectors
    178   *         History of selector versions if changed over time.
    179   *         Array with a single item (the original selector) if never changed.
    180   * @param  {number} level
    181   *         Level of nesting within a CSS rule tree.
    182   * @return {string}
    183   */
    184  function writeSelector(selectors = [], level) {
    185    const indent = indentChar.repeat(level);
    186    let selectorText;
    187    switch (selectors.length) {
    188      case 0:
    189        selectorText = "";
    190        break;
    191      case 1:
    192        selectorText = `${indent}${selectors[0]}`;
    193        break;
    194      default:
    195        selectorText =
    196          `${indent}/* ${selectors[0]} { */\n` +
    197          `${indent}${selectors[selectors.length - 1]}`;
    198    }
    199 
    200    return selectorText;
    201  }
    202 
    203  function writeRule(ruleId, rule, level) {
    204    // Write nested rules, if any.
    205    let ruleBody = rule.children.reduce((str, childRule) => {
    206      str += writeRule(childRule.ruleId, childRule, level + 1);
    207      return str;
    208    }, "");
    209 
    210    // Write changed CSS declarations.
    211    ruleBody += writeDeclarations(rule.remove, rule.add, level + 1);
    212 
    213    const indent = indentChar.repeat(level);
    214    const selectorText = writeSelector(rule.selectors, level);
    215    return `\n${selectorText} {${ruleBody}\n${indent}}`;
    216  }
    217 
    218  function writeDeclarations(remove = [], add = [], level) {
    219    const indent = indentChar.repeat(level);
    220    const removals = remove
    221      // Sort declarations in the order in which they exist in the original CSS rule.
    222      .sort((a, b) => a.index > b.index)
    223      .reduce((str, { property, value }) => {
    224        str += `\n${indent}/* ${property}: ${value}; */`;
    225        return str;
    226      }, "");
    227 
    228    const additions = add
    229      // Sort declarations in the order in which they exist in the original CSS rule.
    230      .sort((a, b) => a.index > b.index)
    231      .reduce((str, { property, value }) => {
    232        str += `\n${indent}${property}: ${value};`;
    233        return str;
    234      }, "");
    235 
    236    return removals + additions;
    237  }
    238 
    239  // Iterate through all sources in the change tree and build a CSS stylesheet string.
    240  return Object.values(changeTree).reduce((stylesheetText, source) => {
    241    const { href, rules } = source;
    242    // Write code comment with source origin
    243    stylesheetText += `\n/* ${getSourceForDisplay(source)} | ${href} */\n`;
    244    // Write CSS rules
    245    stylesheetText += Object.entries(rules).reduce((str, [ruleId, rule]) => {
    246      // Add a new like only after top-level rules (level == 0)
    247      str += writeRule(ruleId, rule, 0) + "\n";
    248      return str;
    249    }, "");
    250 
    251    return stylesheetText;
    252  }, "");
    253 }
    254 
    255 module.exports = {
    256  getChangesTree,
    257  getChangesStylesheet,
    258 };