toolkit/devtools/css-color.js

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

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 });

mercurial