Bumping manifests a=b2g-bump
[gecko.git] / toolkit / devtools / css-color.js
blob170e245682285cd9ad3ba61bd78a816b3310e121
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);
81 module.exports.colorUtils = {
82   CssColor: CssColor,
83   processCSSString: processCSSString,
84   rgbToHsl: rgbToHsl
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"
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   },
375  * Process a CSS string
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;
400  * Convert rgb value to hsl
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)];
444 loader.lazyGetter(this, "DOMUtils", function () {
445   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);