Bug 1158288 - Show color swatch in drop-shadow function. r=pbrosset
authorTom Tromey <tromey@mozilla.com>
Mon, 08 Jun 2015 12:26:00 -0400
changeset 248138 d94caa738b3fb71b96630ff4c87841c21f3033d8
parent 248137 34d7922bed374935df91f8b162e4b49b7c7c03ca
child 248139 846253a39b05f5449f8750d92dcbdb7a480388d4
push id60888
push userkwierso@gmail.com
push dateThu, 11 Jun 2015 01:38:38 +0000
treeherdermozilla-inbound@39e638ed06bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbrosset
bugs1158288
milestone41.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 1158288 - Show color swatch in drop-shadow function. r=pbrosset
browser/devtools/shared/test/browser_outputparser.js
browser/devtools/shared/widgets/Tooltip.js
browser/devtools/styleinspector/rule-view.js
toolkit/devtools/output-parser.js
--- a/browser/devtools/shared/test/browser_outputparser.js
+++ b/browser/devtools/shared/test/browser_outputparser.js
@@ -87,24 +87,23 @@ function testParseCssProperty(doc, parse
                    ", 2px 2px 0 0 ",
                    {name: "rgba(0,0,0,.5)", value: "rgba(0,0,0,.5)"}]),
 
     makeColorTest("content", "\"red\"", ["\"red\""]),
 
     // Invalid property names should not cause exceptions.
     makeColorTest("hellothere", "'red'", ["'red'"]),
 
-    // This requires better parsing than we currently have available.
-    // See bug 1158288.
-    // makeColorTest("filter",
-    //               "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
-    //               ["blur(1px) drop-shadow(0 0 0 ",
-    //                {name: "blue", value: "#00F"},
-    //                ") url(red.svg#blue)"])
-
+    makeColorTest("filter",
+                  "blur(1px) drop-shadow(0 0 0 blue) url(red.svg#blue)",
+                  ["<span data-filters=\"blur(1px) drop-shadow(0 0 0 blue) ",
+                   "url(red.svg#blue)\"><span>",
+                   "blur(1px) drop-shadow(0 0 0 ",
+                   {name: "blue", value: "#00F"},
+                   ") url(\"red.svg#blue\")</span></span>"])
   ];
 
   let target = doc.querySelector("div");
   ok(target, "captain, we have the div");
 
   for (let test of tests) {
     info(test.desc);
 
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -1662,27 +1662,55 @@ SwatchFilterTooltip.prototype = Heritage
     }
   },
 
   _onUpdate: function(event, filters) {
     if (!this.activeSwatch) {
       return;
     }
 
-    this.currentFilterValue.textContent = filters;
+    // Remove the old children and reparse the property value to
+    // recompute them.
+    while (this.currentFilterValue.firstChild) {
+      this.currentFilterValue.firstChild.remove();
+    }
+    let node = this._parser.parseCssProperty("filter", filters, this._options);
+    this.currentFilterValue.appendChild(node);
+
     this.preview();
   },
 
   destroy: function() {
     SwatchBasedEditorTooltip.prototype.destroy.call(this);
     this.currentFilterValue = null;
     this.widget.then(widget => {
       widget.off("updated", this._onUpdate);
       widget.destroy();
     });
+  },
+
+  /**
+   * Like SwatchBasedEditorTooltip.addSwatch, but accepts a parser object
+   * to use when previewing the updated property value.
+   *
+   * @param {node} swatchEl
+   *        @see SwatchBasedEditorTooltip.addSwatch
+   * @param {object} callbacks
+   *        @see SwatchBasedEditorTooltip.addSwatch
+   * @param {object} parser
+   *        A parser object; @see OutputParser object
+   * @param {object} options
+   *        options to pass to the output parser, with
+   *          the option |filterSwatch| set.
+   */
+  addSwatch: function(swatchEl, callbacks, parser, options) {
+    SwatchBasedEditorTooltip.prototype.addSwatch.call(this, swatchEl,
+                                                      callbacks);
+    this._parser = parser;
+    this._options = options;
   }
 });
 
 /**
  * L10N utility class
  */
 function L10N() {}
 L10N.prototype = {};
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -3116,27 +3116,28 @@ TextPropertyEditor.prototype = {
     }
 
     const sharedSwatchClass = "ruleview-swatch ";
     const colorSwatchClass = "ruleview-colorswatch";
     const bezierSwatchClass = "ruleview-bezierswatch";
     const filterSwatchClass = "ruleview-filterswatch";
 
     let outputParser = this.ruleEditor.ruleView._outputParser;
-    let frag = outputParser.parseCssProperty(name, val, {
+    let parserOptions = {
       colorSwatchClass: sharedSwatchClass + colorSwatchClass,
       colorClass: "ruleview-color",
       bezierSwatchClass: sharedSwatchClass + bezierSwatchClass,
       bezierClass: "ruleview-bezier",
       filterSwatchClass: sharedSwatchClass + filterSwatchClass,
       filterClass: "ruleview-filter",
       defaultColorType: !propDirty,
       urlClass: "theme-link",
       baseURI: this.sheetURI
-    });
+    };
+    let frag = outputParser.parseCssProperty(name, val, parserOptions);
     this.valueSpan.innerHTML = "";
     this.valueSpan.appendChild(frag);
 
     // Attach the color picker tooltip to the color swatches
     this._colorSwatchSpans =
       this.valueSpan.querySelectorAll("." + colorSwatchClass);
     if (this.ruleEditor.isEditable) {
       for (let span of this._colorSwatchSpans) {
@@ -3168,23 +3169,24 @@ TextPropertyEditor.prototype = {
         });
       }
     }
 
     // Attach the filter editor tooltip to the filter swatch
     let span = this.valueSpan.querySelector("." + filterSwatchClass);
     if (this.ruleEditor.isEditable) {
       if (span) {
+        parserOptions.filterSwatch = true;
         let originalValue = this.valueSpan.textContent;
 
         this.ruleEditor.ruleView.tooltips.filterEditor.addSwatch(span, {
           onPreview: () => this._previewValue(this.valueSpan.textContent),
           onCommit: () => this._applyNewValue(this.valueSpan.textContent),
           onRevert: () => this._applyNewValue(originalValue, false)
-        });
+        }, outputParser, parserOptions);
       }
     }
 
     // Populate the computed styles.
     this._updateComputed();
   },
 
   _onStartEditing: function() {
--- a/toolkit/devtools/output-parser.js
+++ b/toolkit/devtools/output-parser.js
@@ -20,17 +20,18 @@ const BEZIER_KEYWORDS = ["linear", "ease
 // Functions that accept a color argument.
 const COLOR_TAKING_FUNCTIONS = ["linear-gradient",
                                 "-moz-linear-gradient",
                                 "repeating-linear-gradient",
                                 "-moz-repeating-linear-gradient",
                                 "radial-gradient",
                                 "-moz-radial-gradient",
                                 "repeating-radial-gradient",
-                                "-moz-repeating-radial-gradient"];
+                                "-moz-repeating-radial-gradient",
+                                "drop-shadow"];
 
 loader.lazyGetter(this, "DOMUtils", function() {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
 /**
  * This module is used to process text for output by developer tools. This means
  * linking JS files with the debugger, CSS files with the style editor, JS
@@ -138,104 +139,124 @@ OutputParser.prototype = {
    *         _mergeOptions().
    * @return {DocumentFragment}
    *         A document fragment.
    */
   _parse: function(text, options={}) {
     text = text.trim();
     this.parsed.length = 0;
 
-    if (options.expectFilter) {
-      this._appendFilter(text, options);
-    } else {
-      let tokenStream = DOMUtils.getCSSLexer(text);
-      let i = 0;
-      while (true) {
-        let token = tokenStream.nextToken();
-        if (!token) {
+    let tokenStream = DOMUtils.getCSSLexer(text);
+    let i = 0;
+    let parenDepth = 0;
+    let outerMostFunctionTakesColor = false;
+
+    let colorOK = function() {
+      return options.supportsColor ||
+        (options.expectFilter && parenDepth === 1 &&
+         outerMostFunctionTakesColor);
+    };
+
+    while (true) {
+      let token = tokenStream.nextToken();
+      if (!token) {
+        break;
+      }
+      if (token.tokenType === "comment") {
+        continue;
+      }
+
+      // Prevent this loop from slowing down the browser with too
+      // many nodes being appended into output. In practice it is very unlikely
+      // that this will ever happen.
+      i++;
+      if (i > MAX_ITERATIONS) {
+        this._appendTextNode(text.substring(token.startOffset,
+                                            token.endOffset));
+        continue;
+      }
+
+      switch (token.tokenType) {
+        case "function": {
+          if (COLOR_TAKING_FUNCTIONS.indexOf(token.text) >= 0) {
+            // The function can accept a color argument, and we know
+            // it isn't special in some other way.  So, we let it
+            // through to the ordinary parsing loop so that colors
+            // can be handled in a single place.
+            this._appendTextNode(text.substring(token.startOffset,
+                                                token.endOffset));
+            if (parenDepth === 0) {
+              outerMostFunctionTakesColor = true;
+            }
+            ++parenDepth;
+          } else {
+            let functionText = this._collectFunctionText(token, text,
+                                                         tokenStream);
+
+            if (options.expectCubicBezier && token.text === "cubic-bezier") {
+              this._appendCubicBezier(functionText, options);
+            } else if (colorOK() && DOMUtils.isValidCSSColor(functionText)) {
+              this._appendColor(functionText, options);
+            } else {
+              this._appendTextNode(functionText);
+            }
+          }
           break;
         }
-        if (token.tokenType === "comment") {
-          continue;
-        }
-
-        // Prevent this loop from slowing down the browser with too
-        // many nodes being appended to the output. In practice it is
-        // very unlikely that this will ever happen.
-        i++;
-        if (i > MAX_ITERATIONS) {
-          this._appendTextNode(text.substring(token.startOffset,
-                                              token.endOffset));
-          continue;
-        }
 
-        switch (token.tokenType) {
-          case "function": {
-            if (COLOR_TAKING_FUNCTIONS.indexOf(token.text) >= 0) {
-              // The function can accept a color argument, and we know
-              // it isn't special in some other way.  So, we let it
-              // through to the ordinary parsing loop so that colors
-              // can be handled in a single place.
-              this._appendTextNode(text.substring(token.startOffset,
-                                                  token.endOffset));
-            } else {
-              let functionText = this._collectFunctionText(token, text,
-                                                           tokenStream);
-
-              if (options.expectCubicBezier && token.text === "cubic-bezier") {
-                this._appendCubicBezier(functionText, options);
-              } else if (options.supportsColor &&
-                         DOMUtils.isValidCSSColor(functionText)) {
-                this._appendColor(functionText, options);
-              } else {
-                this._appendTextNode(functionText);
-              }
-            }
-            break;
-          }
-
-          case "ident":
-            if (options.expectCubicBezier &&
-                BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
-              this._appendCubicBezier(token.text, options);
-            } else if (options.supportsColor &&
-                       DOMUtils.isValidCSSColor(token.text)) {
-              this._appendColor(token.text, options);
-            } else {
-              this._appendTextNode(text.substring(token.startOffset,
-                                                  token.endOffset));
-            }
-            break;
-
-          case "id":
-          case "hash": {
-            let original = text.substring(token.startOffset, token.endOffset);
-            if (options.supportsColor && DOMUtils.isValidCSSColor(original)) {
-              this._appendColor(original, options);
-            } else {
-              this._appendTextNode(original);
-            }
-            break;
-          }
-
-          case "url":
-          case "bad_url":
-            this._appendURL(text.substring(token.startOffset, token.endOffset),
-                            token.text, options);
-            break;
-
-          default:
+        case "ident":
+          if (options.expectCubicBezier &&
+              BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
+            this._appendCubicBezier(token.text, options);
+          } else if (colorOK() && DOMUtils.isValidCSSColor(token.text)) {
+            this._appendColor(token.text, options);
+          } else {
             this._appendTextNode(text.substring(token.startOffset,
                                                 token.endOffset));
-            break;
+          }
+          break;
+
+        case "id":
+        case "hash": {
+          let original = text.substring(token.startOffset, token.endOffset);
+          if (colorOK() && DOMUtils.isValidCSSColor(original)) {
+            this._appendColor(original, options);
+          } else {
+            this._appendTextNode(original);
+          }
+          break;
         }
+
+        case "url":
+        case "bad_url":
+          this._appendURL(text.substring(token.startOffset, token.endOffset),
+                          token.text, options);
+          break;
+
+        case "symbol":
+          if (token.text === "(") {
+            ++parenDepth;
+          } else if (token.token === ")") {
+            --parenDepth;
+          }
+          // falls through
+        default:
+          this._appendTextNode(text.substring(token.startOffset,
+                                              token.endOffset));
+          break;
       }
     }
 
-    return this._toDOM();
+    let result = this._toDOM();
+
+    if (options.expectFilter && !options.filterSwatch) {
+      result = this._wrapFilter(text, options, result);
+    }
+
+    return result;
   },
 
   /**
    * Append a cubic-bezier timing function value to the output
    *
    * @param {String} bezier
    *        The cubic-bezier timing function
    * @param {Object} options
@@ -325,34 +346,48 @@ OutputParser.prototype = {
 
       container.appendChild(value);
       this.parsed.push(container);
       return true;
     }
     return false;
   },
 
-  _appendFilter: function(filters, options={}) {
+  /**
+   * 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().
+   *
+   * @returns {object}
+   *        A new node that supplies a filter swatch and that wraps |nodes|.
+   */
+  _wrapFilter: function(filters, options, nodes) {
     let container = this._createNode("span", {
       "data-filters": filters
     });
 
     if (options.filterSwatchClass) {
       let swatch = this._createNode("span", {
         class: options.filterSwatchClass
       });
       container.appendChild(swatch);
     }
 
     let value = this._createNode("span", {
       class: options.filterClass
-    }, filters);
+    });
+    value.appendChild(nodes);
+    container.appendChild(value);
 
-    container.appendChild(value);
-    this.parsed.push(container);
+    return container;
   },
 
   _onSwatchMouseDown: function(event) {
     // Prevent text selection in the case of shift-click or double-click.
     event.preventDefault();
 
     if (!event.shiftKey) {
       return;
@@ -498,29 +533,35 @@ OutputParser.prototype = {
    *                                    // that follows the swatch.
    *           - bezierSwatchClass: ""  // The class to use for bezier swatches.
    *           - bezierClass: ""        // The class to use for the bezier value
    *                                    // that follows the swatch.
    *           - supportsColor: false   // Does the CSS property support colors?
    *           - urlClass: ""           // The class to be used for url() links.
    *           - baseURI: ""            // A string or nsIURI used to resolve
    *                                    // relative links.
+   *           - filterSwatch: false    // A special case for parsing a
+   *                                    // "filter" property, causing the
+   *                                    // parser to skip the call to
+   *                                    // _wrapFilter.  Used only for
+   *                                    // previewing with the filter swatch.
    * @return {Object}
    *         Overridden options object
    */
   _mergeOptions: function(overrides) {
     let defaults = {
       defaultColorType: true,
       colorSwatchClass: "",
       colorClass: "",
       bezierSwatchClass: "",
       bezierClass: "",
       supportsColor: false,
       urlClass: "",
-      baseURI: ""
+      baseURI: "",
+      filterSwatch: false
     };
 
     if (typeof overrides.baseURI === "string") {
       overrides.baseURI = Services.io.newURI(overrides.baseURI, null, null);
     }
 
     for (let item in overrides) {
       defaults[item] = overrides[item];