From 537372dffa535c72a4b282cc974c46808e24393e Mon Sep 17 00:00:00 2001 From: tophf Date: Fri, 1 Apr 2022 16:38:52 +0300 Subject: [PATCH] colorpicker: add hwb colors --- js/color/color-converter.js | 232 ++++++++++++++++++++++-------------- js/color/color-picker.js | 185 +++++++++++----------------- js/color/color-view.js | 61 +++------- js/usercss-compiler.js | 2 +- 4 files changed, 225 insertions(+), 255 deletions(-) diff --git a/js/color/color-converter.js b/js/color/color-converter.js index 38dae511..73013ab9 100644 --- a/js/color/color-converter.js +++ b/js/color/color-converter.js @@ -2,29 +2,82 @@ const colorConverter = (() => { + const RXS_NUM = /\s*([+-]?(?:\d+\.?\d*|\d*\.\d+))(?:e[+-]?\d+)?/.source; + const RXS_NUM_ANGLE = `${RXS_NUM}(deg|g?rad|turn)?`; + const RX_COLOR = { + hex: /#([a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)\b/iy, + + hsl: new RegExp([ + // num_or_angle, pct, pct [ , num_or_pct]? + `^(${RXS_NUM_ANGLE})\\s*,(${RXS_NUM}%\\s*(,|$)){2}(${RXS_NUM}%?)?\\s*$`, + // num_or_angle pct pct [ / num_or_pct]? + `^(${RXS_NUM_ANGLE})\\s+(${RXS_NUM}%\\s*(\\s|$)){2}(/${RXS_NUM}%?)?\\s*$`, + ].join('|'), 'iy'), + + hwb: new RegExp( + // num|angle|none pct|none pct|none [ / num|pct|none ]? + `^(${RXS_NUM_ANGLE}|none)(\\s+(${RXS_NUM}%|none)){2}(\\s+|$)(/${RXS_NUM}%?|none)?\\s*$`, + 'iy'), + + rgb: new RegExp([ + // num, num, num [ , num_or_pct]? + // pct, pct, pct [ , num_or_pct]? + `^((${RXS_NUM}\\s*(,|$)){3}|(${RXS_NUM}%\\s*(,|$)){3})(${RXS_NUM}%?)?\\s*$`, + // num num num [ / num_or_pct]? + // pct pct pct [ / num_or_pct]? + `^((${RXS_NUM}\\s*(\\s|$)){3}|(${RXS_NUM}%\\s*(\\s|$)){3})(/${RXS_NUM}%?)?\\s*$`, + ].join('|'), 'iy'), + }; + const ANGLE_TO_DEG = { + grad: 360 / 400, + rad: 180 / Math.PI, + turn: 360, + }; + const TO_HSV = { + hex: RGBtoHSV, + hsl: HSLtoHSV, + hwb: HWBtoHSV, + rgb: RGBtoHSV, + }; + const FROM_HSV = { + hex: HSVtoRGB, + hsl: HSVtoHSL, + hwb: HSVtoHWB, + rgb: HSVtoRGB, + }; + const guessType = c => + 'r' in c ? 'rgb' : + 'w' in c ? 'hwb' : + 'v' in c ? 'hsv' : + 'l' in c ? 'hsl' : + undefined; + return { parse, format, formatAlpha, - RGBtoHSV, - HSVtoRGB, - HSLtoHSV, - HSVtoHSL, + fromHSV: (color, type) => FROM_HSV[type](color), + toHSV: color => TO_HSV[color.type || 'rgb'](color), + constrain, constrainHue, + guessType, snapToInt, + testAt, ALPHA_DIGITS: 3, + RX_COLOR, // NAMED_COLORS is added below }; - function format(color = '', type = color.type, hexUppercase, usoMode) { + function format(color = '', type = color.type, {hexUppercase, usoMode, round} = {}) { if (!color || !type) return typeof color === 'string' ? color : ''; - const {a} = color; - let aStr = formatAlpha(a); - if (aStr) aStr = ', ' + aStr; - if (type !== 'hsl' && color.type === 'hsl') { - color = HSVtoRGB(HSLtoHSV(color)); - } - const {r, g, b, h, s, l} = color; + const {a, type: src = guessType(color)} = color; + const aFmt = formatAlpha(a); + const aStr = aFmt ? ', ' + aFmt : ''; + const srcConv = src === 'hex' ? 'rgb' : src; + const dstConv = type === 'hex' ? 'rgb' : type; + color = srcConv === dstConv ? color : FROM_HSV[dstConv](TO_HSV[srcConv](color)); + round = round ? Math.round : v => v; + const {r, g, b, h, s, l, w} = color; switch (type) { case 'hex': { let res = '#' + hex2(r) + hex2(g) + hex2(b) + (aStr ? hex2(Math.round(a * 255)) : ''); @@ -36,51 +89,15 @@ const colorConverter = (() => { return usoMode ? rgb : `rgb${aStr ? 'a' : ''}(${rgb}${aStr})`; } case 'hsl': - return `hsl${aStr ? 'a' : ''}(${h}, ${s}%, ${l}%${aStr})`; + return `hsl${aStr ? 'a' : ''}(${round(h)}, ${round(s)}%, ${round(l)}%${aStr})`; + case 'hwb': + return `hwb(${round(h)} ${round(w)}% ${round(b)}%${aFmt ? ' / ' + aFmt : ''})`; } } - // Copied from _hexcolor() in parserlib.js - function validateHex(color) { - return /^#[a-f\d]+$/i.test(color) && [4, 5, 7, 9].some(n => color.length === n); - } - - function validateRGB(nums) { - const isPercentage = nums[0].endsWith('%'); - const valid = isPercentage ? validatePercentage : validateNum; - return nums.slice(0, 3).every(valid); - } - - function validatePercentage(s) { - if (!s.endsWith('%')) return false; - const n = Number(s.slice(0, -1)); - return n >= 0 && n <= 100; - } - - function validateNum(s) { - const n = Number(s); - return n >= 0 && n <= 255; - } - - function validateHSL(nums) { - return validateAngle(nums[0]) && nums.slice(1, 3).every(validatePercentage); - } - - function validateAngle(s) { - return /^-?(\d+|\d*\.\d+)(deg|grad|rad|turn)?$/i.test(s); - } - - function validateAlpha(alpha) { - if (alpha.endsWith('%')) { - return validatePercentage(alpha); - } - const n = Number(alpha); - return n >= 0 && n <= 1; - } - function parse(str) { if (typeof str !== 'string') return; - str = str.trim(); + str = str.trim().toLowerCase(); if (!str) return; if (str[0] !== '#' && !str.includes('(')) { @@ -90,47 +107,45 @@ const colorConverter = (() => { } if (str[0] === '#') { - if (!validateHex(str)) { - return null; + if (!testAt(RX_COLOR.hex, 0, str)) { + return; } str = str.slice(1); const [r, g, b, a = 255] = str.length <= 4 ? str.match(/(.)/g).map(c => parseInt(c + c, 16)) : str.match(/(..)/g).map(c => parseInt(c, 16)); - return {type: 'hex', r, g, b, a: a === 255 ? undefined : a / 255}; + return { + type: 'hex', + r, + g, + b, + a: a === 255 ? undefined : a / 255, + }; } - const [, type, value] = str.match(/^(rgb|hsl)a?\((.*?)\)|$/i); - if (!type) return; - - const comma = value.includes(',') && !value.includes('/'); - const num = value.trim().split(comma ? /\s*,\s*/ : /\s+(?!\/)|\s*\/\s*/); - if (num.length < 3 || num.length > 4) return; - if (num[3] && !validateAlpha(num[3])) return null; - - let a = !num[3] ? 1 : parseFloat(num[3]) / (num[3].endsWith('%') ? 100 : 1); - if (isNaN(a)) a = 1; - - const first = num[0]; - if (/rgb/i.test(type)) { - if (!validateRGB(num)) { - return null; - } - const k = first.endsWith('%') ? 2.55 : 1; - const [r, g, b] = num.map(s => Math.round(parseFloat(s) * k)); - return {type: 'rgb', r, g, b, a}; - } else { - if (!validateHSL(num)) { - return null; - } - let h = parseFloat(first); - if (first.endsWith('grad')) h *= 360 / 400; - else if (first.endsWith('rad')) h *= 180 / Math.PI; - else if (first.endsWith('turn')) h *= 360; - const s = parseFloat(num[1]); - const l = parseFloat(num[2]); - return {type: 'hsl', h, s, l, a}; + const [, func, type = func, value] = str.match(/^((rgb|hsl)a?|hwb)\(\s*(.*?)\s*\)|$/); + if (!func || !testAt(RX_COLOR[type], 0, value)) { + return; } + const [s1, s2, s3, sA] = value.split(/\s*[,/]\s*|\s+/); + const a = isNaN(sA) ? 1 : constrain(0, 1, sA / (sA.endsWith('%') ? 100 : 1)); + + if (type === 'rgb') { + const k = s1.endsWith('%') ? 2.55 : 1; + return { + type, + r: constrain(0, 255, Math.round(s1 * k)), + g: constrain(0, 255, Math.round(s2 * k)), + b: constrain(0, 255, Math.round(s3 * k)), + a, + }; + } + const h = constrainHue(parseFloat(s1) * (ANGLE_TO_DEG[s1.match(/\D*$/)[0]] || 1)); + const n2 = constrain(0, 100, parseFloat(s2) || 0); + const n3 = constrain(0, 100, parseFloat(s3) || 0); + return type === 'hwb' + ? {type, h, w: n2, b: n3, a} + : {type, h, s: n2, l: n3, a}; } function formatAlpha(a) { @@ -164,8 +179,8 @@ const colorConverter = (() => { }; } - function HSVtoRGB({h, s, v}) { - h = constrainHue(h) % 360; + function HSVtoRGB({h, s, v, a}) { + h = constrainHue(h); const C = s * v; const X = C * (1 - Math.abs((h / 60) % 2 - 1)); const m = v - C; @@ -180,9 +195,11 @@ const colorConverter = (() => { r: snapToInt(Math.round((r + m) * 255)), g: snapToInt(Math.round((g + m) * 255)), b: snapToInt(Math.round((b + m) * 255)), + a, }; } + function HSLtoHSV({h, s, l, a}) { const t = s * (l < 50 ? l : 100 - l) / 100; return { @@ -193,16 +210,41 @@ const colorConverter = (() => { }; } - function HSVtoHSL({h, s, v}) { + function HSVtoHSL({h, s, v, a}) { const l = (2 - s) * v / 2; const t = l < .5 ? l * 2 : 2 - l * 2; return { - h: Math.round(constrainHue(h)), - s: Math.round(t ? s * v / t * 100 : 0), - l: Math.round(l * 100), + h: constrainHue(h), + s: t ? s * v / t * 100 : 0, + l: l * 100, + a, }; } + function HWBtoHSV({h, w, b, a}) { + w = constrain(0, 100, w) / 100; + b = constrain(0, 100, b) / 100; + return { + h: constrainHue(h), + s: b === 1 ? 0 : 1 - w / (1 - b), + v: 1 - b, + a, + }; + } + + function HSVtoHWB({h, s, v, a}) { + return { + h: constrainHue(h), + w: (1 - s) * v * 100, + b: (1 - v) * 100, + a, + }; + } + + function constrain(min, max, value) { + return value < min ? min : value > max ? max : value; + } + function constrainHue(h) { return h < 0 ? h % 360 + 360 : h > 360 ? h % 360 : @@ -215,7 +257,13 @@ const colorConverter = (() => { } function hex2(val) { - return (val < 16 ? '0' : '') + (val >> 0).toString(16); + return (val < 16 ? '0' : '') + Math.round(val).toString(16); + } + + function testAt(rx, index, text) { + if (!rx) return false; + rx.lastIndex = index; + return rx.test(text); } })(); diff --git a/js/color/color-picker.js b/js/color/color-picker.js index 53a8d2c6..4a39be61 100644 --- a/js/color/color-picker.js +++ b/js/color/color-picker.js @@ -3,6 +3,7 @@ 'use strict'; (window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () { + const {constrain} = colorConverter; const cm = window.CodeMirror && this; const CSS_PREFIX = 'colorpicker-'; const HUE_COLORS = [ @@ -40,8 +41,6 @@ let /** @type {HTMLElement} */ $palette; const $inputGroups = {}; const $inputs = {}; - const $rgb = {}; - const $hsl = {}; const $hexLettercase = {}; const allowInputFocus = !('ontouchstart' in document) || window.innerHeight > 800; @@ -85,6 +84,21 @@ return Object.assign(el, props); } const alphaPattern = /^\s*(0+\.?|0*\.\d+|0*1\.?|0*1\.0*)?\s*$/.source; + const nestedObj = (obj, key) => (obj[key] || (obj[key] = {})); + const makeNum = (type, channel, props, min, max) => + $(['input-field', `${type}-${channel}`], [ + (nestedObj($inputs, type)[channel] = + $('input', props || {tag: 'input', type: 'number', min, max, step: 1})), + $('title', channel.toUpperCase()), + ]); + const ColorGroup = (type, channels) => ( + $inputGroups[type] = $(['input-group', type], [ + ...Object.entries(channels).map(([k, v]) => + makeNum(type, k, null, v[0], v[1])), + makeNum(type, 'a', + {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}), + ]) + ); $root = $('popup', { oninput: setFromInputs, onkeydown: setFromKeyboard, @@ -128,42 +142,9 @@ ]), ]), ]), - $inputGroups.rgb = $(['input-group', 'rgb'], [ - $(['input-field', 'rgb-r'], [ - $rgb.r = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), - $('title', 'R'), - ]), - $(['input-field', 'rgb-g'], [ - $rgb.g = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), - $('title', 'G'), - ]), - $(['input-field', 'rgb-b'], [ - $rgb.b = $('input', {tag: 'input', type: 'number', min: 0, max: 255, step: 1}), - $('title', 'B'), - ]), - $(['input-field', 'rgb-a'], [ - $rgb.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}), - $('title', 'A'), - ]), - ]), - $inputGroups.hsl = $(['input-group', 'hsl'], [ - $(['input-field', 'hsl-h'], [ - $hsl.h = $('input', {tag: 'input', type: 'number', step: 1}), - $('title', 'H'), - ]), - $(['input-field', 'hsl-s'], [ - $hsl.s = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}), - $('title', 'S'), - ]), - $(['input-field', 'hsl-l'], [ - $hsl.l = $('input', {tag: 'input', type: 'number', min: 0, max: 100, step: 1}), - $('title', 'L'), - ]), - $(['input-field', 'hsl-a'], [ - $hsl.a = $('input', {tag: 'input', type: 'text', pattern: alphaPattern, spellcheck: false}), - $('title', 'A'), - ]), - ]), + ColorGroup('rgb', {r: [0, 255], g: [0, 255], b: [0, 255]}), + ColorGroup('hsl', {h: [], s: [0, 100], l: [0, 100]}), + ColorGroup('hwb', {h: [], w: [0, 100], b: [0, 100]}), $('format-change', [ $formatChangeButton = $('format-change-button', {onclick: setFromFormatElement}, '↔'), ]), @@ -180,19 +161,26 @@ }), ]); - $inputs.hex = [$hexCode]; - $inputs.rgb = [$rgb.r, $rgb.g, $rgb.b, $rgb.a]; - $inputs.hsl = [$hsl.h, $hsl.s, $hsl.l, $hsl.a]; - const inputsToArray = inputs => inputs.map(el => parseFloat(el.value)); - const inputsToHexString = () => $hexCode.value.trim(); - const inputsToRGB = ([r, g, b, a] = inputsToArray($inputs.rgb)) => ({r, g, b, a, type: 'rgb'}); - const inputsToHSL = ([h, s, l, a] = inputsToArray($inputs.hsl)) => ({h, s, l, a, type: 'hsl'}); - Object.defineProperty($inputs.hex, 'color', {get: inputsToHexString}); - Object.defineProperty($inputs.rgb, 'color', {get: inputsToRGB}); - Object.defineProperty($inputs.hsl, 'color', {get: inputsToHSL}); - Object.defineProperty($inputs, 'color', {get: () => $inputs[currentFormat].color}); + const inputsToObj = type => { + const res = {type}; + for (const [k, el] of Object.entries($inputs[type])) { + res[k] = parseFloat(el.value); + } + return res; + }; + for (const [key, val] of Object.entries($inputs)) { + Object.defineProperty(val, 'color', { + get: inputsToObj.bind(null, key), + }); + } + Object.defineProperty($inputs.hex = [$hexCode], 'color', { + get: () => $hexCode.value.trim(), + }); + Object.defineProperty($inputs, 'color', { + get: () => $inputs[currentFormat].color, + }); Object.defineProperty($inputs, 'colorString', { - get: () => currentFormat && colorConverter.format($inputs[currentFormat].color), + get: () => currentFormat && colorConverter.format($inputs[currentFormat].color, undefined, {round: true}), }); HUE_COLORS.forEach(color => Object.assign(color, colorConverter.parse(color.hex))); @@ -210,6 +198,7 @@ HSV = {}; currentFormat = ''; options = PUBLIC_API.options = opt; + if (opt.round !== false) opt.round = true; prevFocusedElement = document.activeElement; userActivity = 0; lastOutputColor = opt.color || ''; @@ -246,33 +235,19 @@ } function setColor(color) { - switch (typeof color) { - case 'string': - color = colorConverter.parse(color); - break; - case 'object': { - const {r, g, b, a} = color; - if (!isNaN(r) && !isNaN(g) && !isNaN(b)) { - color = {r, g, b, a, type: 'rgb'}; - break; - } - const {h, s, l} = color; - if (!isNaN(h) && !isNaN(s) && !isNaN(l)) { - color = {h, s, l, a, type: 'hsl'}; - break; - } - } - // fallthrough - default: - return false; + if (typeof color === 'string') { + color = colorConverter.parse(color); + } else if (typeof color === 'object' && color && !color.type) { + color = Object.assign({}, color, {type: colorConverter.guessType(color)}); } - if (color) { - if (!initialized) { - init(); - } - setFromColor(color); + if (!color || !color.type) { + return false; } - return Boolean(color); + if (!initialized) { + init(); + } + setFromColor(color); + return true; } function getColor(type) { @@ -280,9 +255,7 @@ return; } readCurrentColorFromRamps(); - const color = type === 'hsl' ? - colorConverter.HSVtoHSL(HSV) : - colorConverter.HSVtoRGB(HSV); + const color = colorConverter.fromHSV(HSV, type); return type ? colorToString(color, type) : color; } @@ -341,7 +314,7 @@ function setFromFormatElement({shiftKey}) { userActivity = performance.now(); HSV.a = isNaN(HSV.a) ? 1 : HSV.a; - const formats = ['hex', 'rgb', 'hsl']; + const formats = Object.keys($inputGroups); const dir = shiftKey ? -1 : 1; const total = formats.length; if ($inputs.colorString === $inputs.prevColorString) { @@ -362,7 +335,7 @@ function setFromInputs(event) { userActivity = event ? performance.now() : userActivity; - if ($inputs[currentFormat].every(validateInput)) { + if (Object.values($inputs[currentFormat]).every(validateInput)) { setFromColor($inputs.color); } } @@ -375,7 +348,7 @@ case 'PageDown': if (!ctrl && !alt && !meta) { const el = document.activeElement; - const inputs = $inputs[currentFormat]; + const inputs = Object.values($inputs[currentFormat]); const lastInput = inputs[inputs.length - 1]; if (key === 'Tab' && shift && el === inputs[0]) { maybeFocus(lastInput); @@ -424,8 +397,8 @@ newValue = options.hexUppercase ? newValue.toUpperCase() : newValue.toLowerCase(); } else if (!alt) { value = parseFloat(el.value); - const isHue = el === $inputs.hsl[0]; - const isAlpha = el === $inputs[currentFormat][3]; + const isHue = el.title === 'H'; + const isAlpha = el === $inputs[currentFormat].a; const isRGB = currentFormat === 'rgb'; const min = isHue ? -360 : 0; const max = isHue ? 360 : isAlpha ? 1 : isRGB ? 255 : 100; @@ -446,7 +419,7 @@ } function validateInput(el) { - const isAlpha = el === $inputs[currentFormat][3]; + const isAlpha = el === $inputs[currentFormat].a; let isValid = (isAlpha || el.value.trim()) && el.checkValidity(); if (!isAlpha && !isValid && currentFormat === 'rgb') { isValid = parseAs(el, parseInt); @@ -464,9 +437,7 @@ function setFromColor(color) { color = typeof color === 'string' ? colorConverter.parse(color) : color; color = color || colorConverter.parse('#f00'); - const newHSV = color.type === 'hsl' ? - colorConverter.HSLtoHSV(color) : - colorConverter.RGBtoHSV(color); + const newHSV = colorConverter.toHSV(color); if (Object.entries(newHSV).every(([k, v]) => v === HSV[k] || Math.abs(v - HSV[k]) < 1e-3)) { return; } @@ -488,7 +459,7 @@ } } $inputGroups[format].dataset.active = ''; - maybeFocus($inputs[format][0]); + maybeFocus(Object.values($inputs[format])[0]); currentFormat = format; } @@ -510,25 +481,13 @@ } function renderInputs() { - const rgb = colorConverter.HSVtoRGB(HSV); - switch (currentFormat) { - case 'hex': - rgb.a = HSV.a; - $hexCode.value = colorToString(rgb, 'hex'); - break; - case 'rgb': { - $rgb.r.value = rgb.r; - $rgb.g.value = rgb.g; - $rgb.b.value = rgb.b; - $rgb.a.value = alphaToString() || 1; - break; - } - case 'hsl': { - const {h, s, l} = colorConverter.HSVtoHSL(HSV); - $hsl.h.value = h; - $hsl.s.value = s; - $hsl.l.value = l; - $hsl.a.value = alphaToString() || 1; + const rgb = colorConverter.fromHSV(HSV, 'rgb'); + if (currentFormat === 'hex') { + $hexCode.value = colorToString(rgb, 'hex'); + } else { + for (const [k, v] of Object.entries(colorConverter.fromHSV(HSV, currentFormat))) { + const el = $inputs[currentFormat][k]; + if (el) el.value = k === 'a' ? alphaToString() || 1 : Math.round(v); } } $swatch.style.backgroundColor = colorToString(rgb, 'rgb'); @@ -704,7 +663,7 @@ } if ( userActivity && - $inputs[currentFormat].every(el => el.checkValidity()) + Object.values($inputs[currentFormat]).every(el => el.checkValidity()) ) { lastOutputColor = colorString.replace(/\b0\./g, '.'); if (isCallable) { @@ -770,7 +729,7 @@ //region Color conversion utilities function colorToString(color, type = currentFormat) { - return colorConverter.format(color, type, options.hexUppercase); + return colorConverter.format(color, type, options); } function alphaToString(a = HSV.a) { @@ -778,9 +737,7 @@ } function currentColorToString(format = currentFormat, alpha = HSV.a) { - const converted = format === 'hsl' ? - colorConverter.HSVtoHSL(HSV) : - colorConverter.HSVtoRGB(HSV); + const converted = colorConverter.fromHSV(HSV, format); converted.a = isNaN(alpha) || alpha === 1 ? undefined : alpha; return colorToString(converted, format); } @@ -879,10 +836,6 @@ return bgLuma < .5 ? 'dark' : 'light'; } - function constrain(min, max, value) { - return value < min ? min : value > max ? max : value; - } - function parseAs(el, parser) { const num = parser(el.value); if (!isNaN(num) && diff --git a/js/color/color-view.js b/js/color/color-view.js index c8143ecd..da4610c3 100644 --- a/js/color/color-view.js +++ b/js/color/color-view.js @@ -8,50 +8,27 @@ const COLORVIEW_CLASS = 'colorview'; const COLORVIEW_SWATCH_CLASS = COLORVIEW_CLASS + '-swatch'; const COLORVIEW_SWATCH_CSS = `--${COLORVIEW_SWATCH_CLASS}:`; - const CLOSE_POPUP_EVENT = 'close-colorpicker-popup'; - const RXS_NUM = /\s*([+-]?(?:\d+\.?\d*|\d*\.\d+))(?:e[+-]?\d+)?/.source; - const RX_COLOR = { - hex: /#(?:[a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)\b/iy, - - rgb: new RegExp([ - // num, num, num [ , num_or_pct]? - // pct, pct, pct [ , num_or_pct]? - `^((${RXS_NUM}\\s*(,|$)){3}|(${RXS_NUM}%\\s*(,|$)){3})(${RXS_NUM}%?)?\\s*$`, - // num num num [ / num_or_pct]? - // pct pct pct [ / num_or_pct]? - `^((${RXS_NUM}\\s*(\\s|$)){3}|(${RXS_NUM}%\\s*(\\s|$)){3})(/${RXS_NUM}%?)?\\s*$`, - ].join('|'), 'iy'), - - hsl: new RegExp([ - // num_or_angle, pct, pct [ , num_or_pct]? - `^(${RXS_NUM}(|deg|g?rad|turn)\\s*),(${RXS_NUM}%\\s*(,|$)){2}(${RXS_NUM}%?)?\\s*$`, - // num_or_angle pct pct [ / num_or_pct]? - `^(${RXS_NUM}(|deg|g?rad|turn)\\s*)\\s(${RXS_NUM}%\\s*(\\s|$)){2}(/${RXS_NUM}%?)?\\s*$`, - ].join('|'), 'iy'), - - unsupported: new RegExp([ - !CSS.supports('color', '#abcd') && /#(.{4}){1,2}$/, - !CSS.supports('color', 'rgb(1e2,0,0)') && /\de/, - !CSS.supports('color', 'rgb(1.5,0,0)') && /^rgba?\((([^,]+,){0,2}[^,]*\.|(\s*\S+\s+){0,2}\S*\.)/, - !CSS.supports('color', 'rgb(1,2,3,.5)') && /[^a]\(([^,]+,){3}/, - !CSS.supports('color', 'rgb(1,2,3,50%)') && /\((([^,]+,){3}|(\s*\S+[\s/]+){3}).*?%/, - !CSS.supports('color', 'rgb(1 2 3 / 1)') && /^[^,]+$/, - !CSS.supports('color', 'hsl(1turn, 2%, 3%)') && /deg|g?rad|turn/, - ].filter(Boolean).map(rx => rx.source).join('|') || '^$', 'i'), - }; - if (RX_COLOR.unsupported.source === '^$') { - RX_COLOR.unsupported = null; - } + const {RX_COLOR, testAt} = colorConverter; + const RX_UNSUPPORTED = (s => s && new RegExp(s))([ + !CSS.supports('color', '#abcd') && /#(.{4}){1,2}$/, + !CSS.supports('color', 'hwb(1 0% 0%)') && /^hwb\(/, + !CSS.supports('color', 'rgb(1e2,0,0)') && /\de/, + !CSS.supports('color', 'rgb(1.5,0,0)') && + /^rgba?\((([^,]+,){0,2}[^,]*\.|(\s*\S+\s+){0,2}\S*\.)/, + !CSS.supports('color', 'rgb(1,2,3,.5)') && /[^a]\(([^,]+,){3}/, + !CSS.supports('color', 'rgb(1,2,3,50%)') && /\((([^,]+,){3}|(\s*\S+[\s/]+){3}).*?%/, + !CSS.supports('color', 'rgb(1 2 3 / 1)') && /^[^,]+$/, + !CSS.supports('color', 'hsl(1turn, 2%, 3%)') && /deg|g?rad|turn/, + ].filter(Boolean).map(rx => rx.source).join('|')); const RX_DETECT = new RegExp('(^|[\\s(){}[\\]:,/"=])' + '(' + RX_COLOR.hex.source + '|' + - '(?:rgb|hsl)a?(?=\\()|(?:' + [...colorConverter.NAMED_COLORS.keys()].join('|') + ')' + + '(?:(?:rgb|hsl)a?|hwb)(?=\\()|(?:' + [...colorConverter.NAMED_COLORS.keys()].join('|') + ')' + '(?=[\\s;(){}[\\]/"!]|$)' + ')', 'gi'); - const RX_DETECT_FUNC = /(rgb|hsl)a?\(/iy; - + const RX_DETECT_FUNC = /((rgb|hsl)a?|hwb)\(/iy; const RX_COMMENT = /\/\*([^*]|\*(?!\/))*(\*\/|$)/g; const SPACE1K = ' '.repeat(1000); @@ -439,7 +416,7 @@ function getSafeColorValue() { if (isHex && color.length !== 5 && color.length !== 9) return color; - if (!RX_COLOR.unsupported || !RX_COLOR.unsupported.test(color)) return color; + if (!RX_UNSUPPORTED || !RX_UNSUPPORTED.test(color)) return color; const value = colorConverter.parse(color); return colorConverter.format(value, 'rgb'); } @@ -710,14 +687,6 @@ setTimeout(() => el.remove(), DURATION_SEC * 1000); } - - function testAt(rx, index, text) { - if (!rx) return false; - rx.lastIndex = index; - return rx.test(text); - } - - function getStyleAtPos({ line, styles = this.getLineHandle(line).styles, diff --git a/js/usercss-compiler.js b/js/usercss-compiler.js index 764afb38..bc6d7d53 100644 --- a/js/usercss-compiler.js +++ b/js/usercss-compiler.js @@ -83,7 +83,7 @@ const BUILDERS = Object.assign(Object.create(null), { if (alpha) delete value.a; const isRgb = isUsoRgb || value.type === 'rgb' || value.a != null && value.a !== 1; const usoMode = isUsoRgb || !isRgb; - value = colorConverter.format(value, isRgb ? 'rgb' : 'hex', undefined, usoMode); + value = colorConverter.format(value, isRgb ? 'rgb' : 'hex', {usoMode}); } return value; case 'dropdown':