From 067222b7c025b12d143e09faf8066e63b3b38a38 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:07:53 +0000 Subject: [PATCH 1/9] css: parse color(a98-rgb ...) and the r channel of srgb-linear relative colors parse_predefined_relative matched the ident "a99-rgb" when building the predefined color, so color(a98-rgb ...) never parsed and was kept as an unparsed token stream: no sRGB/display-p3 fallbacks, no relative color folding, and Bun.color() returned null for it. SRGBLinear declared its r channel as an angle, so color(from ... srgb-linear r g b) could not resolve r and never parsed either. Both were copied from the original implementation and preserved as intentional. lightningcss accepts both forms. --- src/css/values/color.rs | 7 +-- test/js/bun/css/color.test.ts | 35 +++++++++++ test/js/bun/css/css.test.ts | 109 ++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/src/css/values/color.rs b/src/css/values/color.rs index c79d0857d419..8fd4d187708f 100644 --- a/src/css/values/color.rs +++ b/src/css/values/color.rs @@ -1778,9 +1778,7 @@ define_colorspace! { define_colorspace! { /// A color in the [`sRGB-linear`](https://www.w3.org/TR/css-color-4/#predefined-sRGB-linear) color space. SRGBLinear { r, g, b } - // `r` intentionally uses the angle channel type (sic) — kept for - // behavioral compatibility. - types = (CT_ANG, CT_PCT, CT_PCT); + types = (CT_PCT, CT_PCT, CT_PCT); gamut = bounded; premultiply = rectangular; powerless = none; @@ -2352,8 +2350,7 @@ pub(crate) fn parse_predefined_relative( b"srgb" => PredefinedColor::Srgb(SRGB { r: a, g: b, b: c, alpha }), b"srgb-linear" => PredefinedColor::SrgbLinear(SRGBLinear { r: a, g: b, b: c, alpha }), b"display-p3" => PredefinedColor::DisplayP3(P3 { r: a, g: b, b: c, alpha }), - // "a99-rgb" (sic) — kept for behavioral compatibility. - b"a99-rgb" => PredefinedColor::A98(A98 { r: a, g: b, b: c, alpha }), + b"a98-rgb" => PredefinedColor::A98(A98 { r: a, g: b, b: c, alpha }), b"prophoto-rgb" => PredefinedColor::Prophoto(ProPhoto { r: a, g: b, b: c, alpha }), b"rec2020" => PredefinedColor::Rec2020(Rec2020 { r: a, g: b, b: c, alpha }), b"xyz-d50" => PredefinedColor::XyzD50(XYZd50 { x: a, y: b, z: c, alpha }), diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index 567d7e71632b..eb903c5f1e02 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -347,6 +347,41 @@ describe("lab()/oklab() sRGB fallback for boundary colors (#33331)", () => { }); }); +describe("color() predefined color spaces", () => { + const spaces: [space: string, channels: string, printed?: string][] = [ + ["srgb", "b g r"], + ["srgb-linear", "b g r"], + ["display-p3", "b g r"], + ["a98-rgb", "b g r"], + ["prophoto-rgb", "b g r"], + ["rec2020", "b g r"], + ["xyz-d50", "z y x"], + ["xyz-d65", "z y x", "xyz"], + ["xyz", "z y x"], + ]; + + test.each(spaces)("color(%s ...) parses", (space, channels, printed = space) => { + expect(color(`color(${space.toUpperCase()} 0 1 0 / 50%)`, "css")).toBe(`color(${printed} 0 1 0 / .5)`); + expect(color(`color(from color(${space} .5 .25 .125) ${space} ${channels})`, "css")).toBe( + `color(${printed} .125 .25 .5)`, + ); + }); + + test("a98-rgb converts to sRGB", () => { + expect(color("rgb(from color(a98-rgb .5 .25 .125) r g b)", "hex")).toBe("#923e17"); + expect(color("color-mix(in srgb, color(a98-rgb .5 .25 .125), white)", "hex")).toBe("#c89e8b"); + }); + + test("a98-rgb is a relative color target", () => { + expect(color("color(from #c86432 a98-rgb r g b)", "css")).toBe("color(a98-rgb .695066 .391898 .220089)"); + }); + + test("srgb-linear relative colors can reference r", () => { + expect(color("color(from red srgb-linear r g b)", "css")).toBe("color(srgb-linear 1 0 0)"); + expect(color("color(from red srgb-linear g g r)", "css")).toBe("color(srgb-linear 0 0 1)"); + }); +}); + // 2^24 color() calls take minutes on debug builds (past the per-test timeout) and dominate // the ASAN lane, so those sweep the ansi256 equivalence classes (~13k deterministic inputs): // each single channel, the grey diagonal, the sub-8 cube, and a coarse 17-step cube. diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 5ecfac95ab76..159a99e8788f 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7701,6 +7701,115 @@ describe("css tests", () => { minify_test(".foo{grid-template-areas:none}", ".foo{grid-template-areas:none}"); }); + describe("color()", () => { + // Every predefined color space, in the absolute form and as the target of a + // relative color that references all three channels. Expected values are + // lightningcss output. A color() that fails to parse is kept as an unparsed + // token stream, so the alpha percentage and the channel keywords would be + // printed verbatim instead of being folded. + const predefined: [space: string, channels: string, printed?: string][] = [ + ["srgb", "b g r"], + ["srgb-linear", "b g r"], + ["display-p3", "b g r"], + ["a98-rgb", "b g r"], + ["prophoto-rgb", "b g r"], + ["rec2020", "b g r"], + ["xyz-d50", "z y x"], + ["xyz-d65", "z y x", "xyz"], + ["xyz", "z y x"], + ]; + for (const [space, channels, printed = space] of predefined) { + minify_test(`.foo { color: color(${space} 0 1 0 / 50%) }`, `.foo{color:color(${printed} 0 1 0/.5)}`); + minify_test( + `.foo { color: color(from color(${space} .5 .25 .125) ${space} ${channels}) }`, + `.foo{color:color(${printed} .125 .25 .5)}`, + ); + } + + describe("a98-rgb", () => { + minify_test(".foo { color: color(A98-RGB 0 1 0 / 50%) }", ".foo{color:color(a98-rgb 0 1 0/.5)}"); + minify_test(".foo { color: color(a98-rgb 100% 50% 0%) }", ".foo{color:color(a98-rgb 1 .5 0)}"); + minify_test( + ".foo { color: color(from #c86432 a98-rgb r g b) }", + ".foo{color:color(a98-rgb .695066 .391898 .220089)}", + ); + minify_test( + ".foo { color: color(from #c86432 a98-rgb r g b / 50%) }", + ".foo{color:color(a98-rgb .695066 .391898 .220089/.5)}", + ); + minify_test(".foo { color: rgb(from color(a98-rgb .5 .25 .125) r g b) }", ".foo{color:#923e17}"); + minify_test( + ".foo { color: color(from color(a98-rgb .5 .25 .125) srgb r g b) }", + ".foo{color:color(srgb .570881 .241183 .0913544)}", + ); + minify_test( + ".foo { color: color(from color(a98-rgb .5 .25 .125) display-p3 r g b) }", + ".foo{color:color(display-p3 .530457 .26084 .134638)}", + ); + minify_test(".foo { color: color-mix(in srgb, color(a98-rgb .5 .25 .125), white) }", ".foo{color:#c89e8b}"); + + prefix_test( + ".foo { color: color(a98-rgb 1 0 0) }", + indoc` + .foo { + color: #ff6251; + color: color(a98-rgb 1 0 0); + } + `, + { chrome: 90 << 16 }, + ); + prefix_test( + ".foo { color: color(a98-rgb 1 0 0) }", + indoc` + .foo { + color: #ff6251; + color: color(display-p3 1.0633 .238564 .167582); + color: color(a98-rgb 1 0 0); + } + `, + { chrome: 87 << 16, safari: 14 << 16 }, + ); + prefix_test( + ".foo { color: color(from #c86432 a98-rgb r g b / 50%) }", + indoc` + .foo { + color: #c8643280; + color: color(a98-rgb .695066 .391898 .220089 / .5); + } + `, + { chrome: 90 << 16 }, + ); + prefix_test( + ".foo { background: linear-gradient(color(a98-rgb .5 .25 .125), color(a98-rgb .4 .5 .6)) }", + indoc` + .foo { + background: linear-gradient(#923e17, #59819b); + background: linear-gradient(color(a98-rgb .5 .25 .125), color(a98-rgb .4 .5 .6)); + } + `, + { chrome: 90 << 16 }, + ); + }); + + describe("srgb-linear", () => { + // The `r` channel keyword of an srgb-linear relative color. + minify_test(".foo { color: color(from red srgb-linear r g b) }", ".foo{color:color(srgb-linear 1 0 0)}"); + minify_test(".foo { color: color(from red srgb-linear g g r) }", ".foo{color:color(srgb-linear 0 0 1)}"); + minify_test( + ".foo { color: color(from color(srgb-linear .5 .25 .125) srgb-linear calc(r / 2) g b) }", + ".foo{color:color(srgb-linear .25 .25 .125)}", + ); + minify_test( + ".foo { color: color(from color(srgb-linear .5 .25 .125) srgb-linear r g b / 50%) }", + ".foo{color:color(srgb-linear .5 .25 .125/.5)}", + ); + minify_test( + ".foo { color: color(from #808080 srgb-linear r g b) }", + ".foo{color:color(srgb-linear .215861 .215861 .215861)}", + ); + }); + }); + describe("edge cases", () => { describe("invalid gradient", () => { cssTest( From 7284504c6c888ae1038bd15dd80de66f60d08edd Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:28:40 +0000 Subject: [PATCH 2/9] css: interpolate color-mix(in xyz-d50) in xyz-d50 The xyz-d50 arm of parse_color_mix interpolated in XYZd65 and so returned a color(xyz ...) value; a none channel on an xyz-d50 operand was also resolved to 0 during that conversion instead of being filled from the other operand. lightningcss interpolates in xyz-d50. --- src/css/values/color.rs | 3 +-- test/js/bun/css/color.test.ts | 10 ++++++++++ test/js/bun/css/css.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/css/values/color.rs b/src/css/values/color.rs index 8fd4d187708f..f4cf45c6a36d 100644 --- a/src/css/values/color.rs +++ b/src/css/values/color.rs @@ -2455,9 +2455,8 @@ pub(crate) fn parse_color_mix(input: &mut css::Parser) -> CssResult { ColorSpaceName::Xyz | ColorSpaceName::XyzD65 => { first_color.interpolate::(p1, &second_color, p2, hue_method) } - // Intentionally XYZd65 for xyz-d50 too (sic) — kept for behavioral compatibility. ColorSpaceName::XyzD50 => { - first_color.interpolate::(p1, &second_color, p2, hue_method) + first_color.interpolate::(p1, &second_color, p2, hue_method) } }; diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index eb903c5f1e02..c53335e159ee 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -380,6 +380,16 @@ describe("color() predefined color spaces", () => { expect(color("color(from red srgb-linear r g b)", "css")).toBe("color(srgb-linear 1 0 0)"); expect(color("color(from red srgb-linear g g r)", "css")).toBe("color(srgb-linear 0 0 1)"); }); + + test("color-mix(in xyz-d50) stays in xyz-d50", () => { + expect(color("color-mix(in xyz-d50, color(xyz-d50 .1 .2 .3), color(xyz-d50 .3 .2 .1))", "css")).toBe( + "color(xyz-d50 .2 .2 .2)", + ); + expect(color("color-mix(in xyz-d50, color(xyz-d50 none .2 .3), color(xyz-d50 .3 .2 .1))", "css")).toBe( + "color(xyz-d50 .3 .2 .2)", + ); + expect(color("color-mix(in xyz, color(xyz .1 .2 .3), color(xyz .3 .2 .1))", "css")).toBe("color(xyz .2 .2 .2)"); + }); }); // 2^24 color() calls take minutes on debug builds (past the per-test timeout) and dominate diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 159a99e8788f..14c47afecf72 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7810,6 +7810,29 @@ describe("css tests", () => { }); }); + describe("color-mix()", () => { + // `in xyz-d50` interpolates in, and yields a color in, xyz-d50; it used to be + // routed through xyz-d65. Expected values are lightningcss output. + minify_test( + ".foo { color: color-mix(in xyz-d50, color(xyz-d50 .1 .2 .3), color(xyz-d50 .3 .2 .1)) }", + ".foo{color:color(xyz-d50 .2 .2 .2)}", + ); + minify_test( + ".foo { color: color-mix(in xyz-d50, color(xyz-d50 none .2 .3), color(xyz-d50 .3 .2 .1)) }", + ".foo{color:color(xyz-d50 .3 .2 .2)}", + ); + minify_test( + ".foo { color: color-mix(in xyz-d50, red, blue) }", + ".foo{color:color(xyz-d50 .289572 .141556 .364012)}", + ); + minify_test( + ".foo { color: color-mix(in xyz-d50, rgb(255 0 0 / .5), blue) }", + ".foo{color:color(xyz-d50 .240996 .114718 .480098/.75098)}", + ); + minify_test(".foo { color: color-mix(in xyz-d65, red, blue) }", ".foo{color:color(xyz .296436 .142416 .484931)}"); + minify_test(".foo { color: color-mix(in xyz, red, blue) }", ".foo{color:color(xyz .296436 .142416 .484931)}"); + }); + describe("edge cases", () => { describe("invalid gradient", () => { cssTest( From b33428612e3d09191eaff7af15d1716027a3e7b5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:07:09 +0000 Subject: [PATCH 3/9] ci: retrigger From e2f577f36bdba7b35af5dbb308e8e9b484e56c2c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:13:36 +0000 Subject: [PATCH 4/9] css: cover the alpha slot, light-dark() origins and fallbacks in the color() tests --- test/js/bun/css/color.test.ts | 3 +++ test/js/bun/css/css.test.ts | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index c53335e159ee..0154cf6701d8 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -379,6 +379,9 @@ describe("color() predefined color spaces", () => { test("srgb-linear relative colors can reference r", () => { expect(color("color(from red srgb-linear r g b)", "css")).toBe("color(srgb-linear 1 0 0)"); expect(color("color(from red srgb-linear g g r)", "css")).toBe("color(srgb-linear 0 0 1)"); + expect(color("color(from #808080 srgb-linear r g b / r)", "css")).toBe( + "color(srgb-linear .215861 .215861 .215861 / .215861)", + ); }); test("color-mix(in xyz-d50) stays in xyz-d50", () => { diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 14c47afecf72..05a82963c3ee 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7737,6 +7737,14 @@ describe("css tests", () => { ".foo { color: color(from #c86432 a98-rgb r g b / 50%) }", ".foo{color:color(a98-rgb .695066 .391898 .220089/.5)}", ); + minify_test( + ".foo { color: color(from #c86432 a98-rgb r g b / g) }", + ".foo{color:color(a98-rgb .695066 .391898 .220089/.391898)}", + ); + minify_test( + ".foo { color: color(from light-dark(color(a98-rgb .5 .25 .125), color(a98-rgb .1 .2 .3)) a98-rgb b g r) }", + ".foo{color:light-dark(color(a98-rgb .125 .25 .5),color(a98-rgb .3 .2 .1))}", + ); minify_test(".foo { color: rgb(from color(a98-rgb .5 .25 .125) r g b) }", ".foo{color:#923e17}"); minify_test( ".foo { color: color(from color(a98-rgb .5 .25 .125) srgb r g b) }", @@ -7807,6 +7815,35 @@ describe("css tests", () => { ".foo { color: color(from #808080 srgb-linear r g b) }", ".foo{color:color(srgb-linear .215861 .215861 .215861)}", ); + minify_test( + ".foo { color: color(from #808080 srgb-linear r g b / r) }", + ".foo{color:color(srgb-linear .215861 .215861 .215861/.215861)}", + ); + minify_test( + ".foo { color: color(from light-dark(red, blue) srgb-linear r g b) }", + ".foo{color:light-dark(color(srgb-linear 1 0 0),color(srgb-linear 0 0 1))}", + ); + + prefix_test( + ".foo { color: color(from red srgb-linear r g b) }", + indoc` + .foo { + color: red; + color: color(srgb-linear 1 0 0); + } + `, + { chrome: 90 << 16 }, + ); + prefix_test( + ".foo { color: color(from #808080 srgb-linear r g b) }", + indoc` + .foo { + color: gray; + color: color(srgb-linear .215861 .215861 .215861); + } + `, + { chrome: 90 << 16 }, + ); }); }); From 6c55c191deab7fef2a94441e9ac0fd9ee2f7c842 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:18:33 +0000 Subject: [PATCH 5/9] css: cover none on either and both operands of color-mix(in xyz-d50) --- test/js/bun/css/css.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 05a82963c3ee..aa1d4b6ca788 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7858,6 +7858,18 @@ describe("css tests", () => { ".foo { color: color-mix(in xyz-d50, color(xyz-d50 none .2 .3), color(xyz-d50 .3 .2 .1)) }", ".foo{color:color(xyz-d50 .3 .2 .2)}", ); + minify_test( + ".foo { color: color-mix(in xyz-d50, color(xyz-d50 .1 .2 .3), color(xyz-d50 none .2 .1)) }", + ".foo{color:color(xyz-d50 .1 .2 .2)}", + ); + minify_test( + ".foo { color: color-mix(in xyz-d50, color(xyz-d50 none .2 .3), color(xyz-d50 none .2 .1)) }", + ".foo{color:color(xyz-d50 none .2 .2)}", + ); + minify_test( + ".foo { color: color-mix(in xyz, color(xyz .1 .2 .3), color(xyz none .2 .1)) }", + ".foo{color:color(xyz .1 .2 .2)}", + ); minify_test( ".foo { color: color-mix(in xyz-d50, red, blue) }", ".foo{color:color(xyz-d50 .289572 .141556 .364012)}", From 89944c108fc247acf825bd99e6fac31d8cbd0202 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:24:22 +0000 Subject: [PATCH 6/9] css: pin that a99-rgb is no longer accepted; correct the color() fallback example in the docs --- docs/bundler/css.mdx | 8 +++++--- test/js/bun/css/color.test.ts | 6 ++++++ test/js/bun/css/css.test.ts | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/bundler/css.mdx b/docs/bundler/css.mdx index 809081234796..8f2613d2c944 100644 --- a/docs/bundler/css.mdx +++ b/docs/bundler/css.mdx @@ -194,16 +194,18 @@ The `color()` function specifies colors in predefined color spaces beyond tradit } ``` -For browsers that don't support these color spaces, Bun's CSS bundler adds RGB fallbacks: +For browsers that don't support these color spaces, Bun's CSS bundler adds fallbacks in more widely supported ones: ```css title="styles.css" icon="file-code" .vivid-element { /* RGB fallback first for maximum compatibility */ - color: #fa1a4c; + color: #ff1f51; /* Keep original for browsers that support it */ color: color(display-p3 1 0.1 0.3); - background-color: #6a805d; + background-color: #6a815c; + /* display-p3 is more widely supported than the other color() spaces */ + background-color: color(display-p3 0.431961 0.501334 0.375499); background-color: color(a98-rgb 0.44 0.5 0.37); } ``` diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index 0154cf6701d8..72566e524614 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -367,6 +367,12 @@ describe("color() predefined color spaces", () => { ); }); + test("an unknown color space is not a color", () => { + // a99-rgb is the misspelling the parser used to accept in place of a98-rgb. + expect(color("color(a99-rgb 1 0 0)", "css")).toBeNull(); + expect(color("color(a98rgb 1 0 0)", "css")).toBeNull(); + }); + test("a98-rgb converts to sRGB", () => { expect(color("rgb(from color(a98-rgb .5 .25 .125) r g b)", "hex")).toBe("#923e17"); expect(color("color-mix(in srgb, color(a98-rgb .5 .25 .125), white)", "hex")).toBe("#c89e8b"); diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index aa1d4b6ca788..385540ae8760 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7729,6 +7729,9 @@ describe("css tests", () => { describe("a98-rgb", () => { minify_test(".foo { color: color(A98-RGB 0 1 0 / 50%) }", ".foo{color:color(a98-rgb 0 1 0/.5)}"); minify_test(".foo { color: color(a98-rgb 100% 50% 0%) }", ".foo{color:color(a98-rgb 1 .5 0)}"); + // The misspelling the parser used to accept (and rewrite to a98-rgb) is an + // unknown color space like any other. + minify_test(".foo { color: color(a99-rgb 1 0 0) }", ".foo{color:color(a99-rgb 1 0 0)}"); minify_test( ".foo { color: color(from #c86432 a98-rgb r g b) }", ".foo{color:color(a98-rgb .695066 .391898 .220089)}", From c4b9e4def9e943a80a6881e2126b1bde13170079 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 18:43:33 +0000 Subject: [PATCH 7/9] css: cover the a98-rgb gamma curve, percentage channels and a color()-capable target Folds in the a98-rgb cases from #40725 that this branch did not have: the a98-rgb to xyz conversion applies the 563/256 power curve, percentage channels through Bun.color(), and a target that supports color() gets no fallback. --- test/js/bun/css/color.test.ts | 22 ++++++++++++++++++++++ test/js/bun/css/css.test.ts | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index 72566e524614..9e5e7fd9a6ad 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -382,6 +382,10 @@ describe("color() predefined color spaces", () => { expect(color("color(from #c86432 a98-rgb r g b)", "css")).toBe("color(a98-rgb .695066 .391898 .220089)"); }); + test("a98-rgb channels accept percentages", () => { + expect(color("color(A98-RGB 100% 50% 0%)", "css")).toBe("color(a98-rgb 1 .5 0)"); + }); + test("srgb-linear relative colors can reference r", () => { expect(color("color(from red srgb-linear r g b)", "css")).toBe("color(srgb-linear 1 0 0)"); expect(color("color(from red srgb-linear g g r)", "css")).toBe("color(srgb-linear 0 0 1)"); @@ -817,4 +821,22 @@ describe("conversions between color spaces", () => { 4, ); }); + + test("a98-rgb to xyz applies the 563/256 gamma before the matrix", () => { + // XYZ of the a98-rgb primaries, i.e. the columns of the matrix in + // https://www.w3.org/TR/css-color-4/#color-conversion-code. a98-rgb uses a + // pure power curve instead of the sRGB piecewise transfer function. + const red = [0.576669, 0.297345, 0.027031]; + const green = [0.185558, 0.627364, 0.070689]; + const blue = [0.188229, 0.075291, 0.991338]; + const g = 0.5 ** (563 / 256); + const b = 0.002 ** (563 / 256); + const out = same("xyz", "color(a98-rgb 1 0.5 0.002)"); + expect(out).toStartWith("color(xyz "); + expectChannels( + out, + [0, 1, 2].map(i => red[i] + g * green[i] + b * blue[i]), + 4, + ); + }); }); diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 385540ae8760..5475ff5c8b74 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7780,6 +7780,16 @@ describe("css tests", () => { `, { chrome: 87 << 16, safari: 14 << 16 }, ); + // A target that supports color() gets no fallback. + prefix_test( + ".foo { color: color(a98-rgb 1 0 0) }", + indoc` + .foo { + color: color(a98-rgb 1 0 0); + } + `, + { chrome: 111 << 16 }, + ); prefix_test( ".foo { color: color(from #c86432 a98-rgb r g b / 50%) }", indoc` From dd58ba62c7424439514294fd4a57b31ad7265eb6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 19:00:15 +0000 Subject: [PATCH 8/9] ci: retrigger From 4715a4f995cfcf01ee77a376d5b1786330190ddd Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:12:01 +0000 Subject: [PATCH 9/9] css: port the lightningcss relative color() and color-mix() loops for the predefined spaces Replaces the hand-written same-space cases with the two upstream loops that exercise the three fixes: test_relative_color over srgb, srgb-linear, a98-rgb, rec2020 and prophoto-rgb (68 cases each), and test_color_mix over xyz, xyz-d50 and xyz-d65 (30 cases each). Expected values are lightningcss 1.30.2 output. On the released bun 174 cases fail, all a98-rgb, srgb-linear r, or xyz-d50. The docs example keeps only the RGB fallback. The display-p3 intermediate it showed depends on Safari 14 being in the default browser targets. --- docs/bundler/css.mdx | 4 +- test/js/bun/css/css.test.ts | 261 +++++++++++++++++++++++++++++++----- 2 files changed, 231 insertions(+), 34 deletions(-) diff --git a/docs/bundler/css.mdx b/docs/bundler/css.mdx index 8f2613d2c944..e6b59f1de5ff 100644 --- a/docs/bundler/css.mdx +++ b/docs/bundler/css.mdx @@ -194,7 +194,7 @@ The `color()` function specifies colors in predefined color spaces beyond tradit } ``` -For browsers that don't support these color spaces, Bun's CSS bundler adds fallbacks in more widely supported ones: +For browsers that don't support these color spaces, Bun's CSS bundler adds RGB fallbacks: ```css title="styles.css" icon="file-code" .vivid-element { @@ -204,8 +204,6 @@ For browsers that don't support these color spaces, Bun's CSS bundler adds fallb color: color(display-p3 1 0.1 0.3); background-color: #6a815c; - /* display-p3 is more widely supported than the other color() spaces */ - background-color: color(display-p3 0.431961 0.501334 0.375499); background-color: color(a98-rgb 0.44 0.5 0.37); } ``` diff --git a/test/js/bun/css/css.test.ts b/test/js/bun/css/css.test.ts index 5475ff5c8b74..52ac873e6f1d 100644 --- a/test/js/bun/css/css.test.ts +++ b/test/js/bun/css/css.test.ts @@ -7703,10 +7703,9 @@ describe("css tests", () => { describe("color()", () => { // Every predefined color space, in the absolute form and as the target of a - // relative color that references all three channels. Expected values are - // lightningcss output. A color() that fails to parse is kept as an unparsed - // token stream, so the alpha percentage and the channel keywords would be - // printed verbatim instead of being folded. + // relative color that references all three channels. A color() that fails to + // parse is kept as an unparsed token stream, so the alpha percentage and the + // channel keywords would be printed verbatim instead of being folded. const predefined: [space: string, channels: string, printed?: string][] = [ ["srgb", "b g r"], ["srgb-linear", "b g r"], @@ -7726,6 +7725,108 @@ describe("css tests", () => { ); } + // lightningcss 1.30.2 `test_relative_color`, the loop over the RGB-like predefined + // spaces (src/lib.rs, `for color_space in &["srgb", "srgb-linear", "a98-rgb", "rec2020", + // "prophoto-rgb"]`). `{}` stands for the color space. Like the upstream `test()` helper, + // the expected color literal is normalized through the printer before the comparison. + describe("relative colors in the RGB-like predefined spaces", () => { + function relative_color_test(input: string, expected: string) { + const source = `.foo { color: ${input} }`; + test(source, () => { + const normalized = minify_test_with_options(`.foo { color: ${expected} }`, ""); + expect(minify_test_with_options(source, "")).toBe(normalized); + }); + } + + const cases: [input: string, expected: string][] = [ + // No modifications. + ["color(from color({} 0.7 0.5 0.3) {} r g b)", "color({} 0.7 0.5 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} r g b / alpha)", "color({} 0.7 0.5 0.3)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g b)", "color({} 0.7 0.5 0.3)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / alpha)", "color({} 0.7 0.5 0.3 / 0.4)"], + // Nested relative colors. + ["color(from color(from color({} 0.7 0.5 0.3) {} r g b) {} r g b)", "color({} 0.7 0.5 0.3)"], + // Replacement with 0. + ["color(from color({} 0.7 0.5 0.3) {} 0 0 0)", "color({} 0 0 0)"], + ["color(from color({} 0.7 0.5 0.3) {} 0 0 0 / 0)", "color({} 0 0 0 / 0)"], + ["color(from color({} 0.7 0.5 0.3) {} 0 g b / alpha)", "color({} 0 0.5 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} r 0 b / alpha)", "color({} 0.7 0 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} r g 0 / alpha)", "color({} 0.7 0.5 0)"], + ["color(from color({} 0.7 0.5 0.3) {} r g b / 0)", "color({} 0.7 0.5 0.3 / 0)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} 0 g b / alpha)", "color({} 0 0.5 0.3 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r 0 b / alpha)", "color({} 0.7 0 0.3 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g 0 / alpha)", "color({} 0.7 0.5 0 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / 0)", "color({} 0.7 0.5 0.3 / 0)"], + // Replacement with a constant. + ["color(from color({} 0.7 0.5 0.3) {} 0.2 g b / alpha)", "color({} 0.2 0.5 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} 20% g b / alpha)", "color({} 0.2 0.5 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} r 0.2 b / alpha)", "color({} 0.7 0.2 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} r 20% b / alpha)", "color({} 0.7 0.2 0.3)"], + ["color(from color({} 0.7 0.5 0.3) {} r g 0.2 / alpha)", "color({} 0.7 0.5 0.2)"], + ["color(from color({} 0.7 0.5 0.3) {} r g 20% / alpha)", "color({} 0.7 0.5 0.2)"], + ["color(from color({} 0.7 0.5 0.3) {} r g b / 0.2)", "color({} 0.7 0.5 0.3 / 0.2)"], + ["color(from color({} 0.7 0.5 0.3) {} r g b / 20%)", "color({} 0.7 0.5 0.3 / 0.2)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} 0.2 g b / alpha)", "color({} 0.2 0.5 0.3 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} 20% g b / alpha)", "color({} 0.2 0.5 0.3 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r 0.2 b / alpha)", "color({} 0.7 0.2 0.3 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r 20% b / alpha)", "color({} 0.7 0.2 0.3 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g 0.2 / alpha)", "color({} 0.7 0.5 0.2 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g 20% / alpha)", "color({} 0.7 0.5 0.2 / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / 0.2)", "color({} 0.7 0.5 0.3 / 0.2)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / 20%)", "color({} 0.7 0.5 0.3 / 0.2)"], + ["color(from color({} 0.7 0.5 0.3) {} 2 3 4)", "color({} 2 3 4)"], + ["color(from color({} 0.7 0.5 0.3) {} 2 3 4 / 5)", "color({} 2 3 4)"], + ["color(from color({} 0.7 0.5 0.3) {} -2 -3 -4)", "color({} -2 -3 -4)"], + ["color(from color({} 0.7 0.5 0.3) {} -2 -3 -4 / -5)", "color({} -2 -3 -4 / 0)"], + ["color(from color({} 0.7 0.5 0.3) {} 200% 300% 400%)", "color({} 2 3 4)"], + ["color(from color({} 0.7 0.5 0.3) {} 200% 300% 400% / 500%)", "color({} 2 3 4)"], + ["color(from color({} 0.7 0.5 0.3) {} -200% -300% -400%)", "color({} -2 -3 -4)"], + ["color(from color({} 0.7 0.5 0.3) {} -200% -300% -400% / -500%)", "color({} -2 -3 -4 / 0)"], + // Valid permutations (the types match). + ["color(from color({} 0.7 0.5 0.3) {} g b r)", "color({} 0.5 0.3 0.7)"], + ["color(from color({} 0.7 0.5 0.3) {} b alpha r / g)", "color({} 0.3 1 0.7 / 0.5)"], + ["color(from color({} 0.7 0.5 0.3) {} r r r / r)", "color({} 0.7 0.7 0.7 / 0.7)"], + ["color(from color({} 0.7 0.5 0.3) {} alpha alpha alpha / alpha)", "color({} 1 1 1)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} g b r)", "color({} 0.5 0.3 0.7)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} b alpha r / g)", "color({} 0.3 0.4 0.7 / 0.5)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r r r / r)", "color({} 0.7 0.7 0.7 / 0.7)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} alpha alpha alpha / alpha)", "color({} 0.4 0.4 0.4 / 0.4)"], + // Out of gamut components. + ["color(from color({} 1.7 1.5 1.3) {} r g b)", "color({} 1.7 1.5 1.3)"], + ["color(from color({} 1.7 1.5 1.3) {} r g b / alpha)", "color({} 1.7 1.5 1.3)"], + ["color(from color({} 1.7 1.5 1.3 / 140%) {} r g b)", "color({} 1.7 1.5 1.3)"], + ["color(from color({} 1.7 1.5 1.3 / 140%) {} r g b / alpha)", "color({} 1.7 1.5 1.3)"], + ["color(from color({} -0.7 -0.5 -0.3) {} r g b)", "color({} -0.7 -0.5 -0.3)"], + ["color(from color({} -0.7 -0.5 -0.3) {} r g b / alpha)", "color({} -0.7 -0.5 -0.3)"], + ["color(from color({} -0.7 -0.5 -0.3 / -40%) {} r g b)", "color({} -0.7 -0.5 -0.3)"], + ["color(from color({} -0.7 -0.5 -0.3 / -40%) {} r g b / alpha)", "color({} -0.7 -0.5 -0.3 / 0)"], + // calc(). + ["color(from color({} 0.7 0.5 0.3) {} calc(r) calc(g) calc(b))", "color({} 0.7 0.5 0.3)"], + [ + "color(from color({} 0.7 0.5 0.3 / 40%) {} calc(r) calc(g) calc(b) / calc(alpha))", + "color({} 0.7 0.5 0.3 / 0.4)", + ], + // none. + ["color(from color({} 0.7 0.5 0.3) {} none none none)", "color({} none none none)"], + ["color(from color({} 0.7 0.5 0.3) {} none none none / none)", "color({} none none none / none)"], + ["color(from color({} 0.7 0.5 0.3) {} r g none)", "color({} 0.7 0.5 none)"], + ["color(from color({} 0.7 0.5 0.3) {} r g none / alpha)", "color({} 0.7 0.5 none)"], + ["color(from color({} 0.7 0.5 0.3) {} r g b / none)", "color({} 0.7 0.5 0.3 / none)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g none / alpha)", "color({} 0.7 0.5 none / 0.4)"], + ["color(from color({} 0.7 0.5 0.3 / 40%) {} r g b / none)", "color({} 0.7 0.5 0.3 / none)"], + // A none channel of the origin is 0 when a keyword references it. + ["color(from color({} none none none) {} r g b)", "color({} 0 0 0)"], + ["color(from color({} none none none / none) {} r g b / alpha)", "color({} 0 0 0 / 0)"], + ["color(from color({} 0.7 none 0.3) {} r g b)", "color({} 0.7 0 0.3)"], + ["color(from color({} 0.7 0.5 0.3 / none) {} r g b / alpha)", "color({} 0.7 0.5 0.3 / 0)"], + ]; + for (const space of ["srgb", "srgb-linear", "a98-rgb", "rec2020", "prophoto-rgb"]) { + for (const [input, expected] of cases) { + relative_color_test(input.replaceAll("{}", space), expected.replaceAll("{}", space)); + } + } + }); + describe("a98-rgb", () => { minify_test(".foo { color: color(A98-RGB 0 1 0 / 50%) }", ".foo{color:color(a98-rgb 0 1 0/.5)}"); minify_test(".foo { color: color(a98-rgb 100% 50% 0%) }", ".foo{color:color(a98-rgb 1 .5 0)}"); @@ -7813,17 +7914,14 @@ describe("css tests", () => { }); describe("srgb-linear", () => { - // The `r` channel keyword of an srgb-linear relative color. + // The `r` channel keyword of an srgb-linear relative color, with an origin + // that has to be converted into srgb-linear first. minify_test(".foo { color: color(from red srgb-linear r g b) }", ".foo{color:color(srgb-linear 1 0 0)}"); minify_test(".foo { color: color(from red srgb-linear g g r) }", ".foo{color:color(srgb-linear 0 0 1)}"); minify_test( ".foo { color: color(from color(srgb-linear .5 .25 .125) srgb-linear calc(r / 2) g b) }", ".foo{color:color(srgb-linear .25 .25 .125)}", ); - minify_test( - ".foo { color: color(from color(srgb-linear .5 .25 .125) srgb-linear r g b / 50%) }", - ".foo{color:color(srgb-linear .5 .25 .125/.5)}", - ); minify_test( ".foo { color: color(from #808080 srgb-linear r g b) }", ".foo{color:color(srgb-linear .215861 .215861 .215861)}", @@ -7861,28 +7959,129 @@ describe("css tests", () => { }); describe("color-mix()", () => { - // `in xyz-d50` interpolates in, and yields a color in, xyz-d50; it used to be - // routed through xyz-d65. Expected values are lightningcss output. - minify_test( - ".foo { color: color-mix(in xyz-d50, color(xyz-d50 .1 .2 .3), color(xyz-d50 .3 .2 .1)) }", - ".foo{color:color(xyz-d50 .2 .2 .2)}", - ); - minify_test( - ".foo { color: color-mix(in xyz-d50, color(xyz-d50 none .2 .3), color(xyz-d50 .3 .2 .1)) }", - ".foo{color:color(xyz-d50 .3 .2 .2)}", - ); - minify_test( - ".foo { color: color-mix(in xyz-d50, color(xyz-d50 .1 .2 .3), color(xyz-d50 none .2 .1)) }", - ".foo{color:color(xyz-d50 .1 .2 .2)}", - ); - minify_test( - ".foo { color: color-mix(in xyz-d50, color(xyz-d50 none .2 .3), color(xyz-d50 none .2 .1)) }", - ".foo{color:color(xyz-d50 none .2 .2)}", - ); - minify_test( - ".foo { color: color-mix(in xyz, color(xyz .1 .2 .3), color(xyz none .2 .1)) }", - ".foo{color:color(xyz .1 .2 .2)}", - ); + // lightningcss 1.30.2 `test_color_mix`, the loop over the XYZ spaces (src/lib.rs, + // `for color_space in [/*"srgb", */ "srgb-linear", "xyz", "xyz-d50", "xyz-d65"]`, itself + // converted from the web platform tests). `{}` stands for the interpolation space in + // the input and for the result space in the output: `in xyz-d65` yields `color(xyz ...)`. + // `in xyz-d50` interpolates in, and yields a color in, xyz-d50; it used to be routed + // through xyz-d65. srgb-linear is left out: 8 of its cases mix `none` or out-of-range + // channels, and `CssColor::interpolate` gamut-maps and drops the powerless components + // of the wrong operand (its `converted_*` flags are inverted), which #38508 fixes. + const cases: [input: string, expected: string][] = [ + [".foo { color: color-mix(in {}, color({} .1 .2 .3), color({} .5 .6 .7)) }", ".foo{color:color({} .3 .4 .5)}"], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3) 25%, color({} .5 .6 .7)) }", + ".foo{color:color({} .4 .5 .6)}", + ], + [ + ".foo { color: color-mix(in {}, 25% color({} .1 .2 .3), color({} .5 .6 .7)) }", + ".foo{color:color({} .4 .5 .6)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3), color({} .5 .6 .7) 25%) }", + ".foo{color:color({} .2 .3 .4)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3), 25% color({} .5 .6 .7)) }", + ".foo{color:color({} .2 .3 .4)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3) 25%, color({} .5 .6 .7) 75%) }", + ".foo{color:color({} .4 .5 .6)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3) 30%, color({} .5 .6 .7) 90%) }", + ".foo{color:color({} .4 .5 .6)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3) 12.5%, color({} .5 .6 .7) 37.5%) }", + ".foo{color:color({} .4 .5 .6/.5)}", + ], + [".foo { color: color-mix(in {}, color({} .1 .2 .3) 0%, color({} .5 .6 .7)) }", ".foo{color:color({} .5 .6 .7)}"], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .5), color({} .5 .6 .7 / .8)) }", + ".foo{color:color({} .346154 .446154 .546154/.65)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4) 25%, color({} .5 .6 .7 / .8)) }", + ".foo{color:color({} .442857 .542857 .642857/.7)}", + ], + [ + ".foo { color: color-mix(in {}, 25% color({} .1 .2 .3 / .4), color({} .5 .6 .7 / .8)) }", + ".foo{color:color({} .442857 .542857 .642857/.7)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4), color({} .5 .6 .7 / .8) 25%) }", + ".foo{color:color({} .26 .36 .46/.5)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4), 25% color({} .5 .6 .7 / .8)) }", + ".foo{color:color({} .26 .36 .46/.5)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4) 25%, color({} .5 .6 .7 / .8) 75%) }", + ".foo{color:color({} .442857 .542857 .642857/.7)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4) 30%, color({} .5 .6 .7 / .8) 90%) }", + ".foo{color:color({} .442857 .542857 .642857/.7)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4) 12.5%, color({} .5 .6 .7 / .8) 37.5%) }", + ".foo{color:color({} .442857 .542857 .642857/.35)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / .4) 0%, color({} .5 .6 .7 / .8)) }", + ".foo{color:color({} .5 .6 .7/.8)}", + ], + [".foo { color: color-mix(in {}, color({} 2 3 4 / 5), color({} 4 6 8 / 10)) }", ".foo{color:color({} 3 4.5 6)}"], + [".foo { color: color-mix(in {}, color({} -2 -3 -4), color({} -4 -6 -8)) }", ".foo{color:color({} -3 -4.5 -6)}"], + [ + ".foo { color: color-mix(in {}, color({} -2 -3 -4 / -5), color({} -4 -6 -8 / -10)) }", + ".foo{color:color({} 0 0 0/0)}", + ], + [ + ".foo { color: color-mix(in {}, color({} none none none), color({} none none none)) }", + ".foo{color:color({} none none none)}", + ], + [ + ".foo { color: color-mix(in {}, color({} none none none), color({} .5 .6 .7)) }", + ".foo{color:color({} .5 .6 .7)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3), color({} none none none)) }", + ".foo{color:color({} .1 .2 .3)}", + ], + [".foo { color: color-mix(in {}, color({} .1 .2 none), color({} .5 .6 .7)) }", ".foo{color:color({} .3 .4 .7)}"], + [".foo { color: color-mix(in {}, color({} .1 .2 .3), color({} .5 .6 none)) }", ".foo{color:color({} .3 .4 .3)}"], + [ + ".foo { color: color-mix(in {}, color({} none .2 .3), color({} .5 none .7)) }", + ".foo{color:color({} .5 .2 .5)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / none), color({} .5 .6 .7)) }", + ".foo{color:color({} .3 .4 .5)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / none), color({} .5 .6 .7 / 0.5)) }", + ".foo{color:color({} .3 .4 .5/.5)}", + ], + [ + ".foo { color: color-mix(in {}, color({} .1 .2 .3 / none), color({} .5 .6 .7 / none)) }", + ".foo{color:color({} .3 .4 .5/none)}", + ], + ]; + for (const [space, result] of [ + ["xyz", "xyz"], + ["xyz-d50", "xyz-d50"], + ["xyz-d65", "xyz"], + ]) { + for (const [input, expected] of cases) { + minify_test(input.replaceAll("{}", space), expected.replaceAll("{}", result)); + } + } + + // Operands that have to be converted into the interpolation space first. minify_test( ".foo { color: color-mix(in xyz-d50, red, blue) }", ".foo{color:color(xyz-d50 .289572 .141556 .364012)}",