michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Cc, Ci, Cu} = require("chrome"); michael@0: const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); michael@0: michael@0: const COLOR_UNIT_PREF = "devtools.defaultColorUnit"; michael@0: michael@0: const REGEX_JUST_QUOTES = /^""$/; michael@0: const REGEX_RGB_3_TUPLE = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i; michael@0: const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i; michael@0: const REGEX_HSL_3_TUPLE = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i; michael@0: michael@0: /** michael@0: * This regex matches: michael@0: * - #F00 michael@0: * - #FF0000 michael@0: * - hsl() michael@0: * - hsla() michael@0: * - rgb() michael@0: * - rgba() michael@0: * - red michael@0: * michael@0: * It also matches css keywords e.g. "background-color" otherwise michael@0: * "background" would be replaced with #6363CE ("background" is a platform michael@0: * color). michael@0: */ michael@0: 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; michael@0: michael@0: const SPECIALVALUES = new Set([ michael@0: "currentcolor", michael@0: "initial", michael@0: "inherit", michael@0: "transparent", michael@0: "unset" michael@0: ]); michael@0: michael@0: /** michael@0: * This module is used to convert between various color types. michael@0: * michael@0: * Usage: michael@0: * let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); michael@0: * let {colorUtils} = devtools.require("devtools/css-color"); michael@0: * let color = new colorUtils.CssColor("red"); michael@0: * michael@0: * color.authored === "red" michael@0: * color.hasAlpha === false michael@0: * color.valid === true michael@0: * color.transparent === false // transparent has a special status. michael@0: * color.name === "red" // returns hex or rgba when no name available. michael@0: * color.hex === "#F00" // returns shortHex when available else returns michael@0: * longHex. If alpha channel is present then we michael@0: * return this.rgba. michael@0: * color.longHex === "#FF0000" // If alpha channel is present then we return michael@0: * this.rgba. michael@0: * color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return michael@0: * this.rgba. michael@0: * color.rgba === "rgba(255, 0, 0, 1)" michael@0: * color.hsl === "hsl(0, 100%, 50%)" michael@0: * color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present michael@0: * then we return this.rgba. michael@0: * michael@0: * color.toString() === "#F00"; // Outputs the color type determined in the michael@0: * COLOR_UNIT_PREF constant (above). michael@0: * // Color objects can be reused michael@0: * color.newColor("green") === "#0F0"; // true michael@0: * michael@0: * let processed = colorUtils.processCSSString("color:red; background-color:green;"); michael@0: * // Returns "color:#F00; background-color:#0F0;" michael@0: * michael@0: * Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT. michael@0: */ michael@0: michael@0: function CssColor(colorValue) { michael@0: this.newColor(colorValue); michael@0: } michael@0: michael@0: module.exports.colorUtils = { michael@0: CssColor: CssColor, michael@0: processCSSString: processCSSString, michael@0: rgbToHsl: rgbToHsl michael@0: }; michael@0: michael@0: /** michael@0: * Values used in COLOR_UNIT_PREF michael@0: */ michael@0: CssColor.COLORUNIT = { michael@0: "authored": "authored", michael@0: "hex": "hex", michael@0: "name": "name", michael@0: "rgb": "rgb", michael@0: "hsl": "hsl" michael@0: }; michael@0: michael@0: CssColor.prototype = { michael@0: authored: null, michael@0: michael@0: get hasAlpha() { michael@0: if (!this.valid) { michael@0: return false; michael@0: } michael@0: return this._getRGBATuple().a !== 1; michael@0: }, michael@0: michael@0: get valid() { michael@0: return this._validateColor(this.authored); michael@0: }, michael@0: michael@0: /** michael@0: * Return true for all transparent values e.g. rgba(0, 0, 0, 0). michael@0: */ michael@0: get transparent() { michael@0: try { michael@0: let tuple = this._getRGBATuple(); michael@0: return !(tuple.r || tuple.g || tuple.b || tuple.a); michael@0: } catch(e) { michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: get specialValue() { michael@0: return SPECIALVALUES.has(this.authored) ? this.authored : null; michael@0: }, michael@0: michael@0: get name() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: michael@0: try { michael@0: let tuple = this._getRGBATuple(); michael@0: michael@0: if (tuple.a !== 1) { michael@0: return this.rgb; michael@0: } michael@0: let {r, g, b} = tuple; michael@0: return DOMUtils.rgbToColorName(r, g, b); michael@0: } catch(e) { michael@0: return this.hex; michael@0: } michael@0: }, michael@0: michael@0: get hex() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: if (this.hasAlpha) { michael@0: return this.rgba; michael@0: } michael@0: michael@0: let hex = this.longHex; michael@0: if (hex.charAt(1) == hex.charAt(2) && michael@0: hex.charAt(3) == hex.charAt(4) && michael@0: hex.charAt(5) == hex.charAt(6)) { michael@0: hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5); michael@0: } michael@0: return hex; michael@0: }, michael@0: michael@0: get longHex() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: if (this.hasAlpha) { michael@0: return this.rgba; michael@0: } michael@0: return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) { michael@0: return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase(); michael@0: }); michael@0: }, michael@0: michael@0: get rgb() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: if (!this.hasAlpha) { michael@0: if (this.authored.startsWith("rgb(")) { michael@0: // The color is valid and begins with rgb(. Return the authored value. michael@0: return this.authored; michael@0: } michael@0: let tuple = this._getRGBATuple(); michael@0: return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")"; michael@0: } michael@0: return this.rgba; michael@0: }, michael@0: michael@0: get rgba() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: if (this.authored.startsWith("rgba(")) { michael@0: // The color is valid and begins with rgba(. Return the authored value. michael@0: return this.authored; michael@0: } michael@0: let components = this._getRGBATuple(); michael@0: return "rgba(" + components.r + ", " + michael@0: components.g + ", " + michael@0: components.b + ", " + michael@0: components.a + ")"; michael@0: }, michael@0: michael@0: get hsl() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: if (this.authored.startsWith("hsl(")) { michael@0: // The color is valid and begins with hsl(. Return the authored value. michael@0: return this.authored; michael@0: } michael@0: if (this.hasAlpha) { michael@0: return this.hsla; michael@0: } michael@0: return this._hslNoAlpha(); michael@0: }, michael@0: michael@0: get hsla() { michael@0: if (!this.valid) { michael@0: return ""; michael@0: } michael@0: if (this.specialValue) { michael@0: return this.specialValue; michael@0: } michael@0: if (this.authored.startsWith("hsla(")) { michael@0: // The color is valid and begins with hsla(. Return the authored value. michael@0: return this.authored; michael@0: } michael@0: if (this.hasAlpha) { michael@0: let a = this._getRGBATuple().a; michael@0: return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")"); michael@0: } michael@0: return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)"); michael@0: }, michael@0: michael@0: /** michael@0: * Change color michael@0: * michael@0: * @param {String} color michael@0: * Any valid color string michael@0: */ michael@0: newColor: function(color) { michael@0: this.authored = color.toLowerCase(); michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Return a string representing a color of type defined in COLOR_UNIT_PREF. michael@0: */ michael@0: toString: function() { michael@0: let color; michael@0: let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF); michael@0: let unit = CssColor.COLORUNIT[defaultUnit]; michael@0: michael@0: switch(unit) { michael@0: case CssColor.COLORUNIT.authored: michael@0: color = this.authored; michael@0: break; michael@0: case CssColor.COLORUNIT.hex: michael@0: color = this.hex; michael@0: break; michael@0: case CssColor.COLORUNIT.hsl: michael@0: color = this.hsl; michael@0: break; michael@0: case CssColor.COLORUNIT.name: michael@0: color = this.name; michael@0: break; michael@0: case CssColor.COLORUNIT.rgb: michael@0: color = this.rgb; michael@0: break; michael@0: default: michael@0: color = this.rgb; michael@0: } michael@0: return color; michael@0: }, michael@0: michael@0: /** michael@0: * Returns a RGBA 4-Tuple representation of a color or transparent as michael@0: * appropriate. michael@0: */ michael@0: _getRGBATuple: function() { michael@0: let win = Services.appShell.hiddenDOMWindow; michael@0: let doc = win.document; michael@0: let span = doc.createElement("span"); michael@0: span.style.color = this.authored; michael@0: let computed = win.getComputedStyle(span).color; michael@0: michael@0: if (computed === "transparent") { michael@0: return {r: 0, g: 0, b: 0, a: 0}; michael@0: } michael@0: michael@0: let rgba = computed.match(REGEX_RGBA_4_TUPLE); michael@0: michael@0: if (rgba) { michael@0: let [, r, g, b, a] = rgba; michael@0: return {r: r, g: g, b: b, a: a}; michael@0: } else { michael@0: let rgb = computed.match(REGEX_RGB_3_TUPLE); michael@0: let [, r, g, b] = rgb; michael@0: michael@0: return {r: r, g: g, b: b, a: 1}; michael@0: } michael@0: }, michael@0: michael@0: _hslNoAlpha: function() { michael@0: let {r, g, b} = this._getRGBATuple(); michael@0: michael@0: if (this.authored.startsWith("hsl(")) { michael@0: // We perform string manipulations on our output so let's ensure that it michael@0: // is formatted as we expect. michael@0: let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE); michael@0: return "hsl(" + h + ", " + s + ", " + l + ")"; michael@0: } michael@0: michael@0: let [h,s,l] = rgbToHsl([r,g,b]); michael@0: michael@0: return "hsl(" + h + ", " + s + "%, " + l + "%)"; michael@0: }, michael@0: michael@0: /** michael@0: * This method allows comparison of CssColor objects using ===. michael@0: */ michael@0: valueOf: function() { michael@0: return this.rgba; michael@0: }, michael@0: michael@0: _validateColor: function(color) { michael@0: if (typeof color !== "string" || color === "") { michael@0: return false; michael@0: } michael@0: michael@0: let win = Services.appShell.hiddenDOMWindow; michael@0: let doc = win.document; michael@0: michael@0: // Create a black span in a hidden window. michael@0: let span = doc.createElement("span"); michael@0: span.style.color = "rgb(0, 0, 0)"; michael@0: michael@0: // Attempt to set the color. If the color is no longer black we know that michael@0: // color is valid. michael@0: span.style.color = color; michael@0: if (span.style.color !== "rgb(0, 0, 0)") { michael@0: return true; michael@0: } michael@0: michael@0: // If the color is black then the above check will have failed. We change michael@0: // the span to white and attempt to reapply the color. If the span is not michael@0: // white then we know that the color is valid otherwise we return invalid. michael@0: span.style.color = "rgb(255, 255, 255)"; michael@0: span.style.color = color; michael@0: return span.style.color !== "rgb(255, 255, 255)"; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Process a CSS string michael@0: * michael@0: * @param {String} value michael@0: * CSS string e.g. "color:red; background-color:green;" michael@0: * @return {String} michael@0: * Converted CSS String e.g. "color:#F00; background-color:#0F0;" michael@0: */ michael@0: function processCSSString(value) { michael@0: if (value && REGEX_JUST_QUOTES.test(value)) { michael@0: return value; michael@0: } michael@0: michael@0: let colorPattern = REGEX_ALL_COLORS; michael@0: michael@0: value = value.replace(colorPattern, function(match) { michael@0: let color = new CssColor(match); michael@0: if (color.valid) { michael@0: return color; michael@0: } michael@0: return match; michael@0: }); michael@0: return value; michael@0: } michael@0: michael@0: /** michael@0: * Convert rgb value to hsl michael@0: * michael@0: * @param {array} rgb michael@0: * Array of rgb values michael@0: * @return {array} michael@0: * Array of hsl values. michael@0: */ michael@0: function rgbToHsl([r,g,b]) { michael@0: r = r / 255; michael@0: g = g / 255; michael@0: b = b / 255; michael@0: michael@0: let max = Math.max(r, g, b); michael@0: let min = Math.min(r, g, b); michael@0: let h; michael@0: let s; michael@0: let l = (max + min) / 2; michael@0: michael@0: if(max == min){ michael@0: h = s = 0; michael@0: } else { michael@0: let d = max - min; michael@0: s = l > 0.5 ? d / (2 - max - min) : d / (max + min); michael@0: michael@0: switch(max) { michael@0: case r: michael@0: h = ((g - b) / d) % 6; michael@0: break; michael@0: case g: michael@0: h = (b - r) / d + 2; michael@0: break; michael@0: case b: michael@0: h = (r - g) / d + 4; michael@0: break; michael@0: } michael@0: h *= 60; michael@0: if (h < 0) { michael@0: h += 360; michael@0: } michael@0: } michael@0: michael@0: return [Math.round(h), Math.round(s * 100), Math.round(l * 100)]; michael@0: } michael@0: michael@0: loader.lazyGetter(this, "DOMUtils", function () { michael@0: return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); michael@0: });