|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const {Cc, Ci, Cu} = require("chrome"); |
|
8 const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); |
|
9 |
|
10 const COLOR_UNIT_PREF = "devtools.defaultColorUnit"; |
|
11 |
|
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; |
|
16 |
|
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; |
|
32 |
|
33 const SPECIALVALUES = new Set([ |
|
34 "currentcolor", |
|
35 "initial", |
|
36 "inherit", |
|
37 "transparent", |
|
38 "unset" |
|
39 ]); |
|
40 |
|
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 */ |
|
76 |
|
77 function CssColor(colorValue) { |
|
78 this.newColor(colorValue); |
|
79 } |
|
80 |
|
81 module.exports.colorUtils = { |
|
82 CssColor: CssColor, |
|
83 processCSSString: processCSSString, |
|
84 rgbToHsl: rgbToHsl |
|
85 }; |
|
86 |
|
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 }; |
|
97 |
|
98 CssColor.prototype = { |
|
99 authored: null, |
|
100 |
|
101 get hasAlpha() { |
|
102 if (!this.valid) { |
|
103 return false; |
|
104 } |
|
105 return this._getRGBATuple().a !== 1; |
|
106 }, |
|
107 |
|
108 get valid() { |
|
109 return this._validateColor(this.authored); |
|
110 }, |
|
111 |
|
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 }, |
|
123 |
|
124 get specialValue() { |
|
125 return SPECIALVALUES.has(this.authored) ? this.authored : null; |
|
126 }, |
|
127 |
|
128 get name() { |
|
129 if (!this.valid) { |
|
130 return ""; |
|
131 } |
|
132 if (this.specialValue) { |
|
133 return this.specialValue; |
|
134 } |
|
135 |
|
136 try { |
|
137 let tuple = this._getRGBATuple(); |
|
138 |
|
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 }, |
|
148 |
|
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 } |
|
159 |
|
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 }, |
|
168 |
|
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 }, |
|
183 |
|
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 }, |
|
201 |
|
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 }, |
|
219 |
|
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 }, |
|
236 |
|
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 }, |
|
254 |
|
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 }, |
|
265 |
|
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]; |
|
273 |
|
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 }, |
|
295 |
|
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; |
|
306 |
|
307 if (computed === "transparent") { |
|
308 return {r: 0, g: 0, b: 0, a: 0}; |
|
309 } |
|
310 |
|
311 let rgba = computed.match(REGEX_RGBA_4_TUPLE); |
|
312 |
|
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; |
|
319 |
|
320 return {r: r, g: g, b: b, a: 1}; |
|
321 } |
|
322 }, |
|
323 |
|
324 _hslNoAlpha: function() { |
|
325 let {r, g, b} = this._getRGBATuple(); |
|
326 |
|
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 } |
|
333 |
|
334 let [h,s,l] = rgbToHsl([r,g,b]); |
|
335 |
|
336 return "hsl(" + h + ", " + s + "%, " + l + "%)"; |
|
337 }, |
|
338 |
|
339 /** |
|
340 * This method allows comparison of CssColor objects using ===. |
|
341 */ |
|
342 valueOf: function() { |
|
343 return this.rgba; |
|
344 }, |
|
345 |
|
346 _validateColor: function(color) { |
|
347 if (typeof color !== "string" || color === "") { |
|
348 return false; |
|
349 } |
|
350 |
|
351 let win = Services.appShell.hiddenDOMWindow; |
|
352 let doc = win.document; |
|
353 |
|
354 // Create a black span in a hidden window. |
|
355 let span = doc.createElement("span"); |
|
356 span.style.color = "rgb(0, 0, 0)"; |
|
357 |
|
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 } |
|
364 |
|
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 }; |
|
373 |
|
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 } |
|
386 |
|
387 let colorPattern = REGEX_ALL_COLORS; |
|
388 |
|
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 } |
|
398 |
|
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; |
|
411 |
|
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; |
|
417 |
|
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); |
|
423 |
|
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 } |
|
440 |
|
441 return [Math.round(h), Math.round(s * 100), Math.round(l * 100)]; |
|
442 } |
|
443 |
|
444 loader.lazyGetter(this, "DOMUtils", function () { |
|
445 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); |
|
446 }); |