toolkit/devtools/css-color.js

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 const {Cc, Ci, Cu} = require("chrome");
     8 const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
    10 const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
    12 const REGEX_JUST_QUOTES  = /^""$/;
    13 const REGEX_RGB_3_TUPLE  = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i;
    14 const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i;
    15 const REGEX_HSL_3_TUPLE  = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i;
    17 /**
    18  * This regex matches:
    19  *  - #F00
    20  *  - #FF0000
    21  *  - hsl()
    22  *  - hsla()
    23  *  - rgb()
    24  *  - rgba()
    25  *  - red
    26  *
    27  *  It also matches css keywords e.g. "background-color" otherwise
    28  *  "background" would be replaced with #6363CE ("background" is a platform
    29  *  color).
    30  */
    31 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;
    33 const SPECIALVALUES = new Set([
    34   "currentcolor",
    35   "initial",
    36   "inherit",
    37   "transparent",
    38   "unset"
    39 ]);
    41 /**
    42  * This module is used to convert between various color types.
    43  *
    44  * Usage:
    45  *   let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
    46  *   let {colorUtils} = devtools.require("devtools/css-color");
    47  *   let color = new colorUtils.CssColor("red");
    48  *
    49  *   color.authored === "red"
    50  *   color.hasAlpha === false
    51  *   color.valid === true
    52  *   color.transparent === false // transparent has a special status.
    53  *   color.name === "red"        // returns hex or rgba when no name available.
    54  *   color.hex === "#F00"        // returns shortHex when available else returns
    55  *                                  longHex. If alpha channel is present then we
    56  *                                  return this.rgba.
    57  *   color.longHex === "#FF0000" // If alpha channel is present then we return
    58  *                                  this.rgba.
    59  *   color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return
    60  *                                     this.rgba.
    61  *   color.rgba === "rgba(255, 0, 0, 1)"
    62  *   color.hsl === "hsl(0, 100%, 50%)"
    63  *   color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
    64  *                                             then we return this.rgba.
    65  *
    66  *   color.toString() === "#F00"; // Outputs the color type determined in the
    67  *                                   COLOR_UNIT_PREF constant (above).
    68  *   // Color objects can be reused
    69  *   color.newColor("green") === "#0F0"; // true
    70  *
    71  *   let processed = colorUtils.processCSSString("color:red; background-color:green;");
    72  *   // Returns "color:#F00; background-color:#0F0;"
    73  *
    74  *   Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
    75  */
    77 function CssColor(colorValue) {
    78   this.newColor(colorValue);
    79 }
    81 module.exports.colorUtils = {
    82   CssColor: CssColor,
    83   processCSSString: processCSSString,
    84   rgbToHsl: rgbToHsl
    85 };
    87 /**
    88  * Values used in COLOR_UNIT_PREF
    89  */
    90 CssColor.COLORUNIT = {
    91   "authored": "authored",
    92   "hex": "hex",
    93   "name": "name",
    94   "rgb": "rgb",
    95   "hsl": "hsl"
    96 };
    98 CssColor.prototype = {
    99   authored: null,
   101   get hasAlpha() {
   102     if (!this.valid) {
   103       return false;
   104     }
   105     return this._getRGBATuple().a !== 1;
   106   },
   108   get valid() {
   109     return this._validateColor(this.authored);
   110   },
   112   /**
   113    * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
   114    */
   115   get transparent() {
   116     try {
   117       let tuple = this._getRGBATuple();
   118       return !(tuple.r || tuple.g || tuple.b || tuple.a);
   119     } catch(e) {
   120       return false;
   121     }
   122   },
   124   get specialValue() {
   125     return SPECIALVALUES.has(this.authored) ? this.authored : null;
   126   },
   128   get name() {
   129     if (!this.valid) {
   130       return "";
   131     }
   132     if (this.specialValue) {
   133       return this.specialValue;
   134     }
   136     try {
   137       let tuple = this._getRGBATuple();
   139       if (tuple.a !== 1) {
   140         return this.rgb;
   141       }
   142       let {r, g, b} = tuple;
   143       return DOMUtils.rgbToColorName(r, g, b);
   144     } catch(e) {
   145       return this.hex;
   146     }
   147   },
   149   get hex() {
   150     if (!this.valid) {
   151       return "";
   152     }
   153     if (this.specialValue) {
   154       return this.specialValue;
   155     }
   156     if (this.hasAlpha) {
   157       return this.rgba;
   158     }
   160     let hex = this.longHex;
   161     if (hex.charAt(1) == hex.charAt(2) &&
   162         hex.charAt(3) == hex.charAt(4) &&
   163         hex.charAt(5) == hex.charAt(6)) {
   164       hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5);
   165     }
   166     return hex;
   167   },
   169   get longHex() {
   170     if (!this.valid) {
   171       return "";
   172     }
   173     if (this.specialValue) {
   174       return this.specialValue;
   175     }
   176     if (this.hasAlpha) {
   177       return this.rgba;
   178     }
   179     return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) {
   180       return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase();
   181     });
   182   },
   184   get rgb() {
   185     if (!this.valid) {
   186       return "";
   187     }
   188     if (this.specialValue) {
   189       return this.specialValue;
   190     }
   191     if (!this.hasAlpha) {
   192       if (this.authored.startsWith("rgb(")) {
   193         // The color is valid and begins with rgb(. Return the authored value.
   194         return this.authored;
   195       }
   196       let tuple = this._getRGBATuple();
   197       return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
   198     }
   199     return this.rgba;
   200   },
   202   get rgba() {
   203     if (!this.valid) {
   204       return "";
   205     }
   206     if (this.specialValue) {
   207       return this.specialValue;
   208     }
   209     if (this.authored.startsWith("rgba(")) {
   210       // The color is valid and begins with rgba(. Return the authored value.
   211         return this.authored;
   212     }
   213     let components = this._getRGBATuple();
   214     return "rgba(" + components.r + ", " +
   215                      components.g + ", " +
   216                      components.b + ", " +
   217                      components.a + ")";
   218   },
   220   get hsl() {
   221     if (!this.valid) {
   222       return "";
   223     }
   224     if (this.specialValue) {
   225       return this.specialValue;
   226     }
   227     if (this.authored.startsWith("hsl(")) {
   228       // The color is valid and begins with hsl(. Return the authored value.
   229       return this.authored;
   230     }
   231     if (this.hasAlpha) {
   232       return this.hsla;
   233     }
   234     return this._hslNoAlpha();
   235   },
   237   get hsla() {
   238     if (!this.valid) {
   239       return "";
   240     }
   241     if (this.specialValue) {
   242       return this.specialValue;
   243     }
   244     if (this.authored.startsWith("hsla(")) {
   245       // The color is valid and begins with hsla(. Return the authored value.
   246       return this.authored;
   247     }
   248     if (this.hasAlpha) {
   249       let a = this._getRGBATuple().a;
   250       return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")");
   251     }
   252     return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)");
   253   },
   255   /**
   256    * Change color
   257    *
   258    * @param  {String} color
   259    *         Any valid color string
   260    */
   261   newColor: function(color) {
   262     this.authored = color.toLowerCase();
   263     return this;
   264   },
   266   /**
   267    * Return a string representing a color of type defined in COLOR_UNIT_PREF.
   268    */
   269   toString: function() {
   270     let color;
   271     let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
   272     let unit = CssColor.COLORUNIT[defaultUnit];
   274     switch(unit) {
   275       case CssColor.COLORUNIT.authored:
   276         color = this.authored;
   277         break;
   278       case CssColor.COLORUNIT.hex:
   279         color = this.hex;
   280         break;
   281       case CssColor.COLORUNIT.hsl:
   282         color = this.hsl;
   283         break;
   284       case CssColor.COLORUNIT.name:
   285         color = this.name;
   286         break;
   287       case CssColor.COLORUNIT.rgb:
   288         color = this.rgb;
   289         break;
   290       default:
   291         color = this.rgb;
   292     }
   293     return color;
   294   },
   296   /**
   297    * Returns a RGBA 4-Tuple representation of a color or transparent as
   298    * appropriate.
   299    */
   300   _getRGBATuple: function() {
   301     let win = Services.appShell.hiddenDOMWindow;
   302     let doc = win.document;
   303     let span = doc.createElement("span");
   304     span.style.color = this.authored;
   305     let computed = win.getComputedStyle(span).color;
   307     if (computed === "transparent") {
   308       return {r: 0, g: 0, b: 0, a: 0};
   309     }
   311     let rgba = computed.match(REGEX_RGBA_4_TUPLE);
   313     if (rgba) {
   314       let [, r, g, b, a] = rgba;
   315       return {r: r, g: g, b: b, a: a};
   316     } else {
   317       let rgb = computed.match(REGEX_RGB_3_TUPLE);
   318       let [, r, g, b] = rgb;
   320       return {r: r, g: g, b: b, a: 1};
   321     }
   322   },
   324   _hslNoAlpha: function() {
   325     let {r, g, b} = this._getRGBATuple();
   327     if (this.authored.startsWith("hsl(")) {
   328       // We perform string manipulations on our output so let's ensure that it
   329       // is formatted as we expect.
   330       let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE);
   331       return "hsl(" + h + ", " + s + ", " + l + ")";
   332     }
   334     let [h,s,l] = rgbToHsl([r,g,b]);
   336     return "hsl(" + h + ", " + s + "%, " + l + "%)";
   337   },
   339   /**
   340    * This method allows comparison of CssColor objects using ===.
   341    */
   342   valueOf: function() {
   343     return this.rgba;
   344   },
   346   _validateColor: function(color) {
   347     if (typeof color !== "string" || color === "") {
   348       return false;
   349     }
   351     let win = Services.appShell.hiddenDOMWindow;
   352     let doc = win.document;
   354     // Create a black span in a hidden window.
   355     let span = doc.createElement("span");
   356     span.style.color = "rgb(0, 0, 0)";
   358     // Attempt to set the color. If the color is no longer black we know that
   359     // color is valid.
   360     span.style.color = color;
   361     if (span.style.color !== "rgb(0, 0, 0)") {
   362       return true;
   363     }
   365     // If the color is black then the above check will have failed. We change
   366     // the span to white and attempt to reapply the color. If the span is not
   367     // white then we know that the color is valid otherwise we return invalid.
   368     span.style.color = "rgb(255, 255, 255)";
   369     span.style.color = color;
   370     return span.style.color !== "rgb(255, 255, 255)";
   371   },
   372 };
   374 /**
   375  * Process a CSS string
   376  *
   377  * @param  {String} value
   378  *         CSS string e.g. "color:red; background-color:green;"
   379  * @return {String}
   380  *         Converted CSS String e.g. "color:#F00; background-color:#0F0;"
   381  */
   382 function processCSSString(value) {
   383   if (value && REGEX_JUST_QUOTES.test(value)) {
   384     return value;
   385   }
   387   let colorPattern = REGEX_ALL_COLORS;
   389   value = value.replace(colorPattern, function(match) {
   390     let color = new CssColor(match);
   391     if (color.valid) {
   392       return color;
   393     }
   394     return match;
   395   });
   396   return value;
   397 }
   399 /**
   400  * Convert rgb value to hsl
   401  *
   402  * @param {array} rgb
   403  *         Array of rgb values
   404  * @return {array}
   405  *         Array of hsl values.
   406  */
   407 function rgbToHsl([r,g,b]) {
   408   r = r / 255;
   409   g = g / 255;
   410   b = b / 255;
   412   let max = Math.max(r, g, b);
   413   let min = Math.min(r, g, b);
   414   let h;
   415   let s;
   416   let l = (max + min) / 2;
   418   if(max == min){
   419     h = s = 0;
   420   } else {
   421     let d = max - min;
   422     s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
   424     switch(max) {
   425       case r:
   426         h = ((g - b) / d) % 6;
   427         break;
   428       case g:
   429         h = (b - r) / d + 2;
   430         break;
   431       case b:
   432         h = (r - g) / d + 4;
   433         break;
   434     }
   435     h *= 60;
   436     if (h < 0) {
   437       h += 360;
   438     }
   439   }
   441   return [Math.round(h), Math.round(s * 100), Math.round(l * 100)];
   442 }
   444 loader.lazyGetter(this, "DOMUtils", function () {
   445   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
   446 });

mercurial