Bug 918716 - Add color swatches to devtools output parser. r=jwalker
☠☠ backed out by e5b6c39b7fbe ☠ ☠
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Thu, 17 Oct 2013 19:03:52 +0100
changeset 151122 2601d36dc1f12d8c5c42e183b2d26aeca55cab1a
parent 151121 1374158dab4b8d334ca41a19405a97a92cd0f084
child 151123 7063d9f5c1fdd312bdc8403fc6cd34e30d76a782
push id3096
push userryanvm@gmail.com
push dateThu, 17 Oct 2013 18:27:38 +0000
treeherderfx-team@62732da6ae3d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs918716
milestone27.0a1
Bug 918716 - Add color swatches to devtools output parser. r=jwalker
browser/devtools/markupview/markup-view.js
browser/devtools/shared/test/browser_css_color.js
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/computedview.xhtml
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/ruleview.css
browser/devtools/styleinspector/test/browser_ruleview_bug_902966_revert_value_on_ESC.js
browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
browser/themes/linux/devtools/computedview.css
browser/themes/linux/devtools/ruleview.css
browser/themes/osx/devtools/computedview.css
browser/themes/osx/devtools/ruleview.css
browser/themes/shared/devtools/dark-theme.css
browser/themes/shared/devtools/light-theme.css
browser/themes/windows/devtools/computedview.css
browser/themes/windows/devtools/ruleview.css
toolkit/devtools/Loader.jsm
toolkit/devtools/css-color.js
toolkit/devtools/output-parser.js
toolkit/devtools/server/actors/styles.js
toolkit/devtools/styleinspector/css-logic.js
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -13,16 +13,17 @@ const DEFAULT_MAX_CHILDREN = 100;
 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
 const COLLAPSE_DATA_URL_LENGTH = 60;
 const CONTAINER_FLASHING_DURATION = 500;
 
 const {UndoStack} = require("devtools/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {OutputParser} = require("devtools/output-parser");
 const promise = require("sdk/core/promise");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
@@ -50,16 +51,17 @@ loader.lazyGetter(this, "AutocompletePop
  *        An iframe in which the caller has kindly loaded markup-view.xhtml.
  */
 function MarkupView(aInspector, aFrame, aControllerWindow) {
   this._inspector = aInspector;
   this.walker = this._inspector.walker;
   this._frame = aFrame;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
+  this._outputParser = new OutputParser();
 
   this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
   } catch(ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
@@ -781,16 +783,18 @@ MarkupView.prototype = {
     delete this.undo;
 
     this.popup.destroy();
     delete this.popup;
 
     this._frame.removeEventListener("focus", this._boundFocus, false);
     delete this._boundFocus;
 
+    delete this._outputParser;
+
     if (this._boundUpdatePreview) {
       this._frame.contentWindow.removeEventListener("scroll",
         this._boundUpdatePreview, true);
       delete this._boundUpdatePreview;
     }
 
     if (this._boundResizePreview) {
       this._frame.contentWindow.removeEventListener("resize",
@@ -1532,28 +1536,98 @@ ElementEditor.prototype = {
     // Remove the old version of this attribute from the DOM.
     let oldAttr = this.attrs[aAttr.name];
     if (oldAttr && oldAttr.parentNode) {
       oldAttr.parentNode.removeChild(oldAttr);
     }
 
     this.attrs[aAttr.name] = attr;
 
-    let collapsedValue;
-    if (aAttr.value.match(COLLAPSE_DATA_URL_REGEX)) {
-      collapsedValue = truncateString(aAttr.value, COLLAPSE_DATA_URL_LENGTH);
+    name.textContent = aAttr.name;
+
+    if (typeof aAttr.value !== "undefined") {
+      let outputParser = this.markup._outputParser;
+      let frag = outputParser.parseHTMLAttribute(aAttr.value);
+      frag = this._truncateFrag(frag);
+      val.appendChild(frag);
     }
-    else {
-      collapsedValue = truncateString(aAttr.value, COLLAPSE_ATTRIBUTE_LENGTH);
+
+    return attr;
+  },
+
+  /**
+   * We truncate HTML attributes to a text length defined by
+   * COLLAPSE_DATA_URL_LENGTH and COLLAPSE_ATTRIBUTE_LENGTH. Because we parse
+   * text into document fragments we need to process each fragment and truncate
+   * according to the fragment's textContent length.
+   *
+   * @param  {DocumentFragment} frag
+   *         The fragment to truncate.
+   * @return {[DocumentFragment]}
+   *         Truncated fragment.
+   */
+  _truncateFrag: function(frag) {
+    let chars = 0;
+    let text = frag.textContent;
+    let maxWidth = text.match(COLLAPSE_DATA_URL_REGEX) ?
+                            COLLAPSE_DATA_URL_LENGTH : COLLAPSE_ATTRIBUTE_LENGTH;
+    let overBy = text.length - maxWidth;
+    let children = frag.childNodes;
+    let croppedNode = null;
+
+    if (overBy <= 0) {
+      return frag;
     }
 
-    name.textContent = aAttr.name;
-    val.textContent = collapsedValue;
+    // For fragments containing only one single node we just need to truncate
+    // frag.textContent.
+    if (children.length === 1) {
+      let length = text.length;
+      let start = text.substr(0, maxWidth / 2);
+      let end = text.substr(length - maxWidth / 2, length - 1);
+
+      frag.textContent = start + "…" + end;
+      return frag;
+    }
+
+    // First maxWidth / 2 chars plus &hellip;
+    for (let i = 0; i < children.length; i++) {
+      let node = children[i];
+      let text = node.textContent;
 
-    return attr;
+      let numChars = text.length;
+      if (chars + numChars > maxWidth / 2) {
+        node.textContent = text.substr(0, chars + numChars - maxWidth / 2) + "…";
+        croppedNode = node;
+        break;
+      } else {
+        chars += numChars;
+      }
+    }
+
+    // Last maxWidth / two chars.
+    chars = 0;
+    for (let i = children.length - 1; i >= 0; i--) {
+      let node = children[i];
+      let text = node.textContent;
+
+      let numChars = text.length;
+      if (chars + numChars > maxWidth / 2) {
+        if (node !== croppedNode) {
+          node.parentNode.removeChild(node);
+          chars += numChars;
+        } else {
+          break;
+        }
+      } else {
+        chars += numChars;
+      }
+    }
+
+    return frag;
   },
 
   /**
    * Parse a user-entered attribute string and apply the resulting
    * attributes to the node.  This operation is undoable.
    *
    * @param string aValue the user-entered value.
    * @param Element aAttrNode the attribute editor that created this
--- a/browser/devtools/shared/test/browser_css_color.js
+++ b/browser/devtools/shared/test/browser_css_color.js
@@ -134,17 +134,17 @@ function testProcessCSSString() {
                  "background-color: transparent; " +
                  "border-top-color: rgba(0, 0, 255, 0.5);";
   let after = colorUtils.processCSSString(before);
 
   is(after, expected, "CSS string processed correctly");
 }
 
 function finishUp() {
-  Services = colorUtils.CssColor = Loader = null;
+  Services = colorUtils = Loader = null;
   gBrowser.removeCurrentTab();
   finish();
 }
 
 function getTestData() {
   return [
     {authored: "aliceblue", name: "aliceblue", hex: "#F0F8FF", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
     {authored: "antiquewhite", name: "antiquewhite", hex: "#FAEBD7", hsl: "hsl(34.286, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -6,17 +6,18 @@
 
 const {Cc, Ci, Cu} = require("chrome");
 
 let ToolDefinitions = require("main").Tools;
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
 let promise = require("sdk/core/promise");
 let {EventEmitter} = require("devtools/shared/event-emitter");
-let {colorUtils} = require("devtools/css-color");
+
+const {OutputParser} = require("devtools/output-parser");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
@@ -129,16 +130,18 @@ UpdateProcess.prototype = {
 function CssHtmlTree(aStyleInspector, aPageStyle)
 {
   this.styleWindow = aStyleInspector.window;
   this.styleDocument = aStyleInspector.window.document;
   this.styleInspector = aStyleInspector;
   this.pageStyle = aPageStyle;
   this.propertyViews = [];
 
+  this._outputParser = new OutputParser();
+
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
     getService(Ci.nsIXULChromeRegistry);
   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
 
   // Create bound methods.
   this.focusWindow = this.focusWindow.bind(this);
   this._onContextMenu = this._onContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
@@ -608,16 +611,18 @@ CssHtmlTree.prototype = {
 
   /**
    * Destructor for CssHtmlTree.
    */
   destroy: function CssHtmlTree_destroy()
   {
     delete this.viewedElement;
 
+    delete this._outputParser;
+
     // Remove event listeners
     this.includeBrowserStylesCheckbox.removeEventListener("command",
       this.includeBrowserStylesChanged);
     this.searchField.removeEventListener("command", this.filterChanged);
     gDevTools.off("pref-changed", this._handlePrefChange);
 
     // Cancel tree construction
     if (this._createViewsProcess) {
@@ -671,17 +676,17 @@ CssHtmlTree.prototype = {
 function PropertyInfo(aTree, aName) {
   this.tree = aTree;
   this.name = aName;
 }
 PropertyInfo.prototype = {
   get value() {
     if (this.tree._computed) {
       let value = this.tree._computed[this.name].value;
-      return colorUtils.processCSSString(value);
+      return value;
     }
   }
 };
 
 function createMenuItem(aMenu, aAttributes)
 {
   let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
 
@@ -867,17 +872,16 @@ PropertyView.prototype = {
 
     // Build the style value element
     this.valueNode = doc.createElementNS(HTML_NS, "div");
     this.valueNode.setAttribute("class", "property-value theme-fg-color1");
     // Reset its tabindex attribute otherwise, if an ellipsis is applied
     // it will be reachable via TABing
     this.valueNode.setAttribute("tabindex", "");
     this.valueNode.setAttribute("dir", "ltr");
-    this.valueNode.textContent = this.valueNode.title = this.value;
     // Make it hand over the focus to the container
     this.valueNode.addEventListener("click", this.onFocus, false);
     this.element.appendChild(this.valueNode);
 
     return this.element;
   },
 
   buildSelectorContainer: function PropertyView_buildSelectorContainer()
@@ -909,17 +913,26 @@ PropertyView.prototype = {
       this.valueNode.textContent = this.valueNode.title = "";
       this.matchedSelectorsContainer.parentNode.hidden = true;
       this.matchedSelectorsContainer.textContent = "";
       this.matchedExpander.removeAttribute("open");
       return;
     }
 
     this.tree.numVisibleProperties++;
-    this.valueNode.textContent = this.valueNode.title = this.propertyInfo.value;
+
+    let outputParser = this.tree._outputParser;
+    let frag = outputParser.parseCssProperty(this.propertyInfo.name,
+      this.propertyInfo.value,
+      {
+        colorSwatchClass: "computedview-colorswatch"
+      });
+    this.valueNode.innerHTML = "";
+    this.valueNode.appendChild(frag);
+
     this.refreshMatchedSelectors();
   },
 
   /**
    * Refresh the panel matched rules.
    */
   refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
   {
@@ -1109,18 +1122,28 @@ SelectorView.prototype = {
   get sourceText()
   {
     return this.selectorInfo.sourceText;
   },
 
 
   get value()
   {
-    let val = this.selectorInfo.value;
-    return colorUtils.processCSSString(val);
+    return this.selectorInfo.value;
+  },
+
+  get outputFragment()
+  {
+    let outputParser = this.tree._outputParser;
+    let frag = outputParser.parseCssProperty(
+      this.selectorInfo.name,
+      this.selectorInfo.value, {
+      colorSwatchClass: "computedview-colorswatch"
+    });
+    return frag;
   },
 
   maybeOpenStyleEditor: function(aEvent)
   {
     let keyEvent = Ci.nsIDOMKeyEvent;
     if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
       this.openStyleEditor();
     }
--- a/browser/devtools/styleinspector/computedview.xhtml
+++ b/browser/devtools/styleinspector/computedview.xhtml
@@ -98,17 +98,17 @@
               <a target="_blank" class="link theme-link"
                   onclick="${selector.openStyleEditor}"
                   onkeydown="${selector.maybeOpenStyleEditor}"
                   title="${selector.href}"
                   tabindex="0">${selector.source}</a>
             </span>
             <span dir="ltr" class="rule-text ${selector.statusClass} theme-fg-color3" title="${selector.statusText}">
               ${selector.sourceText}
-              <span class="other-property-value theme-fg-color1">${selector.value}</span>
+              <span class="other-property-value theme-fg-color1">${selector.outputFragment}</span>
             </span>
           </p>
         </loop>
       </div>
     </div>
 
   </body>
 </html>
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -7,19 +7,20 @@
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const promise = require("sdk/core/promise");
 
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
 let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
-let {colorUtils} = require("devtools/css-color");
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
+const {OutputParser} = require("devtools/output-parser");
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
  * These regular expressions are adapted from firebug's css.js, and are
@@ -907,17 +908,17 @@ Rule.prototype = {
  * @param {string} aPriority
  *        The property's priority (either "important" or an empty string).
  *
  */
 function TextProperty(aRule, aName, aValue, aPriority)
 {
   this.rule = aRule;
   this.name = aName;
-  this.value = colorUtils.processCSSString(aValue);
+  this.value = aValue;
   this.priority = aPriority;
   this.enabled = true;
   this.updateComputed();
 }
 
 TextProperty.prototype = {
   /**
    * Update the editor associated with this text property,
@@ -1042,16 +1043,18 @@ function CssRuleView(aDoc, aStore, aPage
 {
   this.doc = aDoc;
   this.store = aStore || {};
   this.pageStyle = aPageStyle;
   this.element = this.doc.createElementNS(HTML_NS, "div");
   this.element.className = "ruleview devtools-monospace";
   this.element.flex = 1;
 
+  this._outputParser = new OutputParser();
+
   this._buildContextMenu = this._buildContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onCopy = this._onCopy.bind(this);
 
   this.element.addEventListener("copy", this._onCopy);
 
   this._handlePrefChange = this._handlePrefChange.bind(this);
@@ -1163,17 +1166,19 @@ CssRuleView.prototype = {
 
       if (target.nodeName == "input") {
         let start = Math.min(target.selectionStart, target.selectionEnd);
         let end = Math.max(target.selectionStart, target.selectionEnd);
         let count = end - start;
         text = target.value.substr(start, count);
       } else {
         let win = this.doc.defaultView;
-        text = win.getSelection().toString();
+        let selection = win.getSelection();
+        debugger;
+        text = selection.toString();
 
         // Remove any double newlines.
         text = text.replace(/(\r?\n)\r?\n/g, "$1");
 
         // Remove "inline"
         let inline = _strings.GetStringFromName("rule.sourceInline");
         let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
         text = text.replace(rx, "");
@@ -1209,16 +1214,18 @@ CssRuleView.prototype = {
   {
     this.clear();
 
     gDevTools.off("pref-changed", this._handlePrefChange);
 
     this.element.removeEventListener("copy", this._onCopy);
     delete this._onCopy;
 
+    delete this._outputParser;
+
     // Remove context menu
     if (this._contextmenu) {
       // Destroy the Select All menuitem.
       this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
       this.menuitemSelectAll = null;
 
       // Destroy the Copy menuitem.
       this.menuitemCopy.removeEventListener("command", this._onCopy);
@@ -1934,16 +1941,25 @@ TextPropertyEditor.prototype = {
     this.nameSpan.textContent = name;
 
     // Combine the property's value and priority into one string for
     // the value.
     let val = this.prop.value;
     if (this.prop.priority) {
       val += " !" + this.prop.priority;
     }
+
+    let store = this.prop.rule.elementStyle.store;
+    let propDirty = store.userProperties.contains(this.prop.rule.style, name);
+    if (propDirty) {
+      this.element.setAttribute("dirty", "");
+    } else {
+      this.element.removeAttribute("dirty");
+    }
+
     // Treat URLs differently than other properties.
     // Allow the user to click a link to the resource and open it.
     let resourceURI = this.getResourceURI();
     if (resourceURI) {
       this.valueSpan.textContent = "";
 
       appendText(this.valueSpan, val.split(resourceURI)[0]);
 
@@ -1961,25 +1977,23 @@ TextPropertyEditor.prototype = {
         aEvent.preventDefault();
 
         this.browserWindow.openUILinkIn(aEvent.target.href, "tab");
 
       }, false);
 
       appendText(this.valueSpan, val.split(resourceURI)[1]);
     } else {
-      this.valueSpan.textContent = val;
-    }
-
-    let store = this.prop.rule.elementStyle.store;
-    let propDirty = store.userProperties.contains(this.prop.rule.style, name);
-    if (propDirty) {
-      this.element.setAttribute("dirty", "");
-    } else {
-      this.element.removeAttribute("dirty");
+      let outputParser = this.ruleEditor.ruleView._outputParser;
+      let frag = outputParser.parseCssProperty(name, val, {
+        colorSwatchClass: "ruleview-colorswatch",
+        defaultColorType: !propDirty
+      });
+      this.valueSpan.innerHTML = "";
+      this.valueSpan.appendChild(frag);
     }
 
     // Populate the computed styles.
     this._updateComputed();
   },
 
   _onStartEditing: function TextPropertyEditor_onStartEditing()
   {
@@ -2016,20 +2030,28 @@ TextPropertyEditor.prototype = {
       }
 
       createChild(li, "span", {
         class: "ruleview-propertyname theme-fg-color5",
         textContent: computed.name
       });
       appendText(li, ": ");
 
+      let outputParser = this.ruleEditor.ruleView._outputParser;
+      let frag = outputParser.parseCssProperty(
+        computed.name, computed.value, {
+          colorSwatchClass: "ruleview-colorswatch"
+        }
+      );
+
       createChild(li, "span", {
         class: "ruleview-propertyvalue theme-fg-color1",
-        textContent: computed.value
+        child: frag
       });
+
       appendText(li, ";");
     }
 
     // Show or hide the expander as needed.
     if (showExpander) {
       this.expander.style.visibility = "visible";
     } else {
       this.expander.style.visibility = "hidden";
@@ -2296,16 +2318,18 @@ UserProperties.prototype = {
  */
 function createChild(aParent, aTag, aAttributes)
 {
   let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
   for (let attr in aAttributes) {
     if (aAttributes.hasOwnProperty(attr)) {
       if (attr === "textContent") {
         elt.textContent = aAttributes[attr];
+      } else if(attr === "child") {
+        elt.appendChild(aAttributes[attr]);
       } else {
         elt.setAttribute(attr, aAttributes[attr]);
       }
     }
   }
   aParent.appendChild(elt);
   return elt;
 }
--- a/browser/devtools/styleinspector/ruleview.css
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -53,8 +53,12 @@
   vertical-align: middle;
   height: 1.5em;
   line-height: 1.5em;
 }
 
 .ruleview-header.ruleview-expandable-header {
   cursor: pointer;
 }
+
+.ruleview-colorswatch {
+  display: inline-block;
+}
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_902966_revert_value_on_ESC.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_902966_revert_value_on_ESC.js
@@ -15,18 +15,18 @@ let originalValue = "#00F";
 // {
 //  value: what char sequence to type,
 //  commitKey: what key to type to "commit" the change,
 //  modifiers: commitKey modifiers,
 //  expected: what value is expected as a result
 // }
 let testData = [
   {value: "red", commitKey: "VK_ESCAPE", modifiers: {}, expected: originalValue},
-  {value: "red", commitKey: "VK_RETURN", modifiers: {}, expected: "red"},
-  {value: "blue", commitKey: "VK_TAB", modifiers: {shiftKey: true}, expected: "blue"}
+  {value: "red", commitKey: "VK_RETURN", modifiers: {}, expected: "#F00"},
+  {value: "blue", commitKey: "VK_TAB", modifiers: {shiftKey: true}, expected: "#00F"}
 ];
 
 function startTests()
 {
   let style = '' +
     '#testid {' +
     '  color: ' + originalValue + ';' +
     '}';
@@ -56,17 +56,17 @@ function runTestData(index)
   waitForEditorFocus(propEditor.element, function(aEditor) {
     is(inplaceEditor(propEditor.valueSpan), aEditor, "Focused editor should be the value span.");
 
     for (let ch of testData[index].value) {
       EventUtils.sendChar(ch, ruleWindow);
     }
     EventUtils.synthesizeKey(testData[index].commitKey, testData[index].modifiers);
 
-    is(propEditor.valueSpan.innerHTML, testData[index].expected);
+    is(propEditor.valueSpan.textContent, testData[index].expected);
 
     runTestData(index + 1);
   });
 
   EventUtils.synthesizeMouse(propEditor.valueSpan, 1, 1, {}, ruleWindow);
 }
 
 function finishTest()
--- a/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
@@ -15,57 +15,57 @@ function simpleOverride(aInspector, aRul
 
     let elementRule = elementStyle.rules[0];
     let firstProp = elementRule.createProperty("background-color", "green", "");
     let secondProp = elementRule.createProperty("background-color", "blue", "");
     is(elementRule.textProps[0], firstProp, "Rules should be in addition order.");
     is(elementRule.textProps[1], secondProp, "Rules should be in addition order.");
 
     promiseDone(elementRule._applyingModifications.then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Second property should have been used.");
+      is(element.style.getPropertyValue("background-color"), "blue", "Second property should have been used.");
 
       secondProp.remove();
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 128, 0)", "After deleting second property, first should be used.");
+      is(element.style.getPropertyValue("background-color"), "green", "After deleting second property, first should be used.");
 
       secondProp = elementRule.createProperty("background-color", "blue", "");
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "New property should be used.");
+      is(element.style.getPropertyValue("background-color"), "blue", "New property should be used.");
 
       is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
       is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
 
       secondProp.setEnabled(false);
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 128, 0)", "After disabling second property, first value should be used");
+      is(element.style.getPropertyValue("background-color"), "green", "After disabling second property, first value should be used");
 
       firstProp.setEnabled(false);
       return elementRule._applyingModifications;
     }).then(() => {
       is(element.style.getPropertyValue("background-color"), "", "After disabling both properties, value should be empty.");
 
       secondProp.setEnabled(true);
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Value should be set correctly after re-enabling");
+      is(element.style.getPropertyValue("background-color"), "blue", "Value should be set correctly after re-enabling");
 
       firstProp.setEnabled(true);
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Re-enabling an earlier property shouldn't make it override a later property.");
+      is(element.style.getPropertyValue("background-color"), "blue", "Re-enabling an earlier property shouldn't make it override a later property.");
       is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
       is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
 
       firstProp.setValue("purple", "");
       return elementRule._applyingModifications;
     }).then(() => {
-      is(element.style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Modifying an earlier property shouldn't override a later property.");
+      is(element.style.getPropertyValue("background-color"), "blue", "Modifying an earlier property shouldn't override a later property.");
       finishTest();
     }));
   });
 }
 
 function finishTest()
 {
   doc = null;
--- a/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
@@ -62,17 +62,17 @@ function testTopLeft()
     let elementAfterRule = afterRules[0];
     let elementAfterRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementAfterRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementAfterRule.textProps),
-      "background: none repeat scroll 0% 0% #F00; content: \" \"; position: absolute; " +
+      "background: none repeat scroll 0% 0% red; content: \" \"; position: absolute; " +
       "border-radius: 50%; height: 32px; width: 32px; top: 50%; left: 50%; margin-top: -16px; margin-left: -16px",
       "TopLeft after properties are correct"
     );
 
     let elementBeforeRule = beforeRules[0];
     let elementBeforeRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementBeforeRule;
     })[0]._ruleEditor;
@@ -237,41 +237,41 @@ function testParagraph()
     let elementFirstLineRule = firstLineRules[0];
     let elementFirstLineRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementFirstLineRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementFirstLineRule.textProps),
-      "background: none repeat scroll 0% 0% #00F",
+      "background: none repeat scroll 0% 0% blue",
       "Paragraph first-line properties are correct"
     );
 
     let elementFirstLetterRule = firstLetterRules[0];
     let elementFirstLetterRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementFirstLetterRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementFirstLetterRule.textProps),
-      "color: #F00; font-size: 130%",
+      "color: red; font-size: 130%",
       "Paragraph first-letter properties are correct"
     );
 
     let elementSelectionRule = selectionRules[0];
     let elementSelectionRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementSelectionRule;
     })[0]._ruleEditor;
 
     is
     (
       convertTextPropsToString(elementSelectionRule.textProps),
-      "color: #FFF; background: none repeat scroll 0% 0% #000",
+      "color: white; background: none repeat scroll 0% 0% black",
       "Paragraph first-letter properties are correct"
     );
 
     testBody();
   });
 }
 
 function testBody() {
--- a/browser/themes/linux/devtools/computedview.css
+++ b/browser/themes/linux/devtools/computedview.css
@@ -142,8 +142,17 @@ body {
   width: 100%;
 }
 
 .link {
   padding: 0 3px;
   cursor: pointer;
   float: right;
 }
+
+.computedview-colorswatch {
+  display: inline-block;
+  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  vertical-align: text-top;
+  -moz-margin-end: 5px;
+}
--- a/browser/themes/linux/devtools/ruleview.css
+++ b/browser/themes/linux/devtools/ruleview.css
@@ -91,16 +91,24 @@
   list-style: none;
   padding: 0;
 }
 
 .ruleview-computed {
   -moz-margin-start: 35px;
 }
 
+.ruleview-colorswatch {
+  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  vertical-align: text-top;
+  -moz-margin-end: 5px;
+}
+
 .ruleview-overridden {
   text-decoration: line-through;
 }
 
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
   padding: 0;
 }
--- a/browser/themes/osx/devtools/computedview.css
+++ b/browser/themes/osx/devtools/computedview.css
@@ -160,8 +160,17 @@ body {
   width: 100%;
 }
 
 .link {
   padding: 0 3px;
   cursor: pointer;
   float: right;
 }
+
+.computedview-colorswatch {
+  display: inline-block;
+  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  vertical-align: text-top;
+  -moz-margin-end: 5px;
+}
--- a/browser/themes/osx/devtools/ruleview.css
+++ b/browser/themes/osx/devtools/ruleview.css
@@ -95,16 +95,24 @@
   list-style: none;
   padding: 0;
 }
 
 .ruleview-computed {
   -moz-margin-start: 35px;
 }
 
+.ruleview-colorswatch {
+  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  vertical-align: text-top;
+  -moz-margin-end: 5px;
+}
+
 .ruleview-overridden {
   text-decoration: line-through;
 }
 
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
   padding: 0;
 }
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -99,8 +99,14 @@
 
 .theme-fg-color7 { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
+
+.ruleview-colorswatch,
+.computedview-colorswatch,
+.markupview-colorswatch {
+  box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
+}
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -99,8 +99,14 @@
 
 .theme-fg-color7 { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
+
+.ruleview-colorswatch,
+.computedview-colorswatch,
+.markupview-colorswatch {
+  box-shadow: 0 0 0 1px #EFEFEF;
+}
--- a/browser/themes/windows/devtools/computedview.css
+++ b/browser/themes/windows/devtools/computedview.css
@@ -160,8 +160,17 @@ body {
   width: 100%;
 }
 
 .link {
   padding: 0 3px;
   cursor: pointer;
   float: right;
 }
+
+.computedview-colorswatch {
+  display: inline-block;
+  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  vertical-align: text-top;
+  -moz-margin-end: 5px;
+}
--- a/browser/themes/windows/devtools/ruleview.css
+++ b/browser/themes/windows/devtools/ruleview.css
@@ -91,16 +91,24 @@
   list-style: none;
   padding: 0;
 }
 
 .ruleview-computed {
   -moz-margin-start: 35px;
 }
 
+.ruleview-colorswatch {
+  border-radius: 50%;
+  width: 1em;
+  height: 1em;
+  vertical-align: text-top;
+  -moz-margin-end: 5px;
+}
+
 .ruleview-overridden {
   text-decoration: line-through;
 }
 
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
   padding: 0;
 }
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -55,16 +55,17 @@ var BuiltinProvider = {
         "": "resource://gre/modules/commonjs/",
         "main": "resource:///modules/devtools/main.js",
         "devtools": "resource:///modules/devtools",
         "devtools/server": "resource://gre/modules/devtools/server",
         "devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
         "devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js",
         "devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
         "devtools/css-color": "resource://gre/modules/devtools/css-color",
+        "devtools/output-parser": "resource://gre/modules/devtools/output-parser",
         "devtools/touch-events": "resource://gre/modules/devtools/touch-events",
         "devtools/client": "resource://gre/modules/devtools/client",
 
         "escodegen": "resource://gre/modules/devtools/escodegen",
         "estraverse": "resource://gre/modules/devtools/escodegen/estraverse",
 
         // Allow access to xpcshell test items from the loader.
         "xpcshell-test": "resource://test"
--- a/toolkit/devtools/css-color.js
+++ b/toolkit/devtools/css-color.js
@@ -1,16 +1,18 @@
 /* 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";
 
+const {Cc, Ci, Cu} = require("chrome");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
-const {Cc, Ci, Cu} = require("chrome");
 
 const REGEX_JUST_QUOTES  = /^""$/;
 const REGEX_RGB_3_TUPLE  = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i;
 const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i;
 const REGEX_HSL_3_TUPLE  = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i;
 
 /**
  * This regex matches:
@@ -31,24 +33,23 @@ const REGEX_ALL_COLORS = /#[0-9a-fA-F]{3
 const SPECIALVALUES = new Set([
   "currentcolor",
   "initial",
   "inherit",
   "transparent",
   "unset"
 ]);
 
-let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
-
 /**
  * This module is used to convert between various color types.
  *
  * Usage:
  *   let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
  *   let {colorUtils} = devtools.require("devtools/css-color");
+ *   let color = new colorUtils.CssColor("red");
  *
  *   color.authored === "red"
  *   color.hasAlpha === false
  *   color.valid === true
  *   color.transparent === false // transparent has a special status.
  *   color.name === "red"        // returns hex or rgba when no name available.
  *   color.hex === "#F00"        // returns shortHex when available else returns
  *                                  longHex. If alpha channel is present then we
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/output-parser.js
@@ -0,0 +1,385 @@
+/* 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";
+
+const {Cc, Ci, Cu} = require("chrome");
+const {colorUtils} = require("devtools/css-color");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+const REGEX_QUOTES = /^".*?"|^".*/;
+const REGEX_URL = /^url\(["']?(.+?)(?::(\d+))?["']?\)/;
+const REGEX_WHITESPACE = /^\s+/;
+const REGEX_FIRST_WORD_OR_CHAR = /^\w+|^./;
+const REGEX_CSS_PROPERTY_VALUE = /(^[^;]+)/;
+
+/**
+ * This regex matches:
+ *  - #F00
+ *  - #FF0000
+ *  - hsl()
+ *  - hsla()
+ *  - rgb()
+ *  - rgba()
+ *  - color names
+ */
+const REGEX_ALL_COLORS = /^#[0-9a-fA-F]{3}\b|^#[0-9a-fA-F]{6}\b|^hsl\(.*?\)|^hsla\(.*?\)|^rgba?\(.*?\)|^[a-zA-Z-]+/;
+
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+/**
+ * This regular expression catches all css property names with their trailing
+ * spaces and semicolon. This is used to ensure a value is valid for a property
+ * name within style="" attributes.
+ */
+loader.lazyGetter(this, "REGEX_ALL_CSS_PROPERTIES", function () {
+  let names = DOMUtils.getCSSPropertyNames();
+    let pattern = "^(";
+
+    for (let i = 0; i < names.length; i++) {
+      if (i > 0) {
+        pattern += "|";
+      }
+      pattern += names[i];
+    }
+    pattern += ")\\s*:\\s*";
+
+    return new RegExp(pattern);
+});
+
+/**
+ * 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
+ * functions with the debugger, placing color swatches next to colors and
+ * adding doorhanger previews where possible (images, angles, lengths,
+ * border radius, cubic-bezier etc.).
+ *
+ * Usage:
+ *   const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+ *   const {OutputParser} = devtools.require("devtools/output-parser");
+ *
+ *   let parser = new OutputParser();
+ *
+ *   parser.parseCssProperty("color", "red"); // Returns document fragment.
+ *   parser.parseHTMLAttribute("color:red; font-size: 12px;"); // Returns document
+ *                                                             // fragment.
+ */
+function OutputParser() {
+  this.parsed = [];
+}
+
+exports.OutputParser = OutputParser;
+
+OutputParser.prototype = {
+  /**
+   * Parse a CSS property value given a property name.
+   *
+   * @param  {String} name
+   *         CSS Property Name
+   * @param  {String} value
+   *         CSS Property value
+   * @param  {Object} [options]
+   *         Options object. For valid options and default values see
+   *         _mergeOptions().
+   * @return {DocumentFragment}
+   *         A document fragment containing color swatches etc.
+   */
+  parseCssProperty: function(name, value, options={}) {
+    options = this._mergeOptions(options);
+    options.cssPropertyName = name;
+
+    // Detect if "name" supports colors by checking if "papayawhip" is a valid
+    // value.
+    options.colors = this._cssPropertySupportsValue(name, "papayawhip");
+
+    return this._parse(value, options);
+  },
+
+  /**
+   * Parse a string.
+   *
+   * @param  {String} value
+   *         Text to parse.
+   * @param  {Object} [options]
+   *         Options object. For valid options and default values see
+   *         _mergeOptions().
+   * @return {DocumentFragment}
+   *         A document fragment containing events etc. Colors will not be
+   *         parsed.
+   */
+  parseHTMLAttribute: function(value, options={}) {
+    options = this._mergeOptions(options);
+    options.colors = false;
+
+    return this._parse(value, options);
+  },
+
+  /**
+   * Parse a string.
+   *
+   * @param  {String} text
+   *         Text to parse.
+   * @param  {Object} [options]
+   *         Options object. For valid options and default values see
+   *         _mergeOptions().
+   * @return {DocumentFragment}
+   *         A document fragment containing events etc. Colors will not be
+   *         parsed.
+   */
+  _parse: function(text, options={}) {
+    text = text.trim();
+    this.parsed.length = 0;
+    let dirty = false;
+    let matched = null;
+    let nameValueSupported = false;
+
+    let trimMatchFromStart = function(match) {
+      text = text.substr(match.length);
+      dirty = true;
+      matched = null;
+    };
+
+    while (text.length > 0) {
+      matched = text.match(REGEX_QUOTES);
+      if (matched) {
+        let match = matched[0];
+        trimMatchFromStart(match);
+        this._appendTextNode(match);
+      }
+
+      matched = text.match(REGEX_WHITESPACE);
+      if (matched) {
+        let match = matched[0];
+        trimMatchFromStart(match);
+        this._appendTextNode(match);
+      }
+
+      matched = text.match(REGEX_URL);
+      if (matched) {
+        let [match, url, line] = matched;
+        trimMatchFromStart(match);
+        this._appendURL(match, url, line, options);
+      }
+
+      // This block checks for valid name and value combinations setting
+      // nameValueSupported to true as appropriate.
+      matched = text.match(REGEX_ALL_CSS_PROPERTIES);
+      if (matched) {
+        let [match, propertyName] = matched;
+        trimMatchFromStart(match);
+        this._appendTextNode(match);
+
+        matched = text.match(REGEX_CSS_PROPERTY_VALUE);
+        if (matched) {
+          let [, value] = matched;
+          nameValueSupported = this._cssPropertySupportsValue(propertyName, value);
+        }
+      }
+
+      // This block should only be used for CSS properties.
+      // options.cssPropertyName is only set if the parse call comes from a CSS
+      // tool containing either a name and value or a string with valid name and
+      // value combinations.
+      if (options.cssPropertyName || (!options.cssPropertyName && nameValueSupported)) {
+        matched = text.match(REGEX_ALL_COLORS);
+        if (matched) {
+          let match = matched[0];
+          trimMatchFromStart(match);
+          this._appendColor(match, options);
+        }
+
+        nameValueSupported = false;
+      }
+
+      if (!dirty) {
+        // This test must always be last as it indicates use of an unknown
+        // character that needs to be removed to prevent infinite loops.
+        matched = text.match(REGEX_FIRST_WORD_OR_CHAR);
+        if (matched) {
+          let match = matched[0];
+          trimMatchFromStart(match);
+          this._appendTextNode(match);
+          nameValueSupported = false;
+        }
+      }
+
+      dirty = false;
+    }
+
+    return this._toDOM();
+  },
+
+  /**
+   * Check if a CSS property supports a specific value.
+   *
+   * @param  {String} propertyName
+   *         CSS Property name to check
+   * @param  {String} propertyValue
+   *         CSS Property value to check
+   */
+  _cssPropertySupportsValue: function(propertyName, propertyValue) {
+    let autoCompleteValues = DOMUtils.getCSSValuesForProperty(propertyName);
+
+    // Detect if propertyName supports colors by checking if papayawhip is a
+    // valid value.
+    if (autoCompleteValues.indexOf("papayawhip") !== -1) {
+      return this._isColorValid(propertyValue);
+    }
+
+    // For the rest we can trust autocomplete value matches.
+    return autoCompleteValues.indexOf(propertyValue) !== -1;
+  },
+
+  /**
+   * Append a color to the output.
+   *
+   * @param  {String} color
+   *         Color to append
+   * @param  {Object} [options]
+   *         Options object. For valid options and default values see
+   *         _mergeOptions().
+   */
+  _appendColor: function(color, options={}) {
+    if (options.colors && this._isColorValid(color)) {
+      if (options.colorSwatchClass) {
+        this._appendNode("span", {
+          class: options.colorSwatchClass,
+          style: "background-color:" + color
+        });
+      }
+      if (options.defaultColorType) {
+        color = new colorUtils.CssColor(color).toString();
+      }
+    }
+    this._appendTextNode(color);
+  },
+
+  /**
+   * Append a URL to the output.
+   *
+   * @param  {String} match
+   *         Complete match that may include "url(xxx)""
+   * @param  {String} url
+   *         Actual URL
+   * @param  {Number} line
+   *         Line number from URL e.g. http://blah:42
+   * @param  {Object} [options]
+   *         Options object. For valid options and default values see
+   *         _mergeOptions().
+   */
+  _appendURL: function(match, url, line, options={}) {
+    this._appendTextNode(match);
+  },
+
+  /**
+   * Append a node to the output.
+   *
+   * @param  {String} tagName
+   *         Tag type e.g. "div"
+   * @param  {Object} attributes
+   *         e.g. {class: "someClass", style: "cursor:pointer"};
+   * @param  {String} [value]
+   *         If a value is included it will be appended as a text node inside
+   *         the tag. This is useful e.g. for span tags.
+   */
+  _appendNode: function(tagName, attributes, value="") {
+    let win = Services.appShell.hiddenDOMWindow;
+    let doc = win.document;
+    let node = doc.createElement(tagName);
+    let attrs = Object.getOwnPropertyNames(attributes);
+
+    for (let attr of attrs) {
+      node.setAttribute(attr, attributes[attr]);
+    }
+
+    if (value) {
+      let textNode = content.document.createTextNode(value);
+      node.appendChild(textNode);
+    }
+
+    this.parsed.push(node);
+  },
+
+  /**
+   * Append a text node to the output. If the previously output item was a text
+   * node then we append the text to that node.
+   *
+   * @param  {String} text
+   *         Text to append
+   */
+  _appendTextNode: function(text) {
+    let lastItem = this.parsed[this.parsed.length - 1];
+
+    if (typeof lastItem !== "undefined" && lastItem.nodeName === "#text") {
+      lastItem.nodeValue += text;
+    } else {
+      let win = Services.appShell.hiddenDOMWindow;
+      let doc = win.document;
+      let textNode = doc.createTextNode(text);
+      this.parsed.push(textNode);
+    }
+  },
+
+  /**
+   * Take all output and append it into a single DocumentFragment.
+   *
+   * @return {DocumentFragment}
+   *         Document Fragment
+   */
+  _toDOM: function() {
+    let win = Services.appShell.hiddenDOMWindow;
+    let doc = win.document;
+    let frag = doc.createDocumentFragment();
+
+    for (let item of this.parsed) {
+      frag.appendChild(item);
+    }
+
+    this.parsed.length = 0;
+    return frag;
+  },
+
+  /**
+   * Check that a string represents a valid volor.
+   *
+   * @param  {String} color
+   *         Color to check
+   */
+  _isColorValid: function(color) {
+    return new colorUtils.CssColor(color).valid;
+  },
+
+  /**
+   * Merges options objects. Default values are set here.
+   *
+   * @param  {Object} overrides
+   *         The option values to override e.g. _mergeOptions({colors: false})
+   *
+   *         Valid options are:
+   *           - colors: true           // Allow processing of colors
+   *           - defaultColorType: true // Convert colors to the default type
+   *                                    // selected in the options panel.
+   *           - colorSwatchClass: ""   // The class to use for color swatches.
+   *           - cssPropertyName: ""    // Used by CSS tools. Passing in the
+   *                                    // property name allows appropriate
+   *                                    // processing of the property value.
+   * @return {Object}
+   *         Overridden options object
+   */
+  _mergeOptions: function(overrides) {
+    let defaults = {
+      colors: true,
+      defaultColorType: true,
+      colorSwatchClass: "",
+      cssPropertyName: ""
+    };
+
+    for (let item in overrides) {
+      defaults[item] = overrides[item];
+    }
+    return defaults;
+  },
+};
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -227,28 +227,29 @@ var PageStyleActor = protocol.ActorClass
 
       let rule = this._styleRef(domRule);
       rules.add(rule);
 
       matched.push({
         rule: rule,
         sourceText: this.getSelectorSource(selectorInfo, node.rawNode),
         selector: selectorInfo.selector.text,
+        name: selectorInfo.property,
         value: selectorInfo.value,
         status: selectorInfo.status
       });
     }
 
     this.expandSets(rules, sheets);
 
     return {
       matched: matched,
       rules: [...rules],
       sheets: [...sheets],
-    }
+    };
   }, {
     request: {
       node: Arg(0, "domnode"),
       property: Arg(1, "string"),
       filter: Option(2, "string")
     },
     response: RetVal(types.addDictType("matchedselectorresponse", {
       rules: "array:domstylerule",
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -46,18 +46,16 @@ const RX_PSEUDO_CLASS_OR_ELT = /(:[\w-]+
 const RX_CONNECTORS = /\s*[\s>+~]\s*/g;
 const RX_ID = /\s*#\w+\s*/g;
 const RX_CLASS_OR_ATTRIBUTE = /\s*(?:\.\w+|\[.+?\])\s*/g;
 const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-let {colorUtils} = require("devtools/css-color");
-
 function CssLogic()
 {
   // The cache of examined CSS properties.
   _propertyInfos: {};
 }
 
 exports.CssLogic = CssLogic;
 
@@ -1609,17 +1607,17 @@ CssPropertyInfo.prototype = {
  * @param {CssLogic.STATUS} aStatus The selector match status.
  * @constructor
  */
 function CssSelectorInfo(aSelector, aProperty, aValue, aStatus)
 {
   this.selector = aSelector;
   this.property = aProperty;
   this.status = aStatus;
-  this.value = colorUtils.processCSSString(aValue);
+  this.value = aValue;
   let priority = this.selector.cssRule.getPropertyPriority(this.property);
   this.important = (priority === "important");
 }
 
 CssSelectorInfo.prototype = {
   /**
    * Retrieve the CssSelector source, which is the source of the CssSheet owning
    * the selector.