From dde6dd54db6b9fac735035f43bbe9656266c414a Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Tue, 16 Dec 2025 08:55:57 +0900 Subject: [PATCH 1/2] Fix color channels resolver Fix #87 --- src/js/relative-color.ts | 38 ++++++++++++++++++++-- test/relative-color.test.ts | 65 +++++++++++++++++++++++++++++++++++-- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/js/relative-color.ts b/src/js/relative-color.ts index 9a64bc3..e947066 100644 --- a/src/js/relative-color.ts +++ b/src/js/relative-color.ts @@ -45,6 +45,7 @@ import { const { CloseParen: PAREN_CLOSE, Comment: COMMENT, + Delim: DELIM, Dimension: DIM, EOF, Function: FUNC, @@ -81,6 +82,7 @@ const REG_FN_REL = new RegExp(FN_REL); const REG_FN_REL_CAPT = new RegExp(`^${FN_REL_CAPT}`); const REG_FN_REL_START = new RegExp(`^${FN_REL}`); const REG_FN_VAR = new RegExp(SYN_FN_VAR); +const REG_FUNC_CALC_SUM = /^(?:abs|sign|sin|cos|tan)\(/; /** * resolve relative color channels @@ -122,7 +124,8 @@ export function resolveColorChannels( ] = [[], [], [], []]; let i = 0; let nest = 0; - let func = false; + let func = ''; + let precededPct = false; while (tokens.length) { const token = tokens.shift(); if (!Array.isArray(token)) { @@ -138,7 +141,24 @@ export function resolveColorChannels( const channel = channels[i]; if (Array.isArray(channel)) { switch (type) { + case DELIM: { + if (func) { + if ( + (value === '+' || value === '-') && + precededPct && + !REG_FUNC_CALC_SUM.test(func) + ) { + return new NullObject(); + } + precededPct = false; + channel.push(value); + } + break; + } case DIM: { + if (!func || !REG_FUNC_CALC_SUM.test(func)) { + return new NullObject(); + } const resolvedValue = resolveDimension(token, opt); if (isString(resolvedValue)) { channel.push(resolvedValue); @@ -149,7 +169,7 @@ export function resolveColorChannels( } case FUNC: { channel.push(value); - func = true; + func = value; nest++; if (REG_FN_MATH_START.test(value)) { mathFunc.add(nest); @@ -192,13 +212,25 @@ export function resolveColorChannels( } nest--; if (nest === 0) { - func = false; + func = ''; i++; } } break; } case PCT: { + if (!func) { + return new NullObject(); + } else if (!REG_FUNC_CALC_SUM.test(func)) { + const lastValue = channel.toReversed().find(v => v !== ' '); + if (lastValue === '+' || lastValue === '-') { + return new NullObject(); + } else if (lastValue === '*' || lastValue === '/') { + precededPct = false; + } else { + precededPct = true; + } + } channel.push(Number(detail?.value) / MAX_PCT); if (!func) { i++; diff --git a/test/relative-color.test.ts b/test/relative-color.test.ts index b41ace3..d1b37d2 100644 --- a/test/relative-color.test.ts +++ b/test/relative-color.test.ts @@ -66,14 +66,14 @@ describe('resolve relative color channels', () => { assert.deepEqual(res, ['r', 'calc(1 * g)', 'abs(-10)'], 'result'); }); - it('should get value', () => { + it('should get null object', () => { const css = ' r calc(g * sign(2em)) 1000%)'; const tokens = tokenize({ css }); const res = func(tokens, { colorSpace: 'rgb', format: 'specifiedValue' }); - assert.deepEqual(res, ['r', 'calc(1 * g)', 10], 'result'); + assert.deepEqual(res.isNull, true, 'result'); }); it('should get value', () => { @@ -325,6 +325,67 @@ describe('extract origin color', () => { }); assert.strictEqual(res, 'rgb(from rgb(0, 128, 0) r g b)', 'result'); }); + + it('should get value', () => { + const res = func('rgb(from rebeccapurple calc(r * 2) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual( + res, + 'rgb(from rebeccapurple calc(2 * r) g b)', + 'result' + ); + }); + + it('should get value', () => { + const res = func('rgb(from rebeccapurple calc(r * 50%) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual( + res, + 'rgb(from rebeccapurple calc(0.5 * r) g b)', + 'result' + ); + }); + + it('should get value', () => { + const res = func('rgb(from rebeccapurple calc(r * sign(50%)) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual( + res, + 'rgb(from rebeccapurple calc(1 * r) g b)', + 'result' + ); + }); + + it('should get null object', () => { + const res = func('rgb(from rebeccapurple calc(r * 2rem) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual(res.isNull, true, 'result'); + }); + + it('should get null object', () => { + const res = func('rgb(from rebeccapurple calc(r + 50%) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual(res.isNull, true, 'result'); + }); + + it('should get null object', () => { + const res = func('rgb(from rebeccapurple calc(2rem * r) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual(res.isNull, true, 'result'); + }); + + it('should get null object', () => { + const res = func('rgb(from rebeccapurple calc(50% + r) g b)', { + format: 'specifiedValue' + }); + assert.strictEqual(res.isNull, true, 'result'); + }); }); describe('resolve relative color', () => { From abc6f05a3f95909538c1c8cba4937679de6f1f20 Mon Sep 17 00:00:00 2001 From: "asamuzaK (Kazz)" Date: Tue, 16 Dec 2025 12:04:09 +0900 Subject: [PATCH 2/2] Refactor regex for calculation functions --- src/js/relative-color.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/relative-color.ts b/src/js/relative-color.ts index e947066..a85967b 100644 --- a/src/js/relative-color.ts +++ b/src/js/relative-color.ts @@ -77,12 +77,12 @@ const REG_COLOR_CAPT = new RegExp( ); const REG_CS_HSL = /(?:hsla?|hwb)$/; const REG_CS_CIE = new RegExp(`^(?:${CS_LAB}|${CS_LCH})$`); +const REG_FN_CALC_SUM = /^(?:abs|sign|sin|cos|tan)\(/; const REG_FN_MATH_START = new RegExp(SYN_FN_MATH_START); const REG_FN_REL = new RegExp(FN_REL); const REG_FN_REL_CAPT = new RegExp(`^${FN_REL_CAPT}`); const REG_FN_REL_START = new RegExp(`^${FN_REL}`); const REG_FN_VAR = new RegExp(SYN_FN_VAR); -const REG_FUNC_CALC_SUM = /^(?:abs|sign|sin|cos|tan)\(/; /** * resolve relative color channels @@ -146,7 +146,7 @@ export function resolveColorChannels( if ( (value === '+' || value === '-') && precededPct && - !REG_FUNC_CALC_SUM.test(func) + !REG_FN_CALC_SUM.test(func) ) { return new NullObject(); } @@ -156,7 +156,7 @@ export function resolveColorChannels( break; } case DIM: { - if (!func || !REG_FUNC_CALC_SUM.test(func)) { + if (!func || !REG_FN_CALC_SUM.test(func)) { return new NullObject(); } const resolvedValue = resolveDimension(token, opt); @@ -221,7 +221,7 @@ export function resolveColorChannels( case PCT: { if (!func) { return new NullObject(); - } else if (!REG_FUNC_CALC_SUM.test(func)) { + } else if (!REG_FN_CALC_SUM.test(func)) { const lastValue = channel.toReversed().find(v => v !== ' '); if (lastValue === '+' || lastValue === '-') { return new NullObject();