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/. */
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;
27 * It also matches css keywords e.g. "background-color" otherwise
28 * "background" would be replaced with #6363CE ("background" is a platform
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([
42 * This module is used to convert between various color types.
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");
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
57 * color.longHex === "#FF0000" // If alpha channel is present then we return
59 * color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return
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.
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
71 * let processed = colorUtils.processCSSString("color:red; background-color:green;");
72 * // Returns "color:#F00; background-color:#0F0;"
74 * Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
77 function CssColor(colorValue) {
78 this.newColor(colorValue);
81 module.exports.colorUtils = {
83 processCSSString: processCSSString,
88 * Values used in COLOR_UNIT_PREF
90 CssColor.COLORUNIT = {
91 "authored": "authored",
98 CssColor.prototype = {
105 return this._getRGBATuple().a !== 1;
109 return this._validateColor(this.authored);
113 * Return true for all transparent values e.g. rgba(0, 0, 0, 0).
117 let tuple = this._getRGBATuple();
118 return !(tuple.r || tuple.g || tuple.b || tuple.a);
125 return SPECIALVALUES.has(this.authored) ? this.authored : null;
132 if (this.specialValue) {
133 return this.specialValue;
137 let tuple = this._getRGBATuple();
142 let {r, g, b} = tuple;
143 return DOMUtils.rgbToColorName(r, g, b);
153 if (this.specialValue) {
154 return this.specialValue;
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);
173 if (this.specialValue) {
174 return this.specialValue;
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();
188 if (this.specialValue) {
189 return this.specialValue;
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;
196 let tuple = this._getRGBATuple();
197 return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
206 if (this.specialValue) {
207 return this.specialValue;
209 if (this.authored.startsWith("rgba(")) {
210 // The color is valid and begins with rgba(. Return the authored value.
211 return this.authored;
213 let components = this._getRGBATuple();
214 return "rgba(" + components.r + ", " +
215 components.g + ", " +
216 components.b + ", " +
224 if (this.specialValue) {
225 return this.specialValue;
227 if (this.authored.startsWith("hsl(")) {
228 // The color is valid and begins with hsl(. Return the authored value.
229 return this.authored;
234 return this._hslNoAlpha();
241 if (this.specialValue) {
242 return this.specialValue;
244 if (this.authored.startsWith("hsla(")) {
245 // The color is valid and begins with hsla(. Return the authored value.
246 return this.authored;
249 let a = this._getRGBATuple().a;
250 return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")");
252 return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)");
258 * @param {String} color
259 * Any valid color string
261 newColor: function(color) {
262 this.authored = color.toLowerCase();
267 * Return a string representing a color of type defined in COLOR_UNIT_PREF.
269 toString: function() {
271 let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
272 let unit = CssColor.COLORUNIT[defaultUnit];
275 case CssColor.COLORUNIT.authored:
276 color = this.authored;
278 case CssColor.COLORUNIT.hex:
281 case CssColor.COLORUNIT.hsl:
284 case CssColor.COLORUNIT.name:
287 case CssColor.COLORUNIT.rgb:
297 * Returns a RGBA 4-Tuple representation of a color or transparent as
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};
311 let rgba = computed.match(REGEX_RGBA_4_TUPLE);
314 let [, r, g, b, a] = rgba;
315 return {r: r, g: g, b: b, a: a};
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};
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 + ")";
334 let [h,s,l] = rgbToHsl([r,g,b]);
336 return "hsl(" + h + ", " + s + "%, " + l + "%)";
340 * This method allows comparison of CssColor objects using ===.
342 valueOf: function() {
346 _validateColor: function(color) {
347 if (typeof color !== "string" || color === "") {
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
360 span.style.color = color;
361 if (span.style.color !== "rgb(0, 0, 0)") {
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)";
375 * Process a CSS string
377 * @param {String} value
378 * CSS string e.g. "color:red; background-color:green;"
380 * Converted CSS String e.g. "color:#F00; background-color:#0F0;"
382 function processCSSString(value) {
383 if (value && REGEX_JUST_QUOTES.test(value)) {
387 let colorPattern = REGEX_ALL_COLORS;
389 value = value.replace(colorPattern, function(match) {
390 let color = new CssColor(match);
400 * Convert rgb value to hsl
403 * Array of rgb values
405 * Array of hsl values.
407 function rgbToHsl([r,g,b]) {
412 let max = Math.max(r, g, b);
413 let min = Math.min(r, g, b);
416 let l = (max + min) / 2;
422 s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
426 h = ((g - b) / d) % 6;
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);