Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | const {Cc, Ci, Cu} = require("chrome"); |
michael@0 | 8 | const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); |
michael@0 | 9 | |
michael@0 | 10 | const COLOR_UNIT_PREF = "devtools.defaultColorUnit"; |
michael@0 | 11 | |
michael@0 | 12 | const REGEX_JUST_QUOTES = /^""$/; |
michael@0 | 13 | const REGEX_RGB_3_TUPLE = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i; |
michael@0 | 14 | const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i; |
michael@0 | 15 | const REGEX_HSL_3_TUPLE = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i; |
michael@0 | 16 | |
michael@0 | 17 | /** |
michael@0 | 18 | * This regex matches: |
michael@0 | 19 | * - #F00 |
michael@0 | 20 | * - #FF0000 |
michael@0 | 21 | * - hsl() |
michael@0 | 22 | * - hsla() |
michael@0 | 23 | * - rgb() |
michael@0 | 24 | * - rgba() |
michael@0 | 25 | * - red |
michael@0 | 26 | * |
michael@0 | 27 | * It also matches css keywords e.g. "background-color" otherwise |
michael@0 | 28 | * "background" would be replaced with #6363CE ("background" is a platform |
michael@0 | 29 | * color). |
michael@0 | 30 | */ |
michael@0 | 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; |
michael@0 | 32 | |
michael@0 | 33 | const SPECIALVALUES = new Set([ |
michael@0 | 34 | "currentcolor", |
michael@0 | 35 | "initial", |
michael@0 | 36 | "inherit", |
michael@0 | 37 | "transparent", |
michael@0 | 38 | "unset" |
michael@0 | 39 | ]); |
michael@0 | 40 | |
michael@0 | 41 | /** |
michael@0 | 42 | * This module is used to convert between various color types. |
michael@0 | 43 | * |
michael@0 | 44 | * Usage: |
michael@0 | 45 | * let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
michael@0 | 46 | * let {colorUtils} = devtools.require("devtools/css-color"); |
michael@0 | 47 | * let color = new colorUtils.CssColor("red"); |
michael@0 | 48 | * |
michael@0 | 49 | * color.authored === "red" |
michael@0 | 50 | * color.hasAlpha === false |
michael@0 | 51 | * color.valid === true |
michael@0 | 52 | * color.transparent === false // transparent has a special status. |
michael@0 | 53 | * color.name === "red" // returns hex or rgba when no name available. |
michael@0 | 54 | * color.hex === "#F00" // returns shortHex when available else returns |
michael@0 | 55 | * longHex. If alpha channel is present then we |
michael@0 | 56 | * return this.rgba. |
michael@0 | 57 | * color.longHex === "#FF0000" // If alpha channel is present then we return |
michael@0 | 58 | * this.rgba. |
michael@0 | 59 | * color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return |
michael@0 | 60 | * this.rgba. |
michael@0 | 61 | * color.rgba === "rgba(255, 0, 0, 1)" |
michael@0 | 62 | * color.hsl === "hsl(0, 100%, 50%)" |
michael@0 | 63 | * color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present |
michael@0 | 64 | * then we return this.rgba. |
michael@0 | 65 | * |
michael@0 | 66 | * color.toString() === "#F00"; // Outputs the color type determined in the |
michael@0 | 67 | * COLOR_UNIT_PREF constant (above). |
michael@0 | 68 | * // Color objects can be reused |
michael@0 | 69 | * color.newColor("green") === "#0F0"; // true |
michael@0 | 70 | * |
michael@0 | 71 | * let processed = colorUtils.processCSSString("color:red; background-color:green;"); |
michael@0 | 72 | * // Returns "color:#F00; background-color:#0F0;" |
michael@0 | 73 | * |
michael@0 | 74 | * Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT. |
michael@0 | 75 | */ |
michael@0 | 76 | |
michael@0 | 77 | function CssColor(colorValue) { |
michael@0 | 78 | this.newColor(colorValue); |
michael@0 | 79 | } |
michael@0 | 80 | |
michael@0 | 81 | module.exports.colorUtils = { |
michael@0 | 82 | CssColor: CssColor, |
michael@0 | 83 | processCSSString: processCSSString, |
michael@0 | 84 | rgbToHsl: rgbToHsl |
michael@0 | 85 | }; |
michael@0 | 86 | |
michael@0 | 87 | /** |
michael@0 | 88 | * Values used in COLOR_UNIT_PREF |
michael@0 | 89 | */ |
michael@0 | 90 | CssColor.COLORUNIT = { |
michael@0 | 91 | "authored": "authored", |
michael@0 | 92 | "hex": "hex", |
michael@0 | 93 | "name": "name", |
michael@0 | 94 | "rgb": "rgb", |
michael@0 | 95 | "hsl": "hsl" |
michael@0 | 96 | }; |
michael@0 | 97 | |
michael@0 | 98 | CssColor.prototype = { |
michael@0 | 99 | authored: null, |
michael@0 | 100 | |
michael@0 | 101 | get hasAlpha() { |
michael@0 | 102 | if (!this.valid) { |
michael@0 | 103 | return false; |
michael@0 | 104 | } |
michael@0 | 105 | return this._getRGBATuple().a !== 1; |
michael@0 | 106 | }, |
michael@0 | 107 | |
michael@0 | 108 | get valid() { |
michael@0 | 109 | return this._validateColor(this.authored); |
michael@0 | 110 | }, |
michael@0 | 111 | |
michael@0 | 112 | /** |
michael@0 | 113 | * Return true for all transparent values e.g. rgba(0, 0, 0, 0). |
michael@0 | 114 | */ |
michael@0 | 115 | get transparent() { |
michael@0 | 116 | try { |
michael@0 | 117 | let tuple = this._getRGBATuple(); |
michael@0 | 118 | return !(tuple.r || tuple.g || tuple.b || tuple.a); |
michael@0 | 119 | } catch(e) { |
michael@0 | 120 | return false; |
michael@0 | 121 | } |
michael@0 | 122 | }, |
michael@0 | 123 | |
michael@0 | 124 | get specialValue() { |
michael@0 | 125 | return SPECIALVALUES.has(this.authored) ? this.authored : null; |
michael@0 | 126 | }, |
michael@0 | 127 | |
michael@0 | 128 | get name() { |
michael@0 | 129 | if (!this.valid) { |
michael@0 | 130 | return ""; |
michael@0 | 131 | } |
michael@0 | 132 | if (this.specialValue) { |
michael@0 | 133 | return this.specialValue; |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | try { |
michael@0 | 137 | let tuple = this._getRGBATuple(); |
michael@0 | 138 | |
michael@0 | 139 | if (tuple.a !== 1) { |
michael@0 | 140 | return this.rgb; |
michael@0 | 141 | } |
michael@0 | 142 | let {r, g, b} = tuple; |
michael@0 | 143 | return DOMUtils.rgbToColorName(r, g, b); |
michael@0 | 144 | } catch(e) { |
michael@0 | 145 | return this.hex; |
michael@0 | 146 | } |
michael@0 | 147 | }, |
michael@0 | 148 | |
michael@0 | 149 | get hex() { |
michael@0 | 150 | if (!this.valid) { |
michael@0 | 151 | return ""; |
michael@0 | 152 | } |
michael@0 | 153 | if (this.specialValue) { |
michael@0 | 154 | return this.specialValue; |
michael@0 | 155 | } |
michael@0 | 156 | if (this.hasAlpha) { |
michael@0 | 157 | return this.rgba; |
michael@0 | 158 | } |
michael@0 | 159 | |
michael@0 | 160 | let hex = this.longHex; |
michael@0 | 161 | if (hex.charAt(1) == hex.charAt(2) && |
michael@0 | 162 | hex.charAt(3) == hex.charAt(4) && |
michael@0 | 163 | hex.charAt(5) == hex.charAt(6)) { |
michael@0 | 164 | hex = "#" + hex.charAt(1) + hex.charAt(3) + hex.charAt(5); |
michael@0 | 165 | } |
michael@0 | 166 | return hex; |
michael@0 | 167 | }, |
michael@0 | 168 | |
michael@0 | 169 | get longHex() { |
michael@0 | 170 | if (!this.valid) { |
michael@0 | 171 | return ""; |
michael@0 | 172 | } |
michael@0 | 173 | if (this.specialValue) { |
michael@0 | 174 | return this.specialValue; |
michael@0 | 175 | } |
michael@0 | 176 | if (this.hasAlpha) { |
michael@0 | 177 | return this.rgba; |
michael@0 | 178 | } |
michael@0 | 179 | return this.rgb.replace(/\brgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)/gi, function(_, r, g, b) { |
michael@0 | 180 | return "#" + ((1 << 24) + (r << 16) + (g << 8) + (b << 0)).toString(16).substr(-6).toUpperCase(); |
michael@0 | 181 | }); |
michael@0 | 182 | }, |
michael@0 | 183 | |
michael@0 | 184 | get rgb() { |
michael@0 | 185 | if (!this.valid) { |
michael@0 | 186 | return ""; |
michael@0 | 187 | } |
michael@0 | 188 | if (this.specialValue) { |
michael@0 | 189 | return this.specialValue; |
michael@0 | 190 | } |
michael@0 | 191 | if (!this.hasAlpha) { |
michael@0 | 192 | if (this.authored.startsWith("rgb(")) { |
michael@0 | 193 | // The color is valid and begins with rgb(. Return the authored value. |
michael@0 | 194 | return this.authored; |
michael@0 | 195 | } |
michael@0 | 196 | let tuple = this._getRGBATuple(); |
michael@0 | 197 | return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")"; |
michael@0 | 198 | } |
michael@0 | 199 | return this.rgba; |
michael@0 | 200 | }, |
michael@0 | 201 | |
michael@0 | 202 | get rgba() { |
michael@0 | 203 | if (!this.valid) { |
michael@0 | 204 | return ""; |
michael@0 | 205 | } |
michael@0 | 206 | if (this.specialValue) { |
michael@0 | 207 | return this.specialValue; |
michael@0 | 208 | } |
michael@0 | 209 | if (this.authored.startsWith("rgba(")) { |
michael@0 | 210 | // The color is valid and begins with rgba(. Return the authored value. |
michael@0 | 211 | return this.authored; |
michael@0 | 212 | } |
michael@0 | 213 | let components = this._getRGBATuple(); |
michael@0 | 214 | return "rgba(" + components.r + ", " + |
michael@0 | 215 | components.g + ", " + |
michael@0 | 216 | components.b + ", " + |
michael@0 | 217 | components.a + ")"; |
michael@0 | 218 | }, |
michael@0 | 219 | |
michael@0 | 220 | get hsl() { |
michael@0 | 221 | if (!this.valid) { |
michael@0 | 222 | return ""; |
michael@0 | 223 | } |
michael@0 | 224 | if (this.specialValue) { |
michael@0 | 225 | return this.specialValue; |
michael@0 | 226 | } |
michael@0 | 227 | if (this.authored.startsWith("hsl(")) { |
michael@0 | 228 | // The color is valid and begins with hsl(. Return the authored value. |
michael@0 | 229 | return this.authored; |
michael@0 | 230 | } |
michael@0 | 231 | if (this.hasAlpha) { |
michael@0 | 232 | return this.hsla; |
michael@0 | 233 | } |
michael@0 | 234 | return this._hslNoAlpha(); |
michael@0 | 235 | }, |
michael@0 | 236 | |
michael@0 | 237 | get hsla() { |
michael@0 | 238 | if (!this.valid) { |
michael@0 | 239 | return ""; |
michael@0 | 240 | } |
michael@0 | 241 | if (this.specialValue) { |
michael@0 | 242 | return this.specialValue; |
michael@0 | 243 | } |
michael@0 | 244 | if (this.authored.startsWith("hsla(")) { |
michael@0 | 245 | // The color is valid and begins with hsla(. Return the authored value. |
michael@0 | 246 | return this.authored; |
michael@0 | 247 | } |
michael@0 | 248 | if (this.hasAlpha) { |
michael@0 | 249 | let a = this._getRGBATuple().a; |
michael@0 | 250 | return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", " + a + ")"); |
michael@0 | 251 | } |
michael@0 | 252 | return this._hslNoAlpha().replace("hsl", "hsla").replace(")", ", 1)"); |
michael@0 | 253 | }, |
michael@0 | 254 | |
michael@0 | 255 | /** |
michael@0 | 256 | * Change color |
michael@0 | 257 | * |
michael@0 | 258 | * @param {String} color |
michael@0 | 259 | * Any valid color string |
michael@0 | 260 | */ |
michael@0 | 261 | newColor: function(color) { |
michael@0 | 262 | this.authored = color.toLowerCase(); |
michael@0 | 263 | return this; |
michael@0 | 264 | }, |
michael@0 | 265 | |
michael@0 | 266 | /** |
michael@0 | 267 | * Return a string representing a color of type defined in COLOR_UNIT_PREF. |
michael@0 | 268 | */ |
michael@0 | 269 | toString: function() { |
michael@0 | 270 | let color; |
michael@0 | 271 | let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF); |
michael@0 | 272 | let unit = CssColor.COLORUNIT[defaultUnit]; |
michael@0 | 273 | |
michael@0 | 274 | switch(unit) { |
michael@0 | 275 | case CssColor.COLORUNIT.authored: |
michael@0 | 276 | color = this.authored; |
michael@0 | 277 | break; |
michael@0 | 278 | case CssColor.COLORUNIT.hex: |
michael@0 | 279 | color = this.hex; |
michael@0 | 280 | break; |
michael@0 | 281 | case CssColor.COLORUNIT.hsl: |
michael@0 | 282 | color = this.hsl; |
michael@0 | 283 | break; |
michael@0 | 284 | case CssColor.COLORUNIT.name: |
michael@0 | 285 | color = this.name; |
michael@0 | 286 | break; |
michael@0 | 287 | case CssColor.COLORUNIT.rgb: |
michael@0 | 288 | color = this.rgb; |
michael@0 | 289 | break; |
michael@0 | 290 | default: |
michael@0 | 291 | color = this.rgb; |
michael@0 | 292 | } |
michael@0 | 293 | return color; |
michael@0 | 294 | }, |
michael@0 | 295 | |
michael@0 | 296 | /** |
michael@0 | 297 | * Returns a RGBA 4-Tuple representation of a color or transparent as |
michael@0 | 298 | * appropriate. |
michael@0 | 299 | */ |
michael@0 | 300 | _getRGBATuple: function() { |
michael@0 | 301 | let win = Services.appShell.hiddenDOMWindow; |
michael@0 | 302 | let doc = win.document; |
michael@0 | 303 | let span = doc.createElement("span"); |
michael@0 | 304 | span.style.color = this.authored; |
michael@0 | 305 | let computed = win.getComputedStyle(span).color; |
michael@0 | 306 | |
michael@0 | 307 | if (computed === "transparent") { |
michael@0 | 308 | return {r: 0, g: 0, b: 0, a: 0}; |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | let rgba = computed.match(REGEX_RGBA_4_TUPLE); |
michael@0 | 312 | |
michael@0 | 313 | if (rgba) { |
michael@0 | 314 | let [, r, g, b, a] = rgba; |
michael@0 | 315 | return {r: r, g: g, b: b, a: a}; |
michael@0 | 316 | } else { |
michael@0 | 317 | let rgb = computed.match(REGEX_RGB_3_TUPLE); |
michael@0 | 318 | let [, r, g, b] = rgb; |
michael@0 | 319 | |
michael@0 | 320 | return {r: r, g: g, b: b, a: 1}; |
michael@0 | 321 | } |
michael@0 | 322 | }, |
michael@0 | 323 | |
michael@0 | 324 | _hslNoAlpha: function() { |
michael@0 | 325 | let {r, g, b} = this._getRGBATuple(); |
michael@0 | 326 | |
michael@0 | 327 | if (this.authored.startsWith("hsl(")) { |
michael@0 | 328 | // We perform string manipulations on our output so let's ensure that it |
michael@0 | 329 | // is formatted as we expect. |
michael@0 | 330 | let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE); |
michael@0 | 331 | return "hsl(" + h + ", " + s + ", " + l + ")"; |
michael@0 | 332 | } |
michael@0 | 333 | |
michael@0 | 334 | let [h,s,l] = rgbToHsl([r,g,b]); |
michael@0 | 335 | |
michael@0 | 336 | return "hsl(" + h + ", " + s + "%, " + l + "%)"; |
michael@0 | 337 | }, |
michael@0 | 338 | |
michael@0 | 339 | /** |
michael@0 | 340 | * This method allows comparison of CssColor objects using ===. |
michael@0 | 341 | */ |
michael@0 | 342 | valueOf: function() { |
michael@0 | 343 | return this.rgba; |
michael@0 | 344 | }, |
michael@0 | 345 | |
michael@0 | 346 | _validateColor: function(color) { |
michael@0 | 347 | if (typeof color !== "string" || color === "") { |
michael@0 | 348 | return false; |
michael@0 | 349 | } |
michael@0 | 350 | |
michael@0 | 351 | let win = Services.appShell.hiddenDOMWindow; |
michael@0 | 352 | let doc = win.document; |
michael@0 | 353 | |
michael@0 | 354 | // Create a black span in a hidden window. |
michael@0 | 355 | let span = doc.createElement("span"); |
michael@0 | 356 | span.style.color = "rgb(0, 0, 0)"; |
michael@0 | 357 | |
michael@0 | 358 | // Attempt to set the color. If the color is no longer black we know that |
michael@0 | 359 | // color is valid. |
michael@0 | 360 | span.style.color = color; |
michael@0 | 361 | if (span.style.color !== "rgb(0, 0, 0)") { |
michael@0 | 362 | return true; |
michael@0 | 363 | } |
michael@0 | 364 | |
michael@0 | 365 | // If the color is black then the above check will have failed. We change |
michael@0 | 366 | // the span to white and attempt to reapply the color. If the span is not |
michael@0 | 367 | // white then we know that the color is valid otherwise we return invalid. |
michael@0 | 368 | span.style.color = "rgb(255, 255, 255)"; |
michael@0 | 369 | span.style.color = color; |
michael@0 | 370 | return span.style.color !== "rgb(255, 255, 255)"; |
michael@0 | 371 | }, |
michael@0 | 372 | }; |
michael@0 | 373 | |
michael@0 | 374 | /** |
michael@0 | 375 | * Process a CSS string |
michael@0 | 376 | * |
michael@0 | 377 | * @param {String} value |
michael@0 | 378 | * CSS string e.g. "color:red; background-color:green;" |
michael@0 | 379 | * @return {String} |
michael@0 | 380 | * Converted CSS String e.g. "color:#F00; background-color:#0F0;" |
michael@0 | 381 | */ |
michael@0 | 382 | function processCSSString(value) { |
michael@0 | 383 | if (value && REGEX_JUST_QUOTES.test(value)) { |
michael@0 | 384 | return value; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | let colorPattern = REGEX_ALL_COLORS; |
michael@0 | 388 | |
michael@0 | 389 | value = value.replace(colorPattern, function(match) { |
michael@0 | 390 | let color = new CssColor(match); |
michael@0 | 391 | if (color.valid) { |
michael@0 | 392 | return color; |
michael@0 | 393 | } |
michael@0 | 394 | return match; |
michael@0 | 395 | }); |
michael@0 | 396 | return value; |
michael@0 | 397 | } |
michael@0 | 398 | |
michael@0 | 399 | /** |
michael@0 | 400 | * Convert rgb value to hsl |
michael@0 | 401 | * |
michael@0 | 402 | * @param {array} rgb |
michael@0 | 403 | * Array of rgb values |
michael@0 | 404 | * @return {array} |
michael@0 | 405 | * Array of hsl values. |
michael@0 | 406 | */ |
michael@0 | 407 | function rgbToHsl([r,g,b]) { |
michael@0 | 408 | r = r / 255; |
michael@0 | 409 | g = g / 255; |
michael@0 | 410 | b = b / 255; |
michael@0 | 411 | |
michael@0 | 412 | let max = Math.max(r, g, b); |
michael@0 | 413 | let min = Math.min(r, g, b); |
michael@0 | 414 | let h; |
michael@0 | 415 | let s; |
michael@0 | 416 | let l = (max + min) / 2; |
michael@0 | 417 | |
michael@0 | 418 | if(max == min){ |
michael@0 | 419 | h = s = 0; |
michael@0 | 420 | } else { |
michael@0 | 421 | let d = max - min; |
michael@0 | 422 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); |
michael@0 | 423 | |
michael@0 | 424 | switch(max) { |
michael@0 | 425 | case r: |
michael@0 | 426 | h = ((g - b) / d) % 6; |
michael@0 | 427 | break; |
michael@0 | 428 | case g: |
michael@0 | 429 | h = (b - r) / d + 2; |
michael@0 | 430 | break; |
michael@0 | 431 | case b: |
michael@0 | 432 | h = (r - g) / d + 4; |
michael@0 | 433 | break; |
michael@0 | 434 | } |
michael@0 | 435 | h *= 60; |
michael@0 | 436 | if (h < 0) { |
michael@0 | 437 | h += 360; |
michael@0 | 438 | } |
michael@0 | 439 | } |
michael@0 | 440 | |
michael@0 | 441 | return [Math.round(h), Math.round(s * 100), Math.round(l * 100)]; |
michael@0 | 442 | } |
michael@0 | 443 | |
michael@0 | 444 | loader.lazyGetter(this, "DOMUtils", function () { |
michael@0 | 445 | return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); |
michael@0 | 446 | }); |