Bug 918716 - Add color swatches to devtools output parser. r=jwalker
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Fri, 18 Oct 2013 15:01:20 +0100
changeset 166171 54355463e47d21ccc2e72c81a9b1a8fe40d87640
parent 166170 f1b97193d16237026d14ebabc35d362bede561ce
child 166172 93e324bc5330445d63fc276e1d9f009c16351743
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs918716
milestone27.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 918716 - Add color swatches to devtools output parser. r=jwalker
browser/devtools/inspector/test/browser_inspector_changes.js
browser/devtools/inspector/test/head.js
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_bug589375_keybindings.js
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/devtools/styleinspector/test/head.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/inspector/test/browser_inspector_changes.js
+++ b/browser/devtools/inspector/test/browser_inspector_changes.js
@@ -11,28 +11,16 @@ function test() {
 
   function createDocument()
   {
     doc.body.innerHTML = '<div id="testdiv">Test div!</div>';
     doc.title = "Inspector Change Test";
     openInspector(runInspectorTests);
   }
 
-
-  function getInspectorComputedProp(aName)
-  {
-    let computedview = inspector.sidebar.getWindowForTab("computedview").computedview.view;
-    for each (let view in computedview.propertyViews) {
-      if (view.name == aName) {
-        return view;
-      }
-    }
-    return null;
-  }
-
   function getInspectorRuleProp(aName)
   {
     let ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
     let inlineStyles = ruleview._elementStyle.rules[0];
 
     for each (let prop in inlineStyles.textProps) {
       if (prop.name == aName) {
         return prop;
@@ -59,34 +47,34 @@ function test() {
     });
   }
 
   function computedStylePanelTests()
   {
     let computedview = inspector.sidebar.getWindowForTab("computedview").computedview;
     ok(computedview, "Style Panel has a cssHtmlTree");
 
-    let propView = getInspectorComputedProp("font-size");
-    is(propView.value, "10px", "Style inspector should be showing the correct font size.");
+    let fontSize = getComputedPropertyValue("font-size");
+    is(fontSize, "10px", "Style inspector should be showing the correct font size.");
 
     testDiv.style.cssText = "font-size: 15px; color: red;";
 
     // Wait until layout-change fires from mutation to skip earlier refresh event
     inspector.once("layout-change", () => {
       inspector.once("computed-view-refreshed", computedStylePanelAfterChange);
     });
   }
 
   function computedStylePanelAfterChange()
   {
-    let propView = getInspectorComputedProp("font-size");
-    is(propView.value, "15px", "Style inspector should be showing the new font size.");
+    let fontSize = getComputedPropertyValue("font-size");
+    is(fontSize, "15px", "Style inspector should be showing the new font size.");
 
-    let propView = getInspectorComputedProp("color");
-    is(propView.value, "#F00", "Style inspector should be showing the new color.");
+    let color = getComputedPropertyValue("color");
+    is(color, "#F00", "Style inspector should be showing the new color.");
 
     computedStylePanelNotActive();
   }
 
   function computedStylePanelNotActive()
   {
     // Tests changes made while the style panel is not active.
     inspector.sidebar.select("ruleview");
@@ -96,24 +84,24 @@ function test() {
     testDiv.style.textAlign = "center";
 
     inspector.once("computed-view-refreshed", computedStylePanelAfterSwitch);
     inspector.sidebar.select("computedview");
   }
 
   function computedStylePanelAfterSwitch()
   {
-    let propView = getInspectorComputedProp("font-size");
-    is(propView.value, "20px", "Style inspector should be showing the new font size.");
+    let fontSize = getComputedPropertyValue("font-size");
+    is(fontSize, "20px", "Style inspector should be showing the new font size.");
 
-    let propView = getInspectorComputedProp("color");
-    is(propView.value, "#00F", "Style inspector should be showing the new color.");
+    let color = getComputedPropertyValue("color");
+    is(color, "#00F", "Style inspector should be showing the new color.");
 
-    let propView = getInspectorComputedProp("text-align");
-    is(propView.value, "center", "Style inspector should be showing the new text align.");
+    let textAlign = getComputedPropertyValue("text-align");
+    is(textAlign, "center", "Style inspector should be showing the new text align.");
 
     rulePanelTests();
   }
 
   function rulePanelTests()
   {
     inspector.sidebar.select("ruleview");
     let ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview;
@@ -132,17 +120,17 @@ function test() {
   }
 
   function rulePanelAfterChange()
   {
     let propView = getInspectorRuleProp("text-align");
     is(propView.value, "right", "Style inspector should be showing the new text align.");
 
     let propView = getInspectorRuleProp("color");
-    is(propView.value, "#FAFAD2", "Style inspector should be showing the new color.")
+    is(propView.value, "lightgoldenrodyellow", "Style inspector should be showing the new color.")
 
     let propView = getInspectorRuleProp("font-size");
     is(propView.value, "3em", "Style inspector should be showing the new font size.");
 
     let propView = getInspectorRuleProp("text-transform");
     is(propView.value, "uppercase", "Style inspector should be showing the new text transform.");
 
     finishTest();
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -105,16 +105,21 @@ function computedViewTree()
 
 function ruleView()
 {
   let sidebar = getActiveInspector().sidebar;
   let iframe = sidebar.tabbox.querySelector(".iframe-ruleview");
   return iframe.contentWindow.ruleView;
 }
 
+function getComputedView() {
+  let inspector = getActiveInspector();
+  return inspector.sidebar.getWindowForTab("computedview").computedview.view;
+}
+
 function synthesizeKeyFromKeyTag(aKeyId) {
   let key = document.getElementById(aKeyId);
   isnot(key, null, "Successfully retrieved the <key> node");
 
   let modifiersAttr = key.getAttribute("modifiers");
 
   let name = null;
 
@@ -164,8 +169,22 @@ function focusSearchBoxUsingShortcut(pan
   let searchBox = panelWin.document.getElementById("inspector-searchbox");
   searchBox.addEventListener("focus", function onFocus() {
     searchBox.removeEventListener("focus", onFocus, false);
     callback && callback();
   }, false);
   EventUtils.synthesizeKey(name, modifiers);
 }
 
+function getComputedPropertyValue(aName)
+{
+  let computedview = getComputedView();
+  let props = computedview.styleDocument.querySelectorAll(".property-view");
+
+  for (let prop of props) {
+    let name = prop.querySelector(".property-name");
+
+    if (name.textContent === aName) {
+      let value = prop.querySelector(".property-value");
+      return value.textContent;
+    }
+  }
+}
--- 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_bug589375_keybindings.js
+++ b/browser/devtools/styleinspector/test/browser_bug589375_keybindings.js
@@ -28,17 +28,17 @@ function openComputedView(aInspector)
   Services.obs.addObserver(runTests, "StyleInspector-populated", false);
 
   inspector.sidebar.select("computedview");
 }
 
 function runTests()
 {
   Services.obs.removeObserver(runTests, "StyleInspector-populated");
-  computedView = getComputedView(inspector);
+  computedView = getComputedView();
 
   var span = doc.querySelector(".matches");
   ok(span, "captain, we have the matches span");
 
   inspector.selection.setNode(span);
 
   is(span, computedView.viewedElement,
     "style inspector node matches the selected node");
--- 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/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -43,16 +43,22 @@ function addTab(aURL)
 function openInspector(callback)
 {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
     callback(toolbox.getCurrentPanel());
   });
 }
 
+function getActiveInspector()
+{
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  return gDevTools.getToolbox(target).getPanel("inspector");
+}
+
 function openRuleView(callback)
 {
   openInspector(inspector => {
     inspector.sidebar.once("ruleview-ready", () => {
       inspector.sidebar.select("ruleview");
       let ruleView = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
       callback(inspector, ruleView);
     })
@@ -94,22 +100,24 @@ function tearDown()
     dump(ex);
   }
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
   browser = hudId = hud = filterBox = outputNode = cs = null;
 }
 
-function getComputedView(inspector) {
+function getComputedView() {
+  let inspector = getActiveInspector();
   return inspector.sidebar.getWindowForTab("computedview").computedview.view;
 }
 
 function ruleView()
 {
+  let inspector = getActiveInspector();
   return inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
 }
 
 function waitForEditorFocus(aParent, aCallback)
 {
   aParent.addEventListener("focus", function onFocus(evt) {
     if (inplaceEditor(evt.target) && evt.target.tagName == "input") {
       aParent.removeEventListener("focus", onFocus, true);
@@ -159,13 +167,27 @@ function promiseDone(promise) {
     ok(false, "Promise failed: " + err);
     if (err.stack) {
       dump(err.stack);
     }
     SimpleTest.finish();
   });
 }
 
+function getComputedPropertyValue(aName)
+{
+  let computedview = getComputedView();
+  let props = computedview.styleDocument.querySelectorAll(".property-view");
+
+  for (let prop of props) {
+    let name = prop.querySelector(".property-name");
+
+    if (name.textContent === aName) {
+      let value = prop.querySelector(".property-value");
+      return value.textContent;
+    }
+  }
+}
 
 registerCleanupFunction(tearDown);
 
 waitForExplicitFinish();
 
--- 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.