toolkit/devtools/css-color.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/devtools/css-color.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,446 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +const {Cc, Ci, Cu} = require("chrome");
    1.11 +const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
    1.12 +
    1.13 +const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
    1.14 +
    1.15 +const REGEX_JUST_QUOTES  = /^""$/;
    1.16 +const REGEX_RGB_3_TUPLE  = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i;
    1.17 +const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i;
    1.18 +const REGEX_HSL_3_TUPLE  = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i;
    1.19 +
    1.20 +/**
    1.21 + * This regex matches:
    1.22 + *  - #F00
    1.23 + *  - #FF0000
    1.24 + *  - hsl()
    1.25 + *  - hsla()
    1.26 + *  - rgb()
    1.27 + *  - rgba()
    1.28 + *  - red
    1.29 + *
    1.30 + *  It also matches css keywords e.g. "background-color" otherwise
    1.31 + *  "background" would be replaced with #6363CE ("background" is a platform
    1.32 + *  color).
    1.33 + */
    1.34 +const REGEX_ALL_COLORS = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
    1.35 +
    1.36 +const SPECIALVALUES = new Set([
    1.37 +  "currentcolor",
    1.38 +  "initial",
    1.39 +  "inherit",
    1.40 +  "transparent",
    1.41 +  "unset"
    1.42 +]);
    1.43 +
    1.44 +/**
    1.45 + * This module is used to convert between various color types.
    1.46 + *
    1.47 + * Usage:
    1.48 + *   let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
    1.49 + *   let {colorUtils} = devtools.require("devtools/css-color");
    1.50 + *   let color = new colorUtils.CssColor("red");
    1.51 + *
    1.52 + *   color.authored === "red"
    1.53 + *   color.hasAlpha === false
    1.54 + *   color.valid === true
    1.55 + *   color.transparent === false // transparent has a special status.
    1.56 + *   color.name === "red"        // returns hex or rgba when no name available.
    1.57 + *   color.hex === "#F00"        // returns shortHex when available else returns
    1.58 + *                                  longHex. If alpha channel is present then we
    1.59 + *                                  return this.rgba.
    1.60 + *   color.longHex === "#FF0000" // If alpha channel is present then we return
    1.61 + *                                  this.rgba.
    1.62 + *   color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return
    1.63 + *                                     this.rgba.
    1.64 + *   color.rgba === "rgba(255, 0, 0, 1)"
    1.65 + *   color.hsl === "hsl(0, 100%, 50%)"
    1.66 + *   color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
    1.67 + *                                             then we return this.rgba.
    1.68 + *
    1.69 + *   color.toString() === "#F00"; // Outputs the color type determined in the
    1.70 + *                                   COLOR_UNIT_PREF constant (above).
    1.71 + *   // Color objects can be reused
    1.72 + *   color.newColor("green") === "#0F0"; // true
    1.73 + *
    1.74 + *   let processed = colorUtils.processCSSString("color:red; background-color:green;");
    1.75 + *   // Returns "color:#F00; background-color:#0F0;"
    1.76 + *
    1.77 + *   Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
    1.78 + */
    1.79 +
    1.80 +function CssColor(colorValue) {
    1.81 +  this.newColor(colorValue);
    1.82 +}
    1.83 +
    1.84 +module.exports.colorUtils = {
    1.85 +  CssColor: CssColor,
    1.86 +  processCSSString: processCSSString,
    1.87 +  rgbToHsl: rgbToHsl
    1.88 +};
    1.89 +
    1.90 +/**
    1.91 + * Values used in COLOR_UNIT_PREF
    1.92 + */
    1.93 +CssColor.COLORUNIT = {
    1.94 +  "authored": "authored",
    1.95 +  "hex": "hex",
    1.96 +  "name": "name",
    1.97 +  "rgb": "rgb",
    1.98 +  "hsl": "hsl"
    1.99 +};
   1.100 +
   1.101 +CssColor.prototype = {
   1.102 +  authored: null,
   1.103 +
   1.104 +  get hasAlpha() {
   1.105 +    if (!this.valid) {
   1.106 +      return false;
   1.107 +    }
   1.108 +    return this._getRGBATuple().a !== 1;
   1.109 +  },
   1.110 +
   1.111 +  get valid() {
   1.112 +    return this._validateColor(this.authored);
   1.113 +  },
   1.114 +
   1.115 +  /**
   1.116 +   * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
   1.117 +   */
   1.118 +  get transparent() {
   1.119 +    try {
   1.120 +      let tuple = this._getRGBATuple();
   1.121 +      return !(tuple.r || tuple.g || tuple.b || tuple.a);
   1.122 +    } catch(e) {
   1.123 +      return false;
   1.124 +    }
   1.125 +  },
   1.126 +
   1.127 +  get specialValue() {
   1.128 +    return SPECIALVALUES.has(this.authored) ? this.authored : null;
   1.129 +  },
   1.130 +
   1.131 +  get name() {
   1.132 +    if (!this.valid) {
   1.133 +      return "";
   1.134 +    }
   1.135 +    if (this.specialValue) {
   1.136 +      return this.specialValue;
   1.137 +    }
   1.138 +
   1.139 +    try {
   1.140 +      let tuple = this._getRGBATuple();
   1.141 +
   1.142 +      if (tuple.a !== 1) {
   1.143 +        return this.rgb;
   1.144 +      }
   1.145 +      let {r, g, b} = tuple;
   1.146 +      return DOMUtils.rgbToColorName(r, g, b);
   1.147 +    } catch(e) {
   1.148 +      return this.hex;
   1.149 +    }
   1.150 +  },
   1.151 +
   1.152 +  get hex() {
   1.153 +    if (!this.valid) {
   1.154 +      return "";
   1.155 +    }
   1.156 +    if (this.specialValue) {
   1.157 +      return this.specialValue;
   1.158 +    }
   1.159 +    if (this.hasAlpha) {
   1.160 +      return this.rgba;
   1.161 +    }
   1.162 +
   1.163 +    let hex = this.longHex;
   1.164 +    if (hex.charAt(1) == hex.charAt(2) &&
   1.165 +        hex.charAt(3) == hex.charAt(4) &&
   1.166 +        hex.charAt(5) == hex.charAt(6)) {
   1.167 +      hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
   1.168 +    }
   1.169 +    return hex;
   1.170 +  },
   1.171 +
   1.172 +  get longHex() {
   1.173 +    if (!this.valid) {
   1.174 +      return "";
   1.175 +    }
   1.176 +    if (this.specialValue) {
   1.177 +      return this.specialValue;
   1.178 +    }
   1.179 +    if (this.hasAlpha) {
   1.180 +      return this.rgba;
   1.181 +    }
   1.182 +    return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) {
   1.183 +      return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase();
   1.184 +    });
   1.185 +  },
   1.186 +
   1.187 +  get rgb() {
   1.188 +    if (!this.valid) {
   1.189 +      return "";
   1.190 +    }
   1.191 +    if (this.specialValue) {
   1.192 +      return this.specialValue;
   1.193 +    }
   1.194 +    if (!this.hasAlpha) {
   1.195 +      if (this.authored.startsWith("rgb(")) {
   1.196 +        // The color is valid and begins with rgb(. Return the authored value.
   1.197 +        return this.authored;
   1.198 +      }
   1.199 +      let tuple = this._getRGBATuple();
   1.200 +      return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
   1.201 +    }
   1.202 +    return this.rgba;
   1.203 +  },
   1.204 +
   1.205 +  get rgba() {
   1.206 +    if (!this.valid) {
   1.207 +      return "";
   1.208 +    }
   1.209 +    if (this.specialValue) {
   1.210 +      return this.specialValue;
   1.211 +    }
   1.212 +    if (this.authored.startsWith("rgba(")) {
   1.213 +      // The color is valid and begins with rgba(. Return the authored value.
   1.214 +        return this.authored;
   1.215 +    }
   1.216 +    let components = this._getRGBATuple();
   1.217 +    return "rgba(" + components.r + ", " +
   1.218 +                     components.g + ", " +
   1.219 +                     components.b + ", " +
   1.220 +                     components.a + ")";
   1.221 +  },
   1.222 +
   1.223 +  get hsl() {
   1.224 +    if (!this.valid) {
   1.225 +      return "";
   1.226 +    }
   1.227 +    if (this.specialValue) {
   1.228 +      return this.specialValue;
   1.229 +    }
   1.230 +    if (this.authored.startsWith("hsl(")) {
   1.231 +      // The color is valid and begins with hsl(. Return the authored value.
   1.232 +      return this.authored;
   1.233 +    }
   1.234 +    if (this.hasAlpha) {
   1.235 +      return this.hsla;
   1.236 +    }
   1.237 +    return this._hslNoAlpha();
   1.238 +  },
   1.239 +
   1.240 +  get hsla() {
   1.241 +    if (!this.valid) {
   1.242 +      return "";
   1.243 +    }
   1.244 +    if (this.specialValue) {
   1.245 +      return this.specialValue;
   1.246 +    }
   1.247 +    if (this.authored.startsWith("hsla(")) {
   1.248 +      // The color is valid and begins with hsla(. Return the authored value.
   1.249 +      return this.authored;
   1.250 +    }
   1.251 +    if (this.hasAlpha) {
   1.252 +      let a = this._getRGBATuple().a;
   1.253 +      return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")");
   1.254 +    }
   1.255 +    return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)");
   1.256 +  },
   1.257 +
   1.258 +  /**
   1.259 +   * Change color
   1.260 +   *
   1.261 +   * @param  {String} color
   1.262 +   *         Any valid color string
   1.263 +   */
   1.264 +  newColor: function(color) {
   1.265 +    this.authored = color.toLowerCase();
   1.266 +    return this;
   1.267 +  },
   1.268 +
   1.269 +  /**
   1.270 +   * Return a string representing a color of type defined in COLOR_UNIT_PREF.
   1.271 +   */
   1.272 +  toString: function() {
   1.273 +    let color;
   1.274 +    let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
   1.275 +    let unit = CssColor.COLORUNIT[defaultUnit];
   1.276 +
   1.277 +    switch(unit) {
   1.278 +      case CssColor.COLORUNIT.authored:
   1.279 +        color = this.authored;
   1.280 +        break;
   1.281 +      case CssColor.COLORUNIT.hex:
   1.282 +        color = this.hex;
   1.283 +        break;
   1.284 +      case CssColor.COLORUNIT.hsl:
   1.285 +        color = this.hsl;
   1.286 +        break;
   1.287 +      case CssColor.COLORUNIT.name:
   1.288 +        color = this.name;
   1.289 +        break;
   1.290 +      case CssColor.COLORUNIT.rgb:
   1.291 +        color = this.rgb;
   1.292 +        break;
   1.293 +      default:
   1.294 +        color = this.rgb;
   1.295 +    }
   1.296 +    return color;
   1.297 +  },
   1.298 +
   1.299 +  /**
   1.300 +   * Returns a RGBA 4-Tuple representation of a color or transparent as
   1.301 +   * appropriate.
   1.302 +   */
   1.303 +  _getRGBATuple: function() {
   1.304 +    let win = Services.appShell.hiddenDOMWindow;
   1.305 +    let doc = win.document;
   1.306 +    let span = doc.createElement("span");
   1.307 +    span.style.color = this.authored;
   1.308 +    let computed = win.getComputedStyle(span).color;
   1.309 +
   1.310 +    if (computed === "transparent") {
   1.311 +      return {r: 0, g: 0, b: 0, a: 0};
   1.312 +    }
   1.313 +
   1.314 +    let rgba = computed.match(REGEX_RGBA_4_TUPLE);
   1.315 +
   1.316 +    if (rgba) {
   1.317 +      let [, r, g, b, a] = rgba;
   1.318 +      return {r: r, g: g, b: b, a: a};
   1.319 +    } else {
   1.320 +      let rgb = computed.match(REGEX_RGB_3_TUPLE);
   1.321 +      let [, r, g, b] = rgb;
   1.322 +
   1.323 +      return {r: r, g: g, b: b, a: 1};
   1.324 +    }
   1.325 +  },
   1.326 +
   1.327 +  _hslNoAlpha: function() {
   1.328 +    let {r, g, b} = this._getRGBATuple();
   1.329 +
   1.330 +    if (this.authored.startsWith("hsl(")) {
   1.331 +      // We perform string manipulations on our output so let's ensure that it
   1.332 +      // is formatted as we expect.
   1.333 +      let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE);
   1.334 +      return "hsl(" + h + ", " + s + ", " + l + ")";
   1.335 +    }
   1.336 +
   1.337 +    let [h,s,l] = rgbToHsl([r,g,b]);
   1.338 +
   1.339 +    return "hsl(" + h + ", " + s + "%, " + l + "%)";
   1.340 +  },
   1.341 +
   1.342 +  /**
   1.343 +   * This method allows comparison of CssColor objects using ===.
   1.344 +   */
   1.345 +  valueOf: function() {
   1.346 +    return this.rgba;
   1.347 +  },
   1.348 +
   1.349 +  _validateColor: function(color) {
   1.350 +    if (typeof color !== "string" || color === "") {
   1.351 +      return false;
   1.352 +    }
   1.353 +
   1.354 +    let win = Services.appShell.hiddenDOMWindow;
   1.355 +    let doc = win.document;
   1.356 +
   1.357 +    // Create a black span in a hidden window.
   1.358 +    let span = doc.createElement("span");
   1.359 +    span.style.color = "rgb(0, 0, 0)";
   1.360 +
   1.361 +    // Attempt to set the color. If the color is no longer black we know that
   1.362 +    // color is valid.
   1.363 +    span.style.color = color;
   1.364 +    if (span.style.color !== "rgb(0, 0, 0)") {
   1.365 +      return true;
   1.366 +    }
   1.367 +
   1.368 +    // If the color is black then the above check will have failed. We change
   1.369 +    // the span to white and attempt to reapply the color. If the span is not
   1.370 +    // white then we know that the color is valid otherwise we return invalid.
   1.371 +    span.style.color = "rgb(255, 255, 255)";
   1.372 +    span.style.color = color;
   1.373 +    return span.style.color !== "rgb(255, 255, 255)";
   1.374 +  },
   1.375 +};
   1.376 +
   1.377 +/**
   1.378 + * Process a CSS string
   1.379 + *
   1.380 + * @param  {String} value
   1.381 + *         CSS string e.g. "color:red; background-color:green;"
   1.382 + * @return {String}
   1.383 + *         Converted CSS String e.g. "color:#F00; background-color:#0F0;"
   1.384 + */
   1.385 +function processCSSString(value) {
   1.386 +  if (value && REGEX_JUST_QUOTES.test(value)) {
   1.387 +    return value;
   1.388 +  }
   1.389 +
   1.390 +  let colorPattern = REGEX_ALL_COLORS;
   1.391 +
   1.392 +  value = value.replace(colorPattern, function(match) {
   1.393 +    let color = new CssColor(match);
   1.394 +    if (color.valid) {
   1.395 +      return color;
   1.396 +    }
   1.397 +    return match;
   1.398 +  });
   1.399 +  return value;
   1.400 +}
   1.401 +
   1.402 +/**
   1.403 + * Convert rgb value to hsl
   1.404 + *
   1.405 + * @param {array} rgb
   1.406 + *         Array of rgb values
   1.407 + * @return {array}
   1.408 + *         Array of hsl values.
   1.409 + */
   1.410 +function rgbToHsl([r,g,b]) {
   1.411 +  r = r / 255;
   1.412 +  g = g / 255;
   1.413 +  b = b / 255;
   1.414 +
   1.415 +  let max = Math.max(r, g, b);
   1.416 +  let min = Math.min(r, g, b);
   1.417 +  let h;
   1.418 +  let s;
   1.419 +  let l = (max + min) / 2;
   1.420 +
   1.421 +  if(max == min){
   1.422 +    h = s = 0;
   1.423 +  } else {
   1.424 +    let d = max - min;
   1.425 +    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
   1.426 +
   1.427 +    switch(max) {
   1.428 +      case r:
   1.429 +        h = ((g - b) / d) % 6;
   1.430 +        break;
   1.431 +      case g:
   1.432 +        h = (b - r) / d + 2;
   1.433 +        break;
   1.434 +      case b:
   1.435 +        h = (r - g) / d + 4;
   1.436 +        break;
   1.437 +    }
   1.438 +    h *= 60;
   1.439 +    if (h < 0) {
   1.440 +      h += 360;
   1.441 +    }
   1.442 +  }
   1.443 +
   1.444 +  return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
   1.445 +}
   1.446 +
   1.447 +loader.lazyGetter(this, "DOMUtils", function () {
   1.448 +  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
   1.449 +});

mercurial