1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/css-color.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,446 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {Cc, Ci, Cu} = require("chrome"); 1.11 +const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); 1.12 + 1.13 +const COLOR_UNIT_PREF = "devtools.defaultColorUnit"; 1.14 + 1.15 +const REGEX_JUST_QUOTES = /^""$/; 1.16 +const REGEX_RGB_3_TUPLE = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i; 1.17 +const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i; 1.18 +const REGEX_HSL_3_TUPLE = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i; 1.19 + 1.20 +/** 1.21 + * This regex matches: 1.22 + * - #F00 1.23 + * - #FF0000 1.24 + * - hsl() 1.25 + * - hsla() 1.26 + * - rgb() 1.27 + * - rgba() 1.28 + * - red 1.29 + * 1.30 + * It also matches css keywords e.g. "background-color" otherwise 1.31 + * "background" would be replaced with #6363CE ("background" is a platform 1.32 + * color). 1.33 + */ 1.34 +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; 1.35 + 1.36 +const SPECIALVALUES = new Set([ 1.37 + "currentcolor", 1.38 + "initial", 1.39 + "inherit", 1.40 + "transparent", 1.41 + "unset" 1.42 +]); 1.43 + 1.44 +/** 1.45 + * This module is used to convert between various color types. 1.46 + * 1.47 + * Usage: 1.48 + * let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 1.49 + * let {colorUtils} = devtools.require("devtools/css-color"); 1.50 + * let color = new colorUtils.CssColor("red"); 1.51 + * 1.52 + * color.authored === "red" 1.53 + * color.hasAlpha === false 1.54 + * color.valid === true 1.55 + * color.transparent === false // transparent has a special status. 1.56 + * color.name === "red" // returns hex or rgba when no name available. 1.57 + * color.hex === "#F00" // returns shortHex when available else returns 1.58 + * longHex. If alpha channel is present then we 1.59 + * return this.rgba. 1.60 + * color.longHex === "#FF0000" // If alpha channel is present then we return 1.61 + * this.rgba. 1.62 + * color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return 1.63 + * this.rgba. 1.64 + * color.rgba === "rgba(255, 0, 0, 1)" 1.65 + * color.hsl === "hsl(0, 100%, 50%)" 1.66 + * color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present 1.67 + * then we return this.rgba. 1.68 + * 1.69 + * color.toString() === "#F00"; // Outputs the color type determined in the 1.70 + * COLOR_UNIT_PREF constant (above). 1.71 + * // Color objects can be reused 1.72 + * color.newColor("green") === "#0F0"; // true 1.73 + * 1.74 + * let processed = colorUtils.processCSSString("color:red; background-color:green;"); 1.75 + * // Returns "color:#F00; background-color:#0F0;" 1.76 + * 1.77 + * Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT. 1.78 + */ 1.79 + 1.80 +function CssColor(colorValue) { 1.81 + this.newColor(colorValue); 1.82 +} 1.83 + 1.84 +module.exports.colorUtils = { 1.85 + CssColor: CssColor, 1.86 + processCSSString: processCSSString, 1.87 + rgbToHsl: rgbToHsl 1.88 +}; 1.89 + 1.90 +/** 1.91 + * Values used in COLOR_UNIT_PREF 1.92 + */ 1.93 +CssColor.COLORUNIT = { 1.94 + "authored": "authored", 1.95 + "hex": "hex", 1.96 + "name": "name", 1.97 + "rgb": "rgb", 1.98 + "hsl": "hsl" 1.99 +}; 1.100 + 1.101 +CssColor.prototype = { 1.102 + authored: null, 1.103 + 1.104 + get hasAlpha() { 1.105 + if (!this.valid) { 1.106 + return false; 1.107 + } 1.108 + return this._getRGBATuple().a !== 1; 1.109 + }, 1.110 + 1.111 + get valid() { 1.112 + return this._validateColor(this.authored); 1.113 + }, 1.114 + 1.115 + /** 1.116 + * Return true for all transparent values e.g. rgba(0, 0, 0, 0). 1.117 + */ 1.118 + get transparent() { 1.119 + try { 1.120 + let tuple = this._getRGBATuple(); 1.121 + return !(tuple.r || tuple.g || tuple.b || tuple.a); 1.122 + } catch(e) { 1.123 + return false; 1.124 + } 1.125 + }, 1.126 + 1.127 + get specialValue() { 1.128 + return SPECIALVALUES.has(this.authored) ? this.authored : null; 1.129 + }, 1.130 + 1.131 + get name() { 1.132 + if (!this.valid) { 1.133 + return ""; 1.134 + } 1.135 + if (this.specialValue) { 1.136 + return this.specialValue; 1.137 + } 1.138 + 1.139 + try { 1.140 + let tuple = this._getRGBATuple(); 1.141 + 1.142 + if (tuple.a !== 1) { 1.143 + return this.rgb; 1.144 + } 1.145 + let {r, g, b} = tuple; 1.146 + return DOMUtils.rgbToColorName(r, g, b); 1.147 + } catch(e) { 1.148 + return this.hex; 1.149 + } 1.150 + }, 1.151 + 1.152 + get hex() { 1.153 + if (!this.valid) { 1.154 + return ""; 1.155 + } 1.156 + if (this.specialValue) { 1.157 + return this.specialValue; 1.158 + } 1.159 + if (this.hasAlpha) { 1.160 + return this.rgba; 1.161 + } 1.162 + 1.163 + let hex = this.longHex; 1.164 + if (hex.charAt(1) == hex.charAt(2) && 1.165 + hex.charAt(3) == hex.charAt(4) && 1.166 + hex.charAt(5) == hex.charAt(6)) { 1.167 + hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5); 1.168 + } 1.169 + return hex; 1.170 + }, 1.171 + 1.172 + get longHex() { 1.173 + if (!this.valid) { 1.174 + return ""; 1.175 + } 1.176 + if (this.specialValue) { 1.177 + return this.specialValue; 1.178 + } 1.179 + if (this.hasAlpha) { 1.180 + return this.rgba; 1.181 + } 1.182 + return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) { 1.183 + return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase(); 1.184 + }); 1.185 + }, 1.186 + 1.187 + get rgb() { 1.188 + if (!this.valid) { 1.189 + return ""; 1.190 + } 1.191 + if (this.specialValue) { 1.192 + return this.specialValue; 1.193 + } 1.194 + if (!this.hasAlpha) { 1.195 + if (this.authored.startsWith("rgb(")) { 1.196 + // The color is valid and begins with rgb(. Return the authored value. 1.197 + return this.authored; 1.198 + } 1.199 + let tuple = this._getRGBATuple(); 1.200 + return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")"; 1.201 + } 1.202 + return this.rgba; 1.203 + }, 1.204 + 1.205 + get rgba() { 1.206 + if (!this.valid) { 1.207 + return ""; 1.208 + } 1.209 + if (this.specialValue) { 1.210 + return this.specialValue; 1.211 + } 1.212 + if (this.authored.startsWith("rgba(")) { 1.213 + // The color is valid and begins with rgba(. Return the authored value. 1.214 + return this.authored; 1.215 + } 1.216 + let components = this._getRGBATuple(); 1.217 + return "rgba(" + components.r + ", " + 1.218 + components.g + ", " + 1.219 + components.b + ", " + 1.220 + components.a + ")"; 1.221 + }, 1.222 + 1.223 + get hsl() { 1.224 + if (!this.valid) { 1.225 + return ""; 1.226 + } 1.227 + if (this.specialValue) { 1.228 + return this.specialValue; 1.229 + } 1.230 + if (this.authored.startsWith("hsl(")) { 1.231 + // The color is valid and begins with hsl(. Return the authored value. 1.232 + return this.authored; 1.233 + } 1.234 + if (this.hasAlpha) { 1.235 + return this.hsla; 1.236 + } 1.237 + return this._hslNoAlpha(); 1.238 + }, 1.239 + 1.240 + get hsla() { 1.241 + if (!this.valid) { 1.242 + return ""; 1.243 + } 1.244 + if (this.specialValue) { 1.245 + return this.specialValue; 1.246 + } 1.247 + if (this.authored.startsWith("hsla(")) { 1.248 + // The color is valid and begins with hsla(. Return the authored value. 1.249 + return this.authored; 1.250 + } 1.251 + if (this.hasAlpha) { 1.252 + let a = this._getRGBATuple().a; 1.253 + return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")"); 1.254 + } 1.255 + return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)"); 1.256 + }, 1.257 + 1.258 + /** 1.259 + * Change color 1.260 + * 1.261 + * @param {String} color 1.262 + * Any valid color string 1.263 + */ 1.264 + newColor: function(color) { 1.265 + this.authored = color.toLowerCase(); 1.266 + return this; 1.267 + }, 1.268 + 1.269 + /** 1.270 + * Return a string representing a color of type defined in COLOR_UNIT_PREF. 1.271 + */ 1.272 + toString: function() { 1.273 + let color; 1.274 + let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF); 1.275 + let unit = CssColor.COLORUNIT[defaultUnit]; 1.276 + 1.277 + switch(unit) { 1.278 + case CssColor.COLORUNIT.authored: 1.279 + color = this.authored; 1.280 + break; 1.281 + case CssColor.COLORUNIT.hex: 1.282 + color = this.hex; 1.283 + break; 1.284 + case CssColor.COLORUNIT.hsl: 1.285 + color = this.hsl; 1.286 + break; 1.287 + case CssColor.COLORUNIT.name: 1.288 + color = this.name; 1.289 + break; 1.290 + case CssColor.COLORUNIT.rgb: 1.291 + color = this.rgb; 1.292 + break; 1.293 + default: 1.294 + color = this.rgb; 1.295 + } 1.296 + return color; 1.297 + }, 1.298 + 1.299 + /** 1.300 + * Returns a RGBA 4-Tuple representation of a color or transparent as 1.301 + * appropriate. 1.302 + */ 1.303 + _getRGBATuple: function() { 1.304 + let win = Services.appShell.hiddenDOMWindow; 1.305 + let doc = win.document; 1.306 + let span = doc.createElement("span"); 1.307 + span.style.color = this.authored; 1.308 + let computed = win.getComputedStyle(span).color; 1.309 + 1.310 + if (computed === "transparent") { 1.311 + return {r: 0, g: 0, b: 0, a: 0}; 1.312 + } 1.313 + 1.314 + let rgba = computed.match(REGEX_RGBA_4_TUPLE); 1.315 + 1.316 + if (rgba) { 1.317 + let [, r, g, b, a] = rgba; 1.318 + return {r: r, g: g, b: b, a: a}; 1.319 + } else { 1.320 + let rgb = computed.match(REGEX_RGB_3_TUPLE); 1.321 + let [, r, g, b] = rgb; 1.322 + 1.323 + return {r: r, g: g, b: b, a: 1}; 1.324 + } 1.325 + }, 1.326 + 1.327 + _hslNoAlpha: function() { 1.328 + let {r, g, b} = this._getRGBATuple(); 1.329 + 1.330 + if (this.authored.startsWith("hsl(")) { 1.331 + // We perform string manipulations on our output so let's ensure that it 1.332 + // is formatted as we expect. 1.333 + let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE); 1.334 + return "hsl(" + h + ", " + s + ", " + l + ")"; 1.335 + } 1.336 + 1.337 + let [h,s,l] = rgbToHsl([r,g,b]); 1.338 + 1.339 + return "hsl(" + h + ", " + s + "%, " + l + "%)"; 1.340 + }, 1.341 + 1.342 + /** 1.343 + * This method allows comparison of CssColor objects using ===. 1.344 + */ 1.345 + valueOf: function() { 1.346 + return this.rgba; 1.347 + }, 1.348 + 1.349 + _validateColor: function(color) { 1.350 + if (typeof color !== "string" || color === "") { 1.351 + return false; 1.352 + } 1.353 + 1.354 + let win = Services.appShell.hiddenDOMWindow; 1.355 + let doc = win.document; 1.356 + 1.357 + // Create a black span in a hidden window. 1.358 + let span = doc.createElement("span"); 1.359 + span.style.color = "rgb(0, 0, 0)"; 1.360 + 1.361 + // Attempt to set the color. If the color is no longer black we know that 1.362 + // color is valid. 1.363 + span.style.color = color; 1.364 + if (span.style.color !== "rgb(0, 0, 0)") { 1.365 + return true; 1.366 + } 1.367 + 1.368 + // If the color is black then the above check will have failed. We change 1.369 + // the span to white and attempt to reapply the color. If the span is not 1.370 + // white then we know that the color is valid otherwise we return invalid. 1.371 + span.style.color = "rgb(255, 255, 255)"; 1.372 + span.style.color = color; 1.373 + return span.style.color !== "rgb(255, 255, 255)"; 1.374 + }, 1.375 +}; 1.376 + 1.377 +/** 1.378 + * Process a CSS string 1.379 + * 1.380 + * @param {String} value 1.381 + * CSS string e.g. "color:red; background-color:green;" 1.382 + * @return {String} 1.383 + * Converted CSS String e.g. "color:#F00; background-color:#0F0;" 1.384 + */ 1.385 +function processCSSString(value) { 1.386 + if (value && REGEX_JUST_QUOTES.test(value)) { 1.387 + return value; 1.388 + } 1.389 + 1.390 + let colorPattern = REGEX_ALL_COLORS; 1.391 + 1.392 + value = value.replace(colorPattern, function(match) { 1.393 + let color = new CssColor(match); 1.394 + if (color.valid) { 1.395 + return color; 1.396 + } 1.397 + return match; 1.398 + }); 1.399 + return value; 1.400 +} 1.401 + 1.402 +/** 1.403 + * Convert rgb value to hsl 1.404 + * 1.405 + * @param {array} rgb 1.406 + * Array of rgb values 1.407 + * @return {array} 1.408 + * Array of hsl values. 1.409 + */ 1.410 +function rgbToHsl([r,g,b]) { 1.411 + r = r / 255; 1.412 + g = g / 255; 1.413 + b = b / 255; 1.414 + 1.415 + let max = Math.max(r, g, b); 1.416 + let min = Math.min(r, g, b); 1.417 + let h; 1.418 + let s; 1.419 + let l = (max + min) / 2; 1.420 + 1.421 + if(max == min){ 1.422 + h = s = 0; 1.423 + } else { 1.424 + let d = max - min; 1.425 + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 1.426 + 1.427 + switch(max) { 1.428 + case r: 1.429 + h = ((g - b) / d) % 6; 1.430 + break; 1.431 + case g: 1.432 + h = (b - r) / d + 2; 1.433 + break; 1.434 + case b: 1.435 + h = (r - g) / d + 4; 1.436 + break; 1.437 + } 1.438 + h *= 60; 1.439 + if (h < 0) { 1.440 + h += 360; 1.441 + } 1.442 + } 1.443 + 1.444 + return [Math.round(h), Math.round(s * 100), Math.round(l * 100)]; 1.445 +} 1.446 + 1.447 +loader.lazyGetter(this, "DOMUtils", function () { 1.448 + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); 1.449 +});