Wed, 31 Dec 2014 13:27:57 +0100
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 });