Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/css-color-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes to CSS Color Parser

### Unreleased (patch)

- Fix handling of `none` in some edge cases

### 4.1.9

_June 25, 2026_
Expand Down
2 changes: 1 addition & 1 deletion packages/css-color-parser/dist/index.mjs

Large diffs are not rendered by default.

523 changes: 318 additions & 205 deletions packages/css-color-parser/src/color-data.ts

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions packages/css-color-parser/src/functions/color-mix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Color } from '@csstools/color-helpers';
import { ColorNotation } from '../color-notation';
import { isTokenIdent, isTokenNumeric, isTokenPercentage } from '@csstools/css-tokenizer';
import { calcFromComponentValues } from '@csstools/css-calc';
import { colorDataTo, SyntaxFlag } from '../color-data';
import { colorDataToForInterpolation, SyntaxFlag } from '../color-data';
import { isFunctionNode, isTokenNode, isWhiteSpaceOrCommentNode } from '@csstools/css-parser-algorithms';
import { toLowerCaseAZ } from '../util/to-lower-case-a-z';
import { mathFunctionNames } from '@csstools/css-calc';
Expand Down Expand Up @@ -279,7 +279,7 @@ function colorMixRectangular(colorSpace: string, items: Array<ColorMixEntry> | f

// 3. If items is length 1, set color to the color of that sole item, converted to the specified interpolation <color-space>.
if (normalizedItems.length === 1) {
const color = colorDataTo(normalizedItems[0].color, outputColorNotation);
const color = colorDataToForInterpolation(normalizedItems[0].color, outputColorNotation);
color.colorNotation = outputColorNotation;
color.syntaxFlags.add(SyntaxFlag.ColorMixVariadic);
color.syntaxFlags.add(SyntaxFlag.ColorMix);
Expand Down Expand Up @@ -369,8 +369,8 @@ function colorMixRectangularPair(colorNotation: ColorNotation, a_color: ColorDat
a_alpha = Number.isNaN(a_alpha) ? b_alpha : a_alpha;
b_alpha = Number.isNaN(b_alpha) ? a_alpha : b_alpha;

const a_channels = colorDataTo(a_color, colorNotation).channels;
const b_channels = colorDataTo(b_color, colorNotation).channels;
const a_channels = colorDataToForInterpolation(a_color, colorNotation).channels;
const b_channels = colorDataToForInterpolation(b_color, colorNotation).channels;

a_channels[0] = fillInMissingComponent(a_channels[0], b_channels[0]);
b_channels[0] = fillInMissingComponent(b_channels[0], a_channels[0]);
Expand Down Expand Up @@ -448,7 +448,7 @@ function colorMixPolar(colorSpace: string, hueInterpolationMethod: string, items

// 3. If items is length 1, set color to the color of that sole item, converted to the specified interpolation <color-space>.
if (normalizedItems.length === 1) {
const color = colorDataTo(normalizedItems[0].color, outputColorNotation);
const color = colorDataToForInterpolation(normalizedItems[0].color, outputColorNotation);
color.colorNotation = outputColorNotation;
color.syntaxFlags.add(SyntaxFlag.ColorMixVariadic);
color.syntaxFlags.add(SyntaxFlag.ColorMix);
Expand Down Expand Up @@ -547,8 +547,8 @@ function colorMixPolarPair(colorNotation: ColorNotation, hueInterpolationMethod:
a_alpha = Number.isNaN(a_alpha) ? b_alpha : a_alpha;
b_alpha = Number.isNaN(b_alpha) ? a_alpha : b_alpha;

const a_channels = colorDataTo(a_color, colorNotation).channels;
const b_channels = colorDataTo(b_color, colorNotation).channels;
const a_channels = colorDataToForInterpolation(a_color, colorNotation).channels;
const b_channels = colorDataToForInterpolation(b_color, colorNotation).channels;

switch (colorNotation) {
case ColorNotation.HSL:
Expand Down
7 changes: 2 additions & 5 deletions packages/css-color-parser/src/functions/color.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ColorData} from '../color-data';
import { SyntaxFlag, colorDataTo, noneToZeroInRelativeColorDataChannels, normalizeRelativeColorDataChannels } from '../color-data';
import { SyntaxFlag, colorDataToForRelativeColorSyntax, noneToZeroInRelativeColorDataChannels, normalizeRelativeColorDataChannels } from '../color-data';
import type { ColorParser } from '../color-parser';
import { ColorNotation } from '../color-notation';
import type { ComponentValue, FunctionNode} from '@csstools/css-parser-algorithms';
Expand Down Expand Up @@ -106,10 +106,7 @@ export function color(colorFunctionNode: FunctionNode, colorParser: ColorParser)
colorData.colorNotation = colorSpaceNameToColorNotation(colorSpace);

if (relativeToColor) {
if (relativeToColor.colorNotation !== colorData.colorNotation) {
relativeToColor = colorDataTo(relativeToColor, colorData.colorNotation);
}

relativeToColor = colorDataToForRelativeColorSyntax(relativeToColor, colorData.colorNotation);
relativeColorChannels = normalizeRelativeColorDataChannels(relativeToColor);
relativeColorChannelsWithoutNone = noneToZeroInRelativeColorDataChannels(relativeColorChannels);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { ColorData} from '../color-data';
import { noneToZeroInRelativeColorDataChannels, normalizeRelativeColorDataChannels } from '../color-data';
import { colorDataToForRelativeColorSyntax, noneToZeroInRelativeColorDataChannels, normalizeRelativeColorDataChannels } from '../color-data';
import type { ComponentValue, FunctionNode } from '@csstools/css-parser-algorithms';
import type { TokenNumber} from '@csstools/css-tokenizer';
import { isTokenDelim, isTokenIdent, isTokenNumber, isTokenNumeric } from '@csstools/css-tokenizer';
import type { ColorNotation } from '../color-notation';
import { SyntaxFlag, colorDataTo } from '../color-data';
import { SyntaxFlag } from '../color-data';
import { calcFromComponentValues } from '@csstools/css-calc';
import { isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode } from '@csstools/css-parser-algorithms';
import type { normalizeChannelValuesFn } from './normalize-channel-values';
Expand Down Expand Up @@ -122,10 +122,7 @@ export function threeChannelSpaceSeparated(

colorData.syntaxFlags.add(SyntaxFlag.RelativeColorSyntax);

if (relativeToColor.colorNotation !== colorNotation) {
relativeToColor = colorDataTo(relativeToColor, colorNotation);
}

relativeToColor = colorDataToForRelativeColorSyntax(relativeToColor, colorNotation);
relativeColorChannels = normalizeRelativeColorDataChannels(relativeToColor);
relativeColorChannelsWithoutNone = noneToZeroInRelativeColorDataChannels(relativeColorChannels);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { serialize_OKLCH_data } from '../util/serialize.mjs';

const tests = [
['color-mix(in oklch, oklch(100% 0% 60deg), oklch(50% 50% 0deg))', 'oklch(0.75 0.1 0)'],
['color-mix(in oklch, oklch(100% 0% none), oklch(50% 50% 0deg))', 'oklch(0.75 0.1 0)'],
['color-mix(in oklch, rgb(255, 255, 255), rgb(180, 6, 95))', 'oklch(0.75031 0.10016 359.858)'],
['color-mix(in lch, oklch(75% 0% 60deg), oklch(75% 50% 0deg))', 'oklch(0.74979 0.09824 0.10588)'],
['color-mix(in oklch, oklch(100% 0% none), oklch(50% 50% 0deg))', 'oklch(0.75 0.1 0)'],
['color-mix(in oklch, oklch(100% none 60deg), oklch(50% 50% 0deg))', 'oklch(0.75 0.2 30)'],
['color-mix(in oklch, oklch(100% none 60deg), oklch(50% 50% 0deg))', 'oklch(0.75 0.2 0)'],

// Analogous sets
['color-mix(in oklch, rgb(none none none), oklch(0.5 0.2 50))', 'oklch(0.5 0.2 50)'],
Expand Down
30 changes: 26 additions & 4 deletions packages/css-color-parser/test/basic/color-mix-function.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ const tests = [
['color-mix(in oklch, green 40%, oklch(0.747 0.196 322.2))', 'rgb(0, 148, 253)'],
['color-mix(in oklch, green 40%, hsl(292deg 85.71% 72.55%))', 'rgb(0, 148, 253)'],

['color-mix(in display-p3-linear, green 40%, color(srgb 0.8978862558205767 0.4885001647805462 0.9594359763905097))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(srgb-linear 0.7832360124544266 0.2035510416163499 0.9101924728483531))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(a98-rgb 0.8035122804301492 0.484896415622613 0.9440692746539695))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(prophoto-rgb 0.7596595159204217 0.4934889951894072 0.8985832663171222))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(display-p3 0.843565234 0.509345345 0.9342344435))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(rec2020 0.807076644727751 0.5627572708703388 0.9326528689276063))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(xyz-d50 0.5501693084815327 0.37536346388820246 0.6806345611398199))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(xyz-d65 0.5600582450343325 0.37782875858447507 0.904570025128693))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, color(xyz 0.5600582450343325 0.37782875858447507 0.904570025128693))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, oklch(0.747 0.196 322.2))', 'rgb(182, 126, 195)'],
['color-mix(in display-p3-linear, green 40%, hsl(292deg 85.71% 72.55%))', 'rgb(182, 126, 195)'],

['color-mix(in srgb, color(xyz 1 none 0) 30%, rgb(none 255 128))', 'rgb(255, 255, 255)'],

['color-mix(in oklab, #09232c, white 50%)', 'rgb(123, 137, 142)'],
Expand Down Expand Up @@ -102,9 +114,10 @@ const tests = [
['color-mix(in hsl increasing hue, hsl(180deg 50% 50%), hsl(180deg 50% 50%))', canonicalize('hsl(180deg 50% 50%)')],
['color-mix(in hsl decreasing hue, hsl(180deg 50% 50%), hsl(180deg 50% 50%))', canonicalize('hsl(180deg 50% 50%)')],

['color-mix(in hsl longer hue, hsl(50deg 0% 50%), hsl(50deg 0% 50%))', canonicalize('hsl(230deg 0% 50%)')],
['color-mix(in hsl, color-mix(in hsl longer hue, hsl(50deg 0% 50%), hsl(50deg 0% 50%)), hsl(180deg 100% 50%))', canonicalize('hsl(180deg 50% 50%)')],

['color-mix(in hsl, hsl(30deg 40% 80% / 25%) 0%, hsl(90deg none none / none))', canonicalize('hsl(90deg 40% 80% / 25%)')],
['color-mix(in hsl, hsl(30deg 40% 80% / 25%) 0%, hsl(90deg none none / none))', canonicalize('hsl(30deg 40% 80% / 25%)')],
['color-mix(in hwb, hwb(30deg 30% 40% / 25%) 0%, hwb(90deg none none / 0.5))', canonicalize('hwb(90deg 30% 40% / 0.5)')],
['color-mix(in hsl, hsl(from hsl(none 50% 50%) h s l), hsl(from hsl(120deg 50% 50%) h s l))', canonicalize('hsl(120deg 50% 50%)')],
['color-mix(in hsl, hsl(from hsl(0deg 50% 50%) none s l), hsl(from hsl(120deg 50% 50%) h s l))', canonicalize('hsl(120deg 50% 50%)')],
Expand Down Expand Up @@ -176,6 +189,15 @@ for (const test of tests) {
);
}

assert.deepStrictEqual(
color(parse('color-mix(in lch, lch(50% 50% 180deg), lch(100% 0% 0deg))')),
{
colorNotation: 'lch',
channels: [75, 37.5, 180],
alpha: 1,
syntaxFlags: new Set(['color-mix']),
},
);

assert.deepStrictEqual(
color(parse('color-mix(in hsl, red 0%, blue 0%)')),
Expand Down Expand Up @@ -221,7 +243,7 @@ assert.deepStrictEqual(
color(parse('oklch(from lch(100 0 0deg) l c h)')),
{
colorNotation: 'oklch',
channels: [1, 4.996003610813204e-16, Number.NaN],
channels: [1, 0, Number.NaN],
alpha: 1,
syntaxFlags: new Set(['relative-color-syntax', 'has-number-values']),
},
Expand All @@ -231,7 +253,7 @@ assert.deepStrictEqual(
color(parse('lch(from oklch(100 0 0deg) l c h)')),
{
colorNotation: 'lch',
channels: [100, 1.1102230246251565e-13, Number.NaN],
channels: [100, 0, Number.NaN],
alpha: 1,
syntaxFlags: new Set(['relative-color-syntax', 'has-number-values']),
},
Expand All @@ -241,7 +263,7 @@ assert.deepStrictEqual(
color(parse('color-mix(in oklch, lch(100 0 40deg), lch(100 0 60deg))')),
{
colorNotation: 'oklch',
channels: [1.0000000000000002, 4.996003610813204e-16, Number.NaN],
channels: [1.0000000000000002, 0, Number.NaN],
alpha: 1,
syntaxFlags: new Set(['color-mix']),
},
Expand Down
Loading
Loading