Bug 1413310 - Part 1: Add swatches for CSS variables r?tromey draft
authorDarren Hobin <darrenhobin@live.com>
Sat, 23 Sep 2017 12:26:26 -0400
changeset 712500 58efd00ee22c3852d2fbdaa3da9b4fbff11da59e
parent 712435 4780efa5f11052cfe775e1d1e76d85cfc0bfc1ea
child 712501 79e63ff17f0c93e9da589a6759f1b5b2e66487b9
push id93349
push userbmo:darrenhobin@live.com
push dateSat, 16 Dec 2017 21:41:04 +0000
reviewerstromey
bugs1413310
milestone59.0a1
Bug 1413310 - Part 1: Add swatches for CSS variables r?tromey MozReview-Commit-ID: 6Z5vqbYiCWq
devtools/client/shared/output-parser.js
devtools/client/themes/rules.css
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -2,16 +2,17 @@
  * 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";
 
 const Services = require("Services");
 const {angleUtils} = require("devtools/client/shared/css-angle");
 const {colorUtils} = require("devtools/shared/css/color");
+const cssPropertiesSpec = require("devtools/shared/fronts/css-properties");
 const {getCSSLexer} = require("devtools/shared/css/lexer");
 const EventEmitter = require("devtools/shared/old-event-emitter");
 const {appendText} = require("devtools/client/inspector/shared/utils");
 
 loader.lazyRequireGetter(this, "ANGLE_TAKING_FUNCTIONS",
   "devtools/shared/css/properties-db", true);
 loader.lazyRequireGetter(this, "BASIC_SHAPE_FUNCTIONS",
   "devtools/shared/css/properties-db", true);
@@ -59,16 +60,18 @@ const CSS_SHAPE_OUTSIDE_ENABLED_PREF = "
 function OutputParser(document,
                       {supportsType, isValidOnClient, supportsCssColor4ColorFunction}) {
   this.parsed = [];
   this.doc = document;
   this.supportsType = supportsType;
   this.isValidOnClient = isValidOnClient;
   this.colorSwatches = new WeakMap();
   this.angleSwatches = new WeakMap();
+  // Keep track of last variable value computed while parsing
+  this.computedVariable = null;
   this._onColorSwatchMouseDown = this._onColorSwatchMouseDown.bind(this);
   this._onAngleSwatchMouseDown = this._onAngleSwatchMouseDown.bind(this);
 
   this.cssColor4 = supportsCssColor4ColorFunction();
 }
 
 OutputParser.prototype = {
   /**
@@ -199,17 +202,17 @@ OutputParser.prototype = {
    *         title. Eg. a span with "var(--var1)" as the textContent
    *         and a title for --var1 like "--var1 = 10" or
    *         "--var1 is not set".
    */
   _parseVariable: function (initialToken, text, tokenStream, options) {
     // Handle the "var(".
     let varText = text.substring(initialToken.startOffset,
                                  initialToken.endOffset);
-    let variableNode = this._createNode("span", {}, varText);
+    let variableNode = this._createNode("span", {});
 
     // Parse the first variable name within the parens of var().
     let {tokens, functionData, sawComma, sawVariable} =
         this._parseMatchingParens(text, tokenStream, options, true);
 
     let result = sawVariable ? "" : functionData.join("");
 
     // Display options for the first and second argument in the var().
@@ -221,50 +224,59 @@ OutputParser.prototype = {
     // Get the variable value if it is in use.
     if (tokens && tokens.length === 1) {
       varValue = options.isVariableInUse(tokens[0].text);
     }
 
     // Get the variable name.
     let varName = text.substring(tokens[0].startOffset, tokens[0].endOffset);
 
+    // Reset swatch variable, may be set in future call to _doParse().
+    this.computedVariable = null;
+
+    // If we saw a ",", get the remainder with the correct highlighting.
+    let rest;
+    if (sawComma) {
+      // Parse the text up until the close paren, being sure to
+      // disable the special case for filter.
+      let subOptions = Object.assign({}, options);
+      subOptions.expectFilter = false;
+      let saveParsed = this.parsed;
+      this.parsed = [];
+      subOptions.expectVar = true;
+      rest = this._doParse(text, subOptions, tokenStream, true);
+      this.parsed = saveParsed;
+    }
+
     if (typeof varValue === "string") {
       // The variable value is valid, set the variable name's title of the first argument
       // in var() to display the variable name and value.
       firstOpts["data-variable"] =
         STYLE_INSPECTOR_L10N.getFormatStr("rule.variableValue", varName, varValue);
+      this.computedVariable = varValue;
       firstOpts.class = options.matchedVariableClass;
       secondOpts.class = options.unmatchedVariableClass;
     } else {
       // The variable name is not valid, mark it unmatched.
       firstOpts.class = options.unmatchedVariableClass;
       firstOpts["data-variable"] = STYLE_INSPECTOR_L10N.getFormatStr("rule.variableUnset",
                                                                       varName);
     }
 
+    this._appendVariable(this.computedVariable, variableNode);
+    appendText(variableNode, varText);
     variableNode.appendChild(this._createNode("span", firstOpts, result));
 
-    // If we saw a ",", then append it and show the remainder using
-    // the correct highlighting.
     if (sawComma) {
       variableNode.appendChild(this.doc.createTextNode(","));
-
-      // Parse the text up until the close paren, being sure to
-      // disable the special case for filter.
-      let subOptions = Object.assign({}, options);
-      subOptions.expectFilter = false;
-      let saveParsed = this.parsed;
-      this.parsed = [];
-      let rest = this._doParse(text, subOptions, tokenStream, true);
-      this.parsed = saveParsed;
-
       let span = this._createNode("span", secondOpts);
       span.appendChild(rest);
       variableNode.appendChild(span);
     }
+
     variableNode.appendChild(this.doc.createTextNode(")"));
 
     return variableNode;
   },
 
   /* eslint-disable complexity */
   /**
    * The workhorse for @see _parse. This parses some CSS text,
@@ -359,68 +371,99 @@ OutputParser.prototype = {
                 this._appendColor(functionText, options);
               } else if (options.expectShape &&
                          Services.prefs.getBoolPref(CSS_SHAPES_ENABLED_PREF) &&
                          BASIC_SHAPE_FUNCTIONS.includes(token.text)) {
                 this._appendShape(functionText, options);
               } else {
                 this._appendTextNode(functionText);
               }
+
+              if (options.expectVar) {
+                this.computedVariable = text.substring(token.startOffset, token.endOffset);
+              }
             }
           }
           break;
         }
 
         case "ident":
+          let tokenSubstring = text.substring(token.startOffset, token.endOffset)
+          let isCssVariable = cssPropertiesSpec.isCssVariable(tokenSubstring);
+
+          if (options.expectVar) {
+            this.computedVariable = text.substring(token.startOffset, token.endOffset);
+          }
+
           if (options.expectCubicBezier &&
               BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
             this._appendCubicBezier(token.text, options);
           } else if (this._isDisplayFlex(text, token, options) &&
                      Services.prefs.getBoolPref(FLEXBOX_HIGHLIGHTER_ENABLED_PREF)) {
             this._appendHighlighterToggle(token.text, options.flexClass);
           } else if (this._isDisplayGrid(text, token, options)) {
             this._appendHighlighterToggle(token.text, options.gridClass);
           } else if (colorOK() &&
                      colorUtils.isValidCSSColor(token.text, this.cssColor4)) {
             this._appendColor(token.text, options);
           } else if (angleOK(token.text)) {
             this._appendAngle(token.text, options);
+          } else if (options.expectVar && isCssVariable) {
+            let variableNode = this._createNode("span", {});
+            this.computedVariable = options.isVariableInUse(tokenSubstring) || null;
+            this._appendVariable(this.computedVariable, variableNode);
+            this.parsed.push(variableNode);
+            this._appendTextNode(tokenSubstring);
           } else {
-            this._appendTextNode(text.substring(token.startOffset,
-                                                token.endOffset));
+            this._appendTextNode(tokenSubstring);
           }
           break;
 
         case "id":
         case "hash": {
           let original = text.substring(token.startOffset, token.endOffset);
           if (colorOK() && colorUtils.isValidCSSColor(original, this.cssColor4)) {
             if (spaceNeeded) {
               // Insert a space to prevent token pasting when a #xxx
               // color is changed to something like rgb(...).
               this._appendTextNode(" ");
             }
             this._appendColor(original, options);
           } else {
             this._appendTextNode(original);
           }
+
+          if (options.expectVar) {
+            this.computedVariable = text.substring(token.startOffset, token.endOffset);
+          }
+
           break;
         }
         case "dimension":
           let value = text.substring(token.startOffset, token.endOffset);
           if (angleOK(value)) {
             this._appendAngle(value, options);
           } else {
             this._appendTextNode(value);
           }
+
+          if (options.expectVar) {
+            this.computedVariable = text.substring(token.startOffset, token.endOffset);
+          }
+
           break;
         case "url":
         case "bad_url":
           this._appendURL(text.substring(token.startOffset, token.endOffset),
                           token.text, options);
+
+          if (options.expectVar) {
+            this.computedVariable = text.substring(token.startOffset, token.endOffset);
+          }
+
           break;
 
         case "symbol":
           if (token.text === "(") {
             ++parenDepth;
           } else if (token.text === ")") {
             --parenDepth;
 
@@ -432,16 +475,21 @@ OutputParser.prototype = {
             if (parenDepth === 0) {
               outerMostFunctionTakesColor = false;
             }
           }
           // falls through
         default:
           this._appendTextNode(
             text.substring(token.startOffset, token.endOffset));
+
+          if (options.expectVar) {
+            this.computedVariable = text.substring(token.startOffset, token.endOffset);
+          }
+
           break;
       }
 
       // If this token might possibly introduce token pasting when
       // color-cycling, require a space.
       spaceNeeded = (token.tokenType === "ident" || token.tokenType === "at" ||
                      token.tokenType === "id" || token.tokenType === "hash" ||
                      token.tokenType === "number" || token.tokenType === "dimension" ||
@@ -1197,16 +1245,43 @@ OutputParser.prototype = {
       container.appendChild(value);
       this.parsed.push(container);
     } else {
       this._appendTextNode(color);
     }
   },
 
   /**
+   * Append a swatch for a css variable to a given node.
+   *
+   * @param  {String} variable
+   *         Value of the variable to append.
+   *         null if the variable has no value.
+   * @param  {Node} node
+   *         Node to append swatch to.
+   */
+  _appendVariable: function (variable, node) {
+    let swatch;
+
+    if (variable === null) {
+      swatch = this._createNode("span", {
+        class: "ruleview-swatch ruleview-variableswatch-unmatched",
+        title: "Cannot be resolved"
+      });
+    } else {
+      swatch = this._createNode("span", {
+        class: "ruleview-swatch ruleview-variableswatch-matched",
+        title: variable
+      });
+    }
+
+    node.appendChild(swatch);
+  },
+
+  /**
    * Wrap some existing nodes in a filter editor.
    *
    * @param {String} filters
    *        The full text of the "filter" property.
    * @param {object} options
    *        The options object passed to parseCssProperty().
    * @param {object} nodes
    *        Nodes created by _toDOM().
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -486,16 +486,29 @@
 .ruleview-shape {
   background: url("chrome://devtools/skin/images/tool-shadereditor.svg");
   -moz-context-properties: fill;
   fill: var(--rule-shape-toggle-color);
   border-radius: 0;
   background-size: 1em;
 }
 
+/* Temporarily use meaningless svg, replace in future */
+.ruleview-variableswatch-matched {
+  background: url("chrome://devtools/skin/images/grid.svg");
+  border-radius: 0;
+}
+
+/* Temporarily use meaningless svg, replace in future */
+.ruleview-variableswatch-unmatched {
+  background: url("chrome://devtools/skin/images/tool-shadereditor.svg");
+  border-radius: 0;
+  background-size: 1em;
+}
+
 .ruleview-shape-point.active {
   background-color: var(--rule-highlight-background-color);
 }
 
 .ruleview-colorswatch::before {
   content: '';
   background-color: #eee;
   background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),