Bug 1524548 - (Part 2) Add Redux selector method to build a stylesheet from a tree of changes. r=pbro
authorRazvan Caliman <rcaliman@mozilla.com>
Thu, 07 Feb 2019 08:36:15 +0000
changeset 457631 8c2113b14a96f565d94004526418923d38e4afb0
parent 457630 08fd81ac849bd369901e1d00989499aa0d27dc1c
child 457632 69f78374b0b2ee4860bc3e4e323afa6b164dad83
push id35516
push userrmaries@mozilla.com
push dateFri, 08 Feb 2019 04:23:26 +0000
treeherdermozilla-central@d599d1a73a3a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1524548
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1524548 - (Part 2) Add Redux selector method to build a stylesheet from a tree of changes. r=pbro Depends on D18703 Adds a new selector method to build the CSS text for a stylesheet with the aggregated changes for one or more rules. Makes use of the filtering capabilities introduced in Part 1. Differential Revision: https://phabricator.services.mozilla.com/D18704
devtools/client/inspector/changes/selectors/changes.js
--- a/devtools/client/inspector/changes/selectors/changes.js
+++ b/devtools/client/inspector/changes/selectors/changes.js
@@ -1,15 +1,19 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
+loader.lazyRequireGetter(this, "getTabPrefs", "devtools/shared/indentation", true);
+
+const { getSourceForDisplay } = require("../utils/changes-utils");
+
 /**
  * In the Redux state, changed CSS rules are grouped by source (stylesheet) and stored in
  * a single level array, regardless of nesting.
  * This method returns a nested tree structure of the changed CSS rules so the React
  * consumer components can traverse it easier when rendering the nested CSS rules view.
  * Keeping this interface updated allows the Redux state structure to change without
  * affecting the consumer components.
  *
@@ -96,11 +100,112 @@ function getChangesTree(state, filter = 
             return rulesObj;
           }, {}),
       };
 
       return sourcesObj;
     }, {});
 }
 
+/**
+ * Build the CSS text of a stylesheet with the changes aggregated in the Redux state.
+ * If filters for rule id or source id are provided, restrict the changes to the matching
+ * sources and rules.
+ *
+ * Code comments with the source origin are put above of the CSS rule (or group of
+ * rules). Removed CSS declarations are written commented out. Added CSS declarations are
+ * written as-is.
+ *
+ * @param  {Object} state
+ *         Redux slice for tracked changes.
+ * @param  {Object} filter
+ *         Object with optional source and rule filters. See getChangesTree()
+ * @return {String}
+ *         CSS stylesheet text.
+ */
+
+ // For stylesheet sources, the stylesheet filename and full path are used:
+ //
+ // /* styles.css | https://example.com/styles.css */
+ //
+ // .selector {
+ //  /* property: oldvalue; */
+ //  property: value;
+ // }
+
+ // For inline stylesheet sources, the stylesheet index and host document URL are used:
+ //
+ // /* Inline #1 | https://example.com */
+ //
+ // .selector {
+ //  /* property: oldvalue; */
+ //  property: value;
+ // }
+
+ // For element style attribute sources, the unique selector generated for the element
+ // and the host document URL are used:
+ //
+ // /* Element (div) | https://example.com */
+ //
+ // div:nth-child(1) {
+ //  /* property: oldvalue; */
+ //  property: value;
+ // }
+function getChangesStylesheet(state, filter) {
+  const changeTree = getChangesTree(state, filter);
+  // Get user prefs about indentation style.
+  const { indentUnit, indentWithTabs } = getTabPrefs();
+  const indentChar = indentWithTabs ? "\t".repeat(indentUnit) : " ".repeat(indentUnit);
+
+  function writeRule(ruleId, rule, level) {
+    // Write nested rules, if any.
+    let ruleBody = rule.children.reduce((str, childRule) => {
+      str += writeRule(childRule.ruleId, childRule, level + 1);
+      return str;
+    }, "");
+
+    // Write changed CSS declarations.
+    ruleBody += writeDeclarations(rule.remove, rule.add, level + 1);
+
+    const indent = indentChar.repeat(level);
+    return `\n${indent}${rule.selector} {${ruleBody}\n${indent}}`;
+  }
+
+  function writeDeclarations(remove = [], add = [], level) {
+    const indent = indentChar.repeat(level);
+    const removals = remove
+      // Sort declarations in the order in which they exist in the original CSS rule.
+      .sort((a, b) => a.index > b.index)
+      .reduce((str, { property, value }) => {
+        str += `\n${indent}/* ${property}: ${value}; */`;
+        return str;
+      }, "");
+
+    const additions = add
+      // Sort declarations in the order in which they exist in the original CSS rule.
+      .sort((a, b) => a.index > b.index)
+      .reduce((str, { property, value }) => {
+        str += `\n${indent}${property}: ${value};`;
+        return str;
+      }, "");
+
+    return removals + additions;
+  }
+
+  // Iterate through all sources in the change tree and build a CSS stylesheet string.
+  return Object.entries(changeTree).reduce((stylesheetText, [sourceId, source]) => {
+    const { href, rules } = source;
+    // Write code comment with source origin
+    stylesheetText += `/* ${getSourceForDisplay(source)} | ${href} */\n`;
+    // Write CSS rules
+    stylesheetText += Object.entries(rules).reduce((str, [ruleId, rule]) => {
+      str += writeRule(ruleId, rule, 0);
+      return str;
+    }, "");
+
+    return stylesheetText;
+  }, "");
+}
+
 module.exports = {
   getChangesTree,
+  getChangesStylesheet,
 };