Bug 1266842 - replace rgbToColorName, colorToRGBA, isValidCSSColor in devtools; r=pbro
authorTom Tromey <ttromey@mozilla.com>
Thu, 28 Apr 2016 08:46:19 -0600
changeset 334887 f7d385fac19af534b7c1e8b199e1c5eb4f3034c4
parent 334886 dcd6211724fd3e604238727236b97e9a91f49332
child 334888 f930f51a9b0aaf4aeb2fbf042ad040774b60d07c
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspbro
bugs1266842
milestone49.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 1266842 - replace rgbToColorName, colorToRGBA, isValidCSSColor in devtools; r=pbro MozReview-Commit-ID: G5Zly0HPJuv
.eslintignore
devtools/client/eyedropper/eyedropper.js
devtools/client/shared/css-color-db.js
devtools/client/shared/css-color.js
devtools/client/shared/moz.build
devtools/client/shared/output-parser.js
devtools/client/shared/test/unit/test_cssColor.js
devtools/client/shared/test/unit/test_cssColorDatabase.js
devtools/client/shared/test/unit/xpcshell.ini
--- a/.eslintignore
+++ b/.eslintignore
@@ -97,16 +97,17 @@ devtools/client/netmonitor/har/test/**
 devtools/client/performance/**
 devtools/client/projecteditor/**
 devtools/client/promisedebugger/**
 devtools/client/responsivedesign/**
 devtools/client/scratchpad/**
 devtools/client/shadereditor/**
 devtools/client/shared/**
 !devtools/client/shared/css-color.js
+!devtools/client/shared/css-color-db.js
 devtools/client/sourceeditor/**
 devtools/client/webaudioeditor/**
 devtools/client/webconsole/**
 !devtools/client/webconsole/panel.js
 !devtools/client/webconsole/jsterm.js
 devtools/client/webide/**
 devtools/server/**
 !devtools/server/actors/webbrowser.js
--- a/devtools/client/eyedropper/eyedropper.js
+++ b/devtools/client/eyedropper/eyedropper.js
@@ -1,14 +1,15 @@
 /* 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/. */
 
 const {Cc, Ci, Cu} = require("chrome");
-const {rgbToHsl} = require("devtools/client/shared/css-color").colorUtils;
+const {rgbToHsl, rgbToColorName} =
+      require("devtools/client/shared/css-color").colorUtils;
 const Telemetry = require("devtools/client/shared/telemetry");
 const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js");
 const promise = require("promise");
 const Services = require("Services");
 const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
 
 loader.lazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"]
@@ -801,17 +802,17 @@ function toColorString(rgb, format) {
     case "rgb":
       return "rgb(" + r + ", " + g + ", " + b + ")";
     case "hsl":
       let [h,s,l] = rgbToHsl(rgb);
       return "hsl(" + h + ", " + s + "%, " + l + "%)";
     case "name":
       let str;
       try {
-        str = DOMUtils.rgbToColorName(r, g, b);
+        str = rgbToColorName(r, g, b);
       } catch(e) {
         str = hexString(rgb);
       }
       return str;
     default:
       return hexString(rgb);
   }
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/css-color-db.js
@@ -0,0 +1,162 @@
+/* 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";
+
+// /!\  Auto-generated from nsColorNameList.h.
+// This should be kept in sync with that list.
+// test_cssColorDatabase.js tries to enforce this.
+
+const cssColors = {
+  aliceblue: [240, 248, 255, 1],
+  antiquewhite: [250, 235, 215, 1],
+  aqua: [0, 255, 255, 1],
+  aquamarine: [127, 255, 212, 1],
+  azure: [240, 255, 255, 1],
+  beige: [245, 245, 220, 1],
+  bisque: [255, 228, 196, 1],
+  black: [0, 0, 0, 1],
+  blanchedalmond: [255, 235, 205, 1],
+  blue: [0, 0, 255, 1],
+  blueviolet: [138, 43, 226, 1],
+  brown: [165, 42, 42, 1],
+  burlywood: [222, 184, 135, 1],
+  cadetblue: [95, 158, 160, 1],
+  chartreuse: [127, 255, 0, 1],
+  chocolate: [210, 105, 30, 1],
+  coral: [255, 127, 80, 1],
+  cornflowerblue: [100, 149, 237, 1],
+  cornsilk: [255, 248, 220, 1],
+  crimson: [220, 20, 60, 1],
+  cyan: [0, 255, 255, 1],
+  darkblue: [0, 0, 139, 1],
+  darkcyan: [0, 139, 139, 1],
+  darkgoldenrod: [184, 134, 11, 1],
+  darkgray: [169, 169, 169, 1],
+  darkgreen: [0, 100, 0, 1],
+  darkgrey: [169, 169, 169, 1],
+  darkkhaki: [189, 183, 107, 1],
+  darkmagenta: [139, 0, 139, 1],
+  darkolivegreen: [85, 107, 47, 1],
+  darkorange: [255, 140, 0, 1],
+  darkorchid: [153, 50, 204, 1],
+  darkred: [139, 0, 0, 1],
+  darksalmon: [233, 150, 122, 1],
+  darkseagreen: [143, 188, 143, 1],
+  darkslateblue: [72, 61, 139, 1],
+  darkslategray: [47, 79, 79, 1],
+  darkslategrey: [47, 79, 79, 1],
+  darkturquoise: [0, 206, 209, 1],
+  darkviolet: [148, 0, 211, 1],
+  deeppink: [255, 20, 147, 1],
+  deepskyblue: [0, 191, 255, 1],
+  dimgray: [105, 105, 105, 1],
+  dimgrey: [105, 105, 105, 1],
+  dodgerblue: [30, 144, 255, 1],
+  firebrick: [178, 34, 34, 1],
+  floralwhite: [255, 250, 240, 1],
+  forestgreen: [34, 139, 34, 1],
+  fuchsia: [255, 0, 255, 1],
+  gainsboro: [220, 220, 220, 1],
+  ghostwhite: [248, 248, 255, 1],
+  gold: [255, 215, 0, 1],
+  goldenrod: [218, 165, 32, 1],
+  gray: [128, 128, 128, 1],
+  grey: [128, 128, 128, 1],
+  green: [0, 128, 0, 1],
+  greenyellow: [173, 255, 47, 1],
+  honeydew: [240, 255, 240, 1],
+  hotpink: [255, 105, 180, 1],
+  indianred: [205, 92, 92, 1],
+  indigo: [75, 0, 130, 1],
+  ivory: [255, 255, 240, 1],
+  khaki: [240, 230, 140, 1],
+  lavender: [230, 230, 250, 1],
+  lavenderblush: [255, 240, 245, 1],
+  lawngreen: [124, 252, 0, 1],
+  lemonchiffon: [255, 250, 205, 1],
+  lightblue: [173, 216, 230, 1],
+  lightcoral: [240, 128, 128, 1],
+  lightcyan: [224, 255, 255, 1],
+  lightgoldenrodyellow: [250, 250, 210, 1],
+  lightgray: [211, 211, 211, 1],
+  lightgreen: [144, 238, 144, 1],
+  lightgrey: [211, 211, 211, 1],
+  lightpink: [255, 182, 193, 1],
+  lightsalmon: [255, 160, 122, 1],
+  lightseagreen: [32, 178, 170, 1],
+  lightskyblue: [135, 206, 250, 1],
+  lightslategray: [119, 136, 153, 1],
+  lightslategrey: [119, 136, 153, 1],
+  lightsteelblue: [176, 196, 222, 1],
+  lightyellow: [255, 255, 224, 1],
+  lime: [0, 255, 0, 1],
+  limegreen: [50, 205, 50, 1],
+  linen: [250, 240, 230, 1],
+  magenta: [255, 0, 255, 1],
+  maroon: [128, 0, 0, 1],
+  mediumaquamarine: [102, 205, 170, 1],
+  mediumblue: [0, 0, 205, 1],
+  mediumorchid: [186, 85, 211, 1],
+  mediumpurple: [147, 112, 219, 1],
+  mediumseagreen: [60, 179, 113, 1],
+  mediumslateblue: [123, 104, 238, 1],
+  mediumspringgreen: [0, 250, 154, 1],
+  mediumturquoise: [72, 209, 204, 1],
+  mediumvioletred: [199, 21, 133, 1],
+  midnightblue: [25, 25, 112, 1],
+  mintcream: [245, 255, 250, 1],
+  mistyrose: [255, 228, 225, 1],
+  moccasin: [255, 228, 181, 1],
+  navajowhite: [255, 222, 173, 1],
+  navy: [0, 0, 128, 1],
+  oldlace: [253, 245, 230, 1],
+  olive: [128, 128, 0, 1],
+  olivedrab: [107, 142, 35, 1],
+  orange: [255, 165, 0, 1],
+  orangered: [255, 69, 0, 1],
+  orchid: [218, 112, 214, 1],
+  palegoldenrod: [238, 232, 170, 1],
+  palegreen: [152, 251, 152, 1],
+  paleturquoise: [175, 238, 238, 1],
+  palevioletred: [219, 112, 147, 1],
+  papayawhip: [255, 239, 213, 1],
+  peachpuff: [255, 218, 185, 1],
+  peru: [205, 133, 63, 1],
+  pink: [255, 192, 203, 1],
+  plum: [221, 160, 221, 1],
+  powderblue: [176, 224, 230, 1],
+  purple: [128, 0, 128, 1],
+  rebeccapurple: [102, 51, 153, 1],
+  red: [255, 0, 0, 1],
+  rosybrown: [188, 143, 143, 1],
+  royalblue: [65, 105, 225, 1],
+  saddlebrown: [139, 69, 19, 1],
+  salmon: [250, 128, 114, 1],
+  sandybrown: [244, 164, 96, 1],
+  seagreen: [46, 139, 87, 1],
+  seashell: [255, 245, 238, 1],
+  sienna: [160, 82, 45, 1],
+  silver: [192, 192, 192, 1],
+  skyblue: [135, 206, 235, 1],
+  slateblue: [106, 90, 205, 1],
+  slategray: [112, 128, 144, 1],
+  slategrey: [112, 128, 144, 1],
+  snow: [255, 250, 250, 1],
+  springgreen: [0, 255, 127, 1],
+  steelblue: [70, 130, 180, 1],
+  tan: [210, 180, 140, 1],
+  teal: [0, 128, 128, 1],
+  thistle: [216, 191, 216, 1],
+  tomato: [255, 99, 71, 1],
+  turquoise: [64, 224, 208, 1],
+  violet: [238, 130, 238, 1],
+  wheat: [245, 222, 179, 1],
+  white: [255, 255, 255, 1],
+  whitesmoke: [245, 245, 245, 1],
+  yellow: [255, 255, 0, 1],
+  yellowgreen: [154, 205, 50, 1],
+};
+
+exports.cssColors = cssColors;
--- a/devtools/client/shared/css-color.js
+++ b/devtools/client/shared/css-color.js
@@ -2,16 +2,18 @@
  * 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} = require("chrome");
 const Services = require("Services");
 
+const {cssColors} = require("devtools/client/shared/css-color-db");
+
 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
 
 const SPECIALVALUES = new Set([
   "currentcolor",
   "initial",
   "inherit",
   "transparent",
   "unset"
@@ -53,17 +55,20 @@ const SPECIALVALUES = new Set([
 function CssColor(colorValue) {
   this.newColor(colorValue);
 }
 
 module.exports.colorUtils = {
   CssColor: CssColor,
   rgbToHsl: rgbToHsl,
   setAlpha: setAlpha,
-  classifyColor: classifyColor
+  classifyColor: classifyColor,
+  rgbToColorName: rgbToColorName,
+  colorToRGBA: colorToRGBA,
+  isValidCSSColor: isValidCSSColor,
 };
 
 /**
  * Values used in COLOR_UNIT_PREF
  */
 CssColor.COLORUNIT = {
   "authored": "authored",
   "hex": "hex",
@@ -113,17 +118,17 @@ CssColor.prototype = {
   get hasAlpha() {
     if (!this.valid) {
       return false;
     }
     return this._getRGBATuple().a !== 1;
   },
 
   get valid() {
-    return DOMUtils.isValidCSSColor(this.authored);
+    return isValidCSSColor(this.authored);
   },
 
   /**
    * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
    */
   get transparent() {
     try {
       let tuple = this._getRGBATuple();
@@ -145,17 +150,17 @@ CssColor.prototype = {
 
     try {
       let tuple = this._getRGBATuple();
 
       if (tuple.a !== 1) {
         return this.rgb;
       }
       let {r, g, b} = tuple;
-      return DOMUtils.rgbToColorName(r, g, b);
+      return rgbToColorName(r, g, b);
     } catch (e) {
       return this.hex;
     }
   },
 
   get hex() {
     let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
     if (invalidOrSpecialValue !== false) {
@@ -464,11 +469,320 @@ function classifyColor(value) {
   } else if (value.startsWith("hsl(") || value.startsWith("hsla(")) {
     return CssColor.COLORUNIT.hsl;
   } else if (/^#[0-9a-f]+$/.exec(value)) {
     return CssColor.COLORUNIT.hex;
   }
   return CssColor.COLORUNIT.name;
 }
 
+// This holds a map from colors back to color names for use by
+// rgbToColorName.
+var cssRGBMap;
+
+/**
+ * Given a color, return its name, if it has one.  Throws an exception
+ * if the color does not have a name.
+ *
+ * @param {Number} r, g, b  The color components.
+ * @return {String} the name of the color
+ */
+function rgbToColorName(r, g, b) {
+  if (!cssRGBMap) {
+    cssRGBMap = {};
+    for (let name in cssColors) {
+      let key = JSON.stringify(cssColors[name]);
+      if (!(key in cssRGBMap)) {
+        cssRGBMap[key] = name;
+      }
+    }
+  }
+  let value = cssRGBMap[JSON.stringify([r, g, b, 1])];
+  if (!value) {
+    throw new Error("no such color");
+  }
+  return value;
+}
+
+// Originally from dom/tests/mochitest/ajax/mochikit/MochiKit/Color.js.
+function _hslValue(n1, n2, hue) {
+  if (hue > 6.0) {
+    hue -= 6.0;
+  } else if (hue < 0.0) {
+    hue += 6.0;
+  }
+  let val;
+  if (hue < 1.0) {
+    val = n1 + (n2 - n1) * hue;
+  } else if (hue < 3.0) {
+    val = n2;
+  } else if (hue < 4.0) {
+    val = n1 + (n2 - n1) * (4.0 - hue);
+  } else {
+    val = n1;
+  }
+  return val;
+}
+
+// Originally from dom/tests/mochitest/ajax/mochikit/MochiKit/Color.js.
+function hslToRGB([hue, saturation, lightness]) {
+  let red;
+  let green;
+  let blue;
+  if (saturation === 0) {
+    red = lightness;
+    green = lightness;
+    blue = lightness;
+  } else {
+    let m2;
+    if (lightness <= 0.5) {
+      m2 = lightness * (1.0 + saturation);
+    } else {
+      m2 = lightness + saturation - (lightness * saturation);
+    }
+    let m1 = (2.0 * lightness) - m2;
+    let f = _hslValue;
+    let h6 = hue * 6.0;
+    red = f(m1, m2, h6 + 2);
+    green = f(m1, m2, h6);
+    blue = f(m1, m2, h6 - 2);
+  }
+  return [red, green, blue];
+}
+
+/**
+ * A helper function to convert a hex string like "F0C" to a color.
+ *
+ * @param {String} name the color string
+ * @return {Object} an object of the form {r, g, b, a}; or null if the
+ *         name was not a valid color
+ */
+function hexToRGBA(name) {
+  let r, g, b;
+
+  if (name.length === 3) {
+    let val = parseInt(name, 16);
+    b = ((val & 15) << 4) + (val & 15);
+    val >>= 4;
+    g = ((val & 15) << 4) + (val & 15);
+    val >>= 4;
+    r = ((val & 15) << 4) + (val & 15);
+  } else if (name.length === 6) {
+    let val = parseInt(name, 16);
+    b = val & 255;
+    val >>= 8;
+    g = val & 255;
+    val >>= 8;
+    r = val & 255;
+  } else {
+    return null;
+  }
+
+  return {r, g, b, a: 1};
+}
+
+/**
+ * A helper function to clamp a value.
+ *
+ * @param {Number} value The value to clamp
+ * @param {Number} min The minimum value
+ * @param {Number} max The maximum value
+ * @return {Number} A value between min and max
+ */
+function clamp(value, min, max) {
+  if (value < min) {
+    value = min;
+  }
+  if (value > max) {
+    value = max;
+  }
+  return value;
+}
+
+/**
+ * A helper function to get a token from a lexer, skipping comments
+ * and whitespace.
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @return {CSSToken} The next non-whitespace, non-comment token; or
+ * null at EOF.
+ */
+function getToken(lexer) {
+  while (true) {
+    let token = lexer.nextToken();
+    if (!token || (token.tokenType !== "comment" &&
+                   token.tokenType !== "whitespace")) {
+      return token;
+    }
+  }
+}
+
+/**
+ * A helper function to examine a token and ensure it is a comma.
+ * Then fetch and return the next token.  Returns null if the
+ * token was not a comma, or at EOF.
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @param {CSSToken} token A token to be examined
+ * @return {CSSToken} The next non-whitespace, non-comment token; or
+ * null if token was not a comma, or at EOF.
+ */
+function requireComma(lexer, token) {
+  if (!token || token.tokenType !== "symbol" || token.text !== ",") {
+    return null;
+  }
+  return getToken(lexer);
+}
+
+/**
+ * A helper function to parse the first three arguments to hsl()
+ * or hsla().
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @return {Array} An array of the form [r,g,b]; or null on error.
+ */
+function parseHsl(lexer) {
+  let vals = [];
+
+  let token = getToken(lexer);
+  if (!token || token.tokenType !== "number") {
+    return null;
+  }
+  let val = token.number % 60;
+  if (val < 0) {
+    val += 60;
+  }
+  vals.push(val / 60.0);
+
+  for (let i = 0; i < 2; ++i) {
+    token = requireComma(lexer, getToken(lexer));
+    if (!token || token.tokenType !== "percentage") {
+      return null;
+    }
+    vals.push(clamp(token.number, 0, 100));
+  }
+
+  return hslToRGB(vals).map((elt) => Math.trunc(elt * 255));
+}
+
+/**
+ * A helper function to parse the first three arguments to rgb()
+ * or rgba().
+ *
+ * @param {CSSLexer} lexer The lexer
+ * @return {Array} An array of the form [r,g,b]; or null on error.
+ */
+function parseRgb(lexer) {
+  let isPercentage = false;
+  let vals = [];
+  for (let i = 0; i < 3; ++i) {
+    let token = getToken(lexer);
+    if (i > 0) {
+      token = requireComma(lexer, token);
+    }
+    if (!token) {
+      return null;
+    }
+
+    /* Either all parameters are integers, or all are percentages, so
+       check the first one to see.  */
+    if (i === 0 && token.tokenType === "percentage") {
+      isPercentage = true;
+    }
+
+    if (isPercentage) {
+      if (token.tokenType !== "percentage") {
+        return null;
+      }
+      vals.push(Math.round(255 * clamp(token.number, 0, 100)));
+    } else {
+      if (token.tokenType !== "number" || !token.isInteger) {
+        return null;
+      }
+      vals.push(clamp(token.number, 0, 255));
+    }
+  }
+  return vals;
+}
+
+/**
+ * Convert a string representing a color to an object holding the
+ * color's components.  Any valid CSS color form can be passed in.
+ *
+ * @param {String} name the color
+ * @return {Object} an object of the form {r, g, b, a}; or null if the
+ *         name was not a valid color
+ */
+function colorToRGBA(name) {
+  name = name.trim().toLowerCase();
+
+  if (name in cssColors) {
+    let result = cssColors[name];
+    return {r: result[0], g: result[1], b: result[2], a: result[3]};
+  } else if (name === "transparent") {
+    return {r: 0, g: 0, b: 0, a: 0};
+  } else if (name === "currentcolor") {
+    return {r: 0, g: 0, b: 0, a: 1};
+  }
+
+  let lexer = DOMUtils.getCSSLexer(name);
+
+  let func = getToken(lexer);
+  if (!func) {
+    return null;
+  }
+
+  if (func.tokenType === "id" || func.tokenType === "hash") {
+    if (getToken(lexer) !== null) {
+      return null;
+    }
+    return hexToRGBA(func.text);
+  }
+
+  const expectedFunctions = ["rgba", "rgb", "hsla", "hsl"];
+  if (!func || func.tokenType !== "function" ||
+      !expectedFunctions.includes(func.text)) {
+    return null;
+  }
+
+  let hsl = func.text === "hsl" || func.text === "hsla";
+  let alpha = func.text === "rgba" || func.text === "hsla";
+
+  let vals = hsl ? parseHsl(lexer) : parseRgb(lexer);
+  if (!vals) {
+    return null;
+  }
+
+  if (alpha) {
+    let token = requireComma(lexer, getToken(lexer));
+    if (!token || token.tokenType !== "number") {
+      return null;
+    }
+    vals.push(clamp(token.number, 0, 1));
+  } else {
+    vals.push(1);
+  }
+
+  let parenToken = getToken(lexer);
+  if (!parenToken || parenToken.tokenType !== "symbol" ||
+      parenToken.text !== ")") {
+    return null;
+  }
+  if (getToken(lexer) !== null) {
+    return null;
+  }
+
+  return {r: vals[0], g: vals[1], b: vals[2], a: vals[3]};
+}
+
+/**
+ * Check whether a string names a valid CSS color.
+ *
+ * @param {String} name The string to check
+ * @return {Boolean} True if the string is a CSS color name.
+ */
+function isValidCSSColor(name) {
+  return colorToRGBA(name) !== null;
+}
+
 loader.lazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -13,16 +13,17 @@ DIRS += [
     'vendor',
     'widgets',
 ]
 
 DevToolsModules(
     'AppCacheUtils.jsm',
     'autocomplete-popup.js',
     'browser-loader.js',
+    'css-color-db.js',
     'css-color.js',
     'css-parsing-utils.js',
     'css-reload.js',
     'Curl.jsm',
     'demangle.js',
     'developer-toolbar.js',
     'devices.js',
     'devtools-file-watcher.js',
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -202,43 +202,43 @@ OutputParser.prototype = {
             }
             ++parenDepth;
           } else {
             let functionText = this._collectFunctionText(token, text,
                                                          tokenStream);
 
             if (options.expectCubicBezier && token.text === "cubic-bezier") {
               this._appendCubicBezier(functionText, options);
-            } else if (colorOK() && DOMUtils.isValidCSSColor(functionText)) {
+            } else if (colorOK() && colorUtils.isValidCSSColor(functionText)) {
               this._appendColor(functionText, options);
             } else {
               this._appendTextNode(functionText);
             }
           }
           break;
         }
 
         case "ident":
           if (options.expectCubicBezier &&
               BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
             this._appendCubicBezier(token.text, options);
-          } else if (colorOK() && DOMUtils.isValidCSSColor(token.text)) {
+          } else if (colorOK() && colorUtils.isValidCSSColor(token.text)) {
             this._appendColor(token.text, options);
           } else if (angleOK(token.text)) {
             this._appendAngle(token.text, options);
           } else {
             this._appendTextNode(text.substring(token.startOffset,
                                                 token.endOffset));
           }
           break;
 
         case "id":
         case "hash": {
           let original = text.substring(token.startOffset, token.endOffset);
-          if (colorOK() && DOMUtils.isValidCSSColor(original)) {
+          if (colorOK() && colorUtils.isValidCSSColor(original)) {
             this._appendColor(original, options);
           } else {
             this._appendTextNode(original);
           }
           break;
         }
         case "dimension":
           let value = text.substring(token.startOffset, token.endOffset);
--- a/devtools/client/shared/test/unit/test_cssColor.js
+++ b/devtools/client/shared/test/unit/test_cssColor.js
@@ -1,34 +1,62 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test classifyColor.
 
 "use strict";
 
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm");
 const {colorUtils} = require("devtools/client/shared/css-color");
 
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
 const CLASSIFY_TESTS = [
   { input: "rgb(255,0,192)", output: "rgb" },
   { input: "RGB(255,0,192)", output: "rgb" },
+  { input: "RGB(100%,0%,83%)", output: "rgb" },
   { input: "rgba(255,0,192, 0.25)", output: "rgb" },
-  { input: "hsl(5, 5, 5)", output: "hsl" },
-  { input: "hsla(5, 5, 5, 0.25)", output: "hsl" },
-  { input: "hSlA(5, 5, 5, 0.25)", output: "hsl" },
+  { input: "hsl(5, 5%, 5%)", output: "hsl" },
+  { input: "hsla(5, 5%, 5%, 0.25)", output: "hsl" },
+  { input: "hSlA(5, 5%, 5%, 0.25)", output: "hsl" },
   { input: "#f0c", output: "hex" },
   { input: "#fe01cb", output: "hex" },
   { input: "#FE01CB", output: "hex" },
   { input: "blue", output: "name" },
   { input: "orange", output: "name" }
 ];
 
+function compareWithDomutils(input, isColor) {
+  let ours = colorUtils.colorToRGBA(input);
+  let platform = DOMUtils.colorToRGBA(input);
+  deepEqual(ours, platform, "color " + input + " matches DOMUtils");
+  if (isColor) {
+    ok(ours !== null, "'" + input + "' is a color");
+  } else {
+    ok(ours === null, "'" + input + "' is not a color");
+  }
+}
+
 function run_test() {
   for (let test of CLASSIFY_TESTS) {
     let result = colorUtils.classifyColor(test.input);
     equal(result, test.output, "test classifyColor(" + test.input + ")");
 
     let obj = new colorUtils.CssColor("purple");
     obj.setAuthoredUnitFromColor(test.input);
     equal(obj.colorUnit, test.output,
           "test setAuthoredUnitFromColor(" + test.input + ")");
+
+    // Check that our implementation matches DOMUtils.
+    compareWithDomutils(test.input, true);
+
+    // And check some obvious errors.
+    compareWithDomutils("mumble" + test.input, false);
+    compareWithDomutils(test.input + "trailingstuff", false);
   }
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/unit/test_cssColorDatabase.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that css-color-db matches platform.
+
+"use strict";
+
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+
+var {require} = Cu.import("resource://devtools/shared/Loader.jsm");
+
+loader.lazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
+
+const {colorUtils} = require("devtools/client/shared/css-color");
+const {cssColors} = require("devtools/client/shared/css-color-db");
+
+function isValid(colorName) {
+  ok(colorUtils.isValidCSSColor(colorName),
+     colorName + " is valid in database");
+  ok(DOMUtils.isValidCSSColor(colorName),
+     colorName + " is valid in DOMUtils");
+}
+
+function checkOne(colorName, checkName) {
+  let ours = colorUtils.colorToRGBA(colorName);
+  let fromDom = DOMUtils.colorToRGBA(colorName);
+  deepEqual(ours, fromDom, colorName + " agrees with DOMUtils");
+
+  isValid(colorName);
+
+  if (checkName) {
+    let {r, g, b} = ours;
+
+    // The color we got might not map back to the same name; but our
+    // implementation should agree with DOMUtils about which name is
+    // canonical.
+    let ourName = colorUtils.rgbToColorName(r, g, b);
+    let domName = DOMUtils.rgbToColorName(r, g, b);
+
+    equal(ourName, domName,
+          colorName + " canonical name agrees with DOMUtils");
+  }
+}
+
+function run_test() {
+  for (let name in cssColors) {
+    checkOne(name, true);
+  }
+  checkOne("transparent", false);
+
+  // Now check that platform didn't add a new name when we weren't
+  // looking.
+  let names = DOMUtils.getCSSValuesForProperty("background-color");
+  for (let name of names) {
+    if (name !== "hsl" && name !== "hsla" &&
+        name !== "rgb" && name !== "rgba" &&
+        name !== "inherit" && name !== "initial" && name !== "unset") {
+      checkOne(name, true);
+    }
+  }
+}
--- a/devtools/client/shared/test/unit/xpcshell.ini
+++ b/devtools/client/shared/test/unit/xpcshell.ini
@@ -5,16 +5,17 @@ tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_advanceValidate.js]
 [test_attribute-parsing-01.js]
 [test_attribute-parsing-02.js]
 [test_bezierCanvas.js]
 [test_cssColor.js]
+[test_cssColorDatabase.js]
 [test_cubicBezier.js]
 [test_escapeCSSComment.js]
 [test_parseDeclarations.js]
 [test_parsePseudoClassesAndAttributes.js]
 [test_parseSingleValue.js]
 [test_rewriteDeclarations.js]
 [test_source-utils.js]
 [test_suggestion-picker.js]