Skip to content

Commit

Permalink
Merge pull request #508 from GuillaumeGomez/better-css-colors
Browse files Browse the repository at this point in the history
Also handle colors from within CSS functions
  • Loading branch information
GuillaumeGomez committed May 22, 2023
2 parents 4ddccec + 92679a5 commit 44ea575
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 66 deletions.
4 changes: 2 additions & 2 deletions src/css-color-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, ")");
}

Expand Down
170 changes: 110 additions & 60 deletions src/css_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,75 @@ 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';
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(')) {
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;
Expand All @@ -26,34 +89,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);
}
}
}
Expand All @@ -62,43 +99,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));
Expand All @@ -113,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;
Expand All @@ -128,6 +160,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);
Expand All @@ -149,6 +191,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);

Expand All @@ -169,10 +213,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);
Expand Down
42 changes: 38 additions & 4 deletions tests/test-js/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
Expand All @@ -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');

Expand All @@ -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');
Expand Down Expand Up @@ -91,6 +103,28 @@ 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), 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) {
Expand Down

0 comments on commit 44ea575

Please sign in to comment.