From cbf9f1a7b17edf027c744c467fd12d0f9a3ee44c Mon Sep 17 00:00:00 2001 From: Durvesh Pilankar Date: Fri, 26 Jun 2026 15:08:46 -0700 Subject: [PATCH 1/2] Fix HashColor channel getters for 3-digit shorthand hex The HashColor parser accepts 3-, 6-, and 8-digit hex, but the r/g/b/a getters sliced fixed 2-char windows, assuming 6/8 digits. For shorthand like '#f0a' (= #ff00aa) this returned r=240, g=10, b=NaN instead of 255/0/170. Add a private #expanded() helper that doubles each digit of a 3-digit value before slicing; 6/8-digit values are unchanged. The stored value is left untouched so toString() and equality semantics are preserved. Adds a regression test that fails before the change and passes after. --- .../src/css-types/__tests__/color-test.js | 15 +++++++++++++ .../style-value-parser/src/css-types/color.js | 22 ++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/style-value-parser/src/css-types/__tests__/color-test.js b/packages/style-value-parser/src/css-types/__tests__/color-test.js index 8cf179dce..2f6600dcb 100644 --- a/packages/style-value-parser/src/css-types/__tests__/color-test.js +++ b/packages/style-value-parser/src/css-types/__tests__/color-test.js @@ -27,6 +27,21 @@ describe('Test CSS Type: ', () => { expect(Color.parser.parse('#ffffff')).toEqual(new HashColor('ffffff')); }); + test('hash color channel getters expand 3-digit shorthand', () => { + // The parser accepts 3-digit shorthand hex, so the channel getters must + // expand each digit (#f0a === #ff00aa) rather than slicing fixed windows. + expect(Color.parser.parse('#f0a')).toEqual(new HashColor('f0a')); + const short = new HashColor('f0a'); + expect(short.r).toBe(255); + expect(short.g).toBe(0); + expect(short.b).toBe(170); + expect(short.a).toBe(1); + // Full-length hex (with and without alpha) is unaffected. + const full = new HashColor('ff00aa'); + expect([full.r, full.g, full.b]).toEqual([255, 0, 170]); + expect(new HashColor('ff00aa80').a).toBeCloseTo(128 / 255); + }); + test('parses RGB values', () => { expect(Color.parser.parse('rgb(255, 0, 0)')).toEqual(new Rgb(255, 0, 0)); expect(Color.parser.parse('rgb(0, 255, 0)')).toEqual(new Rgb(0, 255, 0)); diff --git a/packages/style-value-parser/src/css-types/color.js b/packages/style-value-parser/src/css-types/color.js index 080822759..ac760120c 100644 --- a/packages/style-value-parser/src/css-types/color.js +++ b/packages/style-value-parser/src/css-types/color.js @@ -208,22 +208,32 @@ export class HashColor extends Color { return `#${this.value}`; } + // Expand 3-digit shorthand (e.g. 'f0a' -> 'ff00aa') so the channel getters + // can slice fixed 2-char windows. 6- and 8-digit values are returned as-is. + #expanded(): string { + return this.value.length === 3 + ? this.value + .split('') + .map((c) => c + c) + .join('') + : this.value; + } + get r(): number { - return parseInt(this.value.slice(0, 2), 16); + return parseInt(this.#expanded().slice(0, 2), 16); } get g(): number { - return parseInt(this.value.slice(2, 4), 16); + return parseInt(this.#expanded().slice(2, 4), 16); } get b(): number { - return parseInt(this.value.slice(4, 6), 16); + return parseInt(this.#expanded().slice(4, 6), 16); } get a(): number { - return this.value.length === 8 - ? parseInt(this.value.slice(6, 8), 16) / 255 - : 1; + const value = this.#expanded(); + return value.length === 8 ? parseInt(value.slice(6, 8), 16) / 255 : 1; } static get parser(): TokenParser { From 409a145430e8bccdaff96154e3e24e2da9c1773e Mon Sep 17 00:00:00 2001 From: Durvesh Pilankar Date: Fri, 26 Jun 2026 15:12:33 -0700 Subject: [PATCH 2/2] Fix border-radius serializer dropping asymmetric vertical radii MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BorderRadiusShorthand.toString() initialized the vertical group string (sStr) from the horizontal corner values instead of the vertical ones — a copy-paste of the horizontal initializer. The following branches only overwrite sStr when the vertical radii are symmetric, so a fully asymmetric vertical group (e.g. 1px 2px 3px 4px) fell through to the wrong horizontal-based default, emitting '5px / 5px 5px 5px 5px' instead of '5px / 1px 2px 3px 4px'. Initialize sStr from the vertical values. Adds a toString() regression test (the shorthand toString path previously had no coverage). --- .../properties/__tests__/border-radius.test.js | 17 +++++++++++++++++ .../src/properties/border-radius.js | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/style-value-parser/src/properties/__tests__/border-radius.test.js b/packages/style-value-parser/src/properties/__tests__/border-radius.test.js index f4dc6ef7d..29984bfc4 100644 --- a/packages/style-value-parser/src/properties/__tests__/border-radius.test.js +++ b/packages/style-value-parser/src/properties/__tests__/border-radius.test.js @@ -240,6 +240,23 @@ describe('Test CSS property shorthand: `border-radius`', () => { ); }); + test('toString: distinct vertical radii are not overwritten by horizontal', () => { + // Horizontal group collapses to a single value (all 5px); the vertical + // group is fully asymmetric (1/2/3/4), so it must be serialized as-is and + // not fall back to the horizontal values. + const radius = new BorderRadiusShorthand( + new Length(5, 'px'), + new Length(5, 'px'), + new Length(5, 'px'), + new Length(5, 'px'), + new Length(1, 'px'), + new Length(2, 'px'), + new Length(3, 'px'), + new Length(4, 'px'), + ); + expect(radius.toString()).toBe('5px / 1px 2px 3px 4px'); + }); + test('Valid: border-radius: / ', () => { expect( BorderRadiusShorthand.parse.parseToEnd('10px 20px / 30px 40px'), diff --git a/packages/style-value-parser/src/properties/border-radius.js b/packages/style-value-parser/src/properties/border-radius.js index cfe200831..cf3f9169e 100644 --- a/packages/style-value-parser/src/properties/border-radius.js +++ b/packages/style-value-parser/src/properties/border-radius.js @@ -106,7 +106,7 @@ export class BorderRadiusShorthand { const verticalBottomRight = this.verticalBottomRight.toString(); const verticalBottomLeft = this.verticalBottomLeft.toString(); - let sStr = `${horizontalTopLeft} ${horizontalTopRight} ${horizontalBottomRight} ${horizontalBottomLeft}`; + let sStr = `${verticalTopLeft} ${verticalTopRight} ${verticalBottomRight} ${verticalBottomLeft}`; // All three are the same if ( verticalTopLeft === verticalTopRight &&