From 04664c878968c8d9a517e2aac1600d1d4bca3c27 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 22 May 2023 17:14:10 +0200 Subject: [PATCH 1/4] Also handle colors from within CSS functions --- src/css_parser.js | 166 ++++++++++++++++++++++++++++++---------------- 1 file changed, 107 insertions(+), 59 deletions(-) diff --git a/src/css_parser.js b/src/css_parser.js index 50173a96..345a545d 100644 --- a/src/css_parser.js +++ b/src/css_parser.js @@ -11,12 +11,73 @@ function toHex(n) { } class CssElem { - constructor(kind, value) { + constructor(kind, value, functionName = null, innerArgs = null) { this.kind = kind; this.value = value; + this.functionName = functionName; + this.innerArgs = innerArgs; } } +function convertElemToColor(parser, elem) { + const ret = fromString(elem.value); + if (ret !== null) { + // Ok, it's a color, now we need to get the exact kind for the "back" + // conversion. + let kind = 'keyword'; + if (elem.value.startsWith('#')) { + if (elem.value.length < 7) { + kind = 'hex-short'; + elem.hasAlpha = elem.value.length > 4; + } else { + kind = 'hex'; + elem.hasAlpha = elem.value.length > 7; + } + } else if (elem.value.startsWith('rgb(')) { + kind = 'rgb'; + } else if (elem.value.startsWith('rgba(')) { + kind = 'rgba'; + } else if (elem.value.startsWith('hsl(')) { + kind = 'hsl'; + } else if (elem.value.startsWith('hsla(')) { + kind = 'hsla'; + } + elem.kind = 'color'; + elem.colorKind = kind; + elem.color = ret.toRgbaArray(); + parser.hasColor = true; + return true; + } else if (elem.innerArgs !== null) { + let containsColor = false; + for (const arg of elem.innerArgs) { + containsColor = convertElemToColor(parser, arg) || containsColor; + } + elem.containsColor = containsColor; + parser.hasColor = containsColor || parser.hasColor; + return containsColor; + } + return false; +} + +function toRGBAStringInner(elems, separator) { + let output = ''; + + for (const elem of elems) { + if (output.length > 0) { + output += separator; + } + if (elem.kind === 'function' && elem.containsColor === true) { + output += `${elem.functionName}(${toRGBAStringInner(elem.innerArgs, ', ')})`; + } else if (elem.kind !== 'color') { + output += elem.value; + } else { + const [r, g, b, a] = elem.color; + output += `rgba(${r}, ${g}, ${b}, ${a})`; + } + } + return output; +} + class CssParser { constructor(input) { this.input = input; @@ -26,34 +87,8 @@ class CssParser { this.parse(); for (const elem of this.elems) { - if (elem.kind === 'ident') { - const ret = fromString(elem.value); - if (ret !== null) { - // Ok, it's a color, now we need to get the exact kind for the "back" - // conversion. - let kind = 'keyword'; - if (elem.value.startsWith('#')) { - if (elem.value.length < 7) { - kind = 'hex-short'; - elem.hasAlpha = elem.value.length > 4; - } else { - kind = 'hex'; - elem.hasAlpha = elem.value.length > 7; - } - } else if (elem.value.startsWith('rgb(')) { - kind = 'rgb'; - } else if (elem.value.startsWith('rgba(')) { - kind = 'rgba'; - } else if (elem.value.startsWith('hsl(')) { - kind = 'hsl'; - } else if (elem.value.startsWith('hsla(')) { - kind = 'hsla'; - } - elem.kind = 'color'; - elem.colorKind = kind; - elem.color = ret.toRgbaArray(); - this.hasColor = true; - } + if (elem.kind === 'ident' || elem.kind === 'function') { + convertElemToColor(this, elem); } } } @@ -62,43 +97,38 @@ class CssParser { if (!this.hasColor) { return this.input; } - let output = ''; - - for (const elem of this.elems) { - if (output.length > 0) { - output += ' '; - } - if (elem.kind !== 'color') { - output += elem.value; - } else { - const [r, g, b, a] = elem.color; - output += `rgba(${r}, ${g}, ${b}, ${a})`; - } - } - return output; + return toRGBAStringInner(this.elems, ' '); } - sameFormatAs(other) { - if (!other.hasColor || !this.hasColor) { - return this.input; - } - if (other.elems.length !== this.elems.length) { - return this.input; - } + sameFormatAsInner(elems, otherElems, level, separator) { let output = ''; - for (const [i, elem] of this.elems.entries()) { - const otherElem = other.elems[i]; - if (elem.kind !== otherElem.kind) { - return this.input; - } + for (const [i, elem] of elems.entries()) { + const otherElem = i < otherElems.length ? otherElems[i] : { 'kind': null }; if (output.length > 0) { - output += ' '; + output += separator; } - if (elem.kind !== 'color') { + if (elem.kind !== otherElem.kind) { + if (level === 0) { + return this.input; + } output += elem.value; continue; } + if (elem.kind !== 'color') { + if (elem.kind === 'function' && elem.containsColor === true) { + const inner = this.sameFormatAsInner( + elem.innerArgs, + otherElem.innerArgs, + level + 1, + ', ', + ); + output += `${elem.functionName}(${inner})`; + } else { + output += elem.value; + } + continue; + } if (otherElem.colorKind.startsWith('hex')) { const alpha = otherElem.hasAlpha || elem.color[3] < 1 ? elem.color[3] * 255 : null; const values = elem.color.slice(0, 3).map(v => toHex(v)); @@ -128,6 +158,16 @@ class CssParser { return output; } + sameFormatAs(other) { + if (!other.hasColor || !this.hasColor) { + return this.input; + } + if (other.elems.length !== this.elems.length) { + return this.input; + } + return this.sameFormatAsInner(this.elems, other.elems, 0, ' '); + } + parse(pushTo = null, endChar = null) { while (this.pos < this.input.length) { const c = this.input.charAt(this.pos); @@ -149,6 +189,8 @@ class CssParser { parseString(endChar, pushTo) { const start = this.pos; + // Skipping string character. + this.pos += 1; for (; this.pos < this.input.length; this.pos += 1) { const c = this.input.charAt(this.pos); @@ -169,10 +211,16 @@ class CssParser { if (isStringChar(c) || isWhiteSpace(c) || c === ',' || c === ')') { break; } else if (c === '(') { + const funcName = this.input.substring(start, this.pos); this.pos += 1; // To go to the next character directly. - this.parse([], ')'); + const args = []; + this.parse(args, ')'); this.pos += 1; // To include the ')' at the end. - break; + this.push( + new CssElem('function', this.input.substring(start, this.pos), funcName, args), + pushTo, + ); + return; } } this.push(new CssElem('ident', this.input.substring(start, this.pos)), pushTo); From 531bdda0510d76c5f556224f88fcba434e031d09 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 22 May 2023 17:14:19 +0200 Subject: [PATCH 2/4] Update CSS parser tests --- tests/test-js/parser.js | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/tests/test-js/parser.js b/tests/test-js/parser.js index 69462f4f..586242e5 100644 --- a/tests/test-js/parser.js +++ b/tests/test-js/parser.js @@ -7,7 +7,8 @@ function checkCssParser(x) { let p = new CssParser('rgb()'); x.assert(p.hasColor, false); x.assert(p.elems.length, 1); - x.assert(p.elems[0].kind, 'ident'); + x.assert(p.elems[0].kind, 'function'); + x.assert(p.elems[0].containsColor, false); x.assert(p.elems[0].value, 'rgb()'); p = new CssParser('rgb(1, 2, 3)'); @@ -19,12 +20,17 @@ function checkCssParser(x) { x.assert(p.elems[0].color, [1, 2, 3, 1]); p = new CssParser('1px whatever(rgb(1, 2, 3), a), 3'); - x.assert(p.hasColor, false); + x.assert(p.hasColor, true); x.assert(p.elems.length, 3); x.assert(p.elems[0].kind, 'ident'); x.assert(p.elems[0].value, '1px'); - x.assert(p.elems[1].kind, 'ident'); + x.assert(p.elems[1].kind, 'function'); x.assert(p.elems[1].value, 'whatever(rgb(1, 2, 3), a)'); + x.assert(p.elems[1].functionName, 'whatever'); + x.assert(p.elems[1].containsColor, true); + x.assert(p.elems[1].innerArgs.length, 2); + x.assert(p.elems[1].innerArgs[0].kind, 'color'); + x.assert(p.elems[1].innerArgs[1].kind, 'ident'); x.assert(p.elems[2].kind, 'ident'); x.assert(p.elems[2].value, '3'); @@ -44,10 +50,16 @@ function checkCssParser(x) { x.assert(p.elems[3].kind, 'ident'); x.assert(p.elems[3].value, 'a'); + p = new CssParser('"rgb(1, 2, 3)"'); + x.assert(p.elems.length, 1); + x.assert(p.elems[0].kind, 'string'); + x.assert(p.elems[0].value, '"rgb(1, 2, 3)"'); + p = new CssParser('url("rgb(1, 2, 3)")'); x.assert(p.hasColor, false); x.assert(p.elems.length, 1); - x.assert(p.elems[0].kind, 'ident'); + x.assert(p.elems[0].kind, 'function'); + x.assert(p.elems[0].containsColor, false); x.assert(p.elems[0].value, 'url("rgb(1, 2, 3)")'); p = new CssParser('transparent whitesmoke'); @@ -91,6 +103,24 @@ function checkCssParser(x) { p2.sameFormatAs(p), '#111f #010101ff #010101', ); + + p = new CssParser('linear-gradient(rgb(15, 20, 25), rgba(15, 20, 25, 0))'); + x.assert(p.hasColor, true); + x.assert(p.elems.length, 1); + x.assert(p.elems[0].kind, 'function'); + x.assert(p.elems[0].containsColor, true); + x.assert(p.elems[0].innerArgs.length, 2); + x.assert(p.elems[0].innerArgs[0].kind, 'color'); + x.assert(p.elems[0].innerArgs[0].value, 'rgb(15, 20, 25)'); + x.assert(p.elems[0].innerArgs[1].kind, 'color'); + x.assert(p.elems[0].innerArgs[1].value, 'rgba(15, 20, 25, 0)'); + x.assert(p.toRGBAString(), 'linear-gradient(rgba(15, 20, 25, 1), rgba(15, 20, 25, 0))'); + + p2 = new CssParser('linear-gradient(#aaa, #fff)'); + x.assert(p2.hasColor, true); + x.assert(p2.elems.length, 1); + x.assert(p2.toRGBAString(), 'linear-gradient(rgba(170, 170, 170, 1), rgba(255, 255, 255, 1))'); + x.assert(p2.sameFormatAs(p), 'linear-gradient(rgb(170, 170, 170), rgb(255, 255, 255))'); } function checkTuple(x) { From 65effb099035efb25fa115ca51a3781f6c5905ce Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 22 May 2023 17:18:56 +0200 Subject: [PATCH 3/4] Correctly pick rgba() if the other also uses rgba() --- src/css-color-converter.js | 4 ++-- src/css_parser.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/css-color-converter.js b/src/css-color-converter.js index 037d4890..3b6659d8 100644 --- a/src/css-color-converter.js +++ b/src/css-color-converter.js @@ -118,14 +118,14 @@ var Color = /*#__PURE__*/function () { _createClass(Color, [{ key: "toRgbString", - value: function toRgbString() { + value: function toRgbString(forceAlpha = false) { var _this$values = _slicedToArray(this.values, 4), r = _this$values[0], g = _this$values[1], b = _this$values[2], a = _this$values[3]; - if (a === 1) { + if (a === 1 && !forceAlpha) { return "rgb(".concat(r, ", ").concat(g, ", ").concat(b, ")"); } diff --git a/src/css_parser.js b/src/css_parser.js index 345a545d..32cdd65c 100644 --- a/src/css_parser.js +++ b/src/css_parser.js @@ -35,8 +35,10 @@ function convertElemToColor(parser, elem) { } } else if (elem.value.startsWith('rgb(')) { kind = 'rgb'; + elem.hasAlpha = false; } else if (elem.value.startsWith('rgba(')) { kind = 'rgba'; + elem.hasAlpha = true; } else if (elem.value.startsWith('hsl(')) { kind = 'hsl'; } else if (elem.value.startsWith('hsla(')) { @@ -143,7 +145,7 @@ class CssParser { } else if (otherElem.colorKind.startsWith('hsl')) { output += fromRgba(elem.color).toHslString(); } else if (otherElem.colorKind.startsWith('rgb')) { - output += fromRgba(elem.color).toRgbString(); + output += fromRgba(elem.color).toRgbString(otherElem.hasAlpha); } else { const ret = Object.entries(allColors).find(([key, _]) => { return key === otherElem.value; From 92679a5cc7a2e04cd2e7fbfb22e904110b60921e Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Mon, 22 May 2023 17:19:38 +0200 Subject: [PATCH 4/4] Add CssParser tests for rgba conversions --- tests/test-js/parser.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test-js/parser.js b/tests/test-js/parser.js index 586242e5..de0b26f4 100644 --- a/tests/test-js/parser.js +++ b/tests/test-js/parser.js @@ -120,7 +120,11 @@ function checkCssParser(x) { x.assert(p2.hasColor, true); x.assert(p2.elems.length, 1); x.assert(p2.toRGBAString(), 'linear-gradient(rgba(170, 170, 170, 1), rgba(255, 255, 255, 1))'); - x.assert(p2.sameFormatAs(p), 'linear-gradient(rgb(170, 170, 170), rgb(255, 255, 255))'); + x.assert(p2.sameFormatAs(p), 'linear-gradient(rgb(170, 170, 170), rgba(255, 255, 255, 1))'); + + p = new CssParser('rgba(15, 20, 25, 1)'); + p2 = new CssParser('rgb(0,0,0)'); + x.assert(p2.sameFormatAs(p), 'rgba(0, 0, 0, 1)'); } function checkTuple(x) {