Skip to content
Merged
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
5 changes: 3 additions & 2 deletions apps/automated/src/ui/styling/style-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1831,7 +1831,7 @@ export function test_evaluateCssColorMixExpression() {
TKUnit.assertEqual(new Color('color-mix(in hsl, hsl(200 50 80), coral 80%)').toRgbString(), 'rgba(247, 103, 149, 1.00)', 'Color mix (2)');
TKUnit.assertEqual(new Color('color-mix(in srgb, plum, #f00)').toRgbString(), 'rgba(238, 80, 110, 1.00)', 'Color mix (4)');
TKUnit.assertEqual(new Color('color-mix(in lab, plum 60%, #f00 50%)').toRgbString(), 'rgba(247, 112, 125, 1.00)', 'Color mix (5)');
TKUnit.assertEqual(new Color('color-mix(in --swop5c, red, blue)').toRgbString(), 'rgba(0, 0, 255, 0.00)', 'Color mix (6)');
TKUnit.assertThrowsRegExp(() => new Color('color-mix(in --swop5c, red, blue)'), 'Color mix (6)', /^Invalid color: color-mix\(in --swop5c, red, blue\)$/);
}

export function test_nested_css_color_mix() {
Expand All @@ -1854,7 +1854,8 @@ export function test_nested_css_color_mix() {

(stack as any).style = `background-color: color-mix(in --swop5c, red, blue);`;

TKUnit.assertDeepEqual((stack.backgroundColor as Color).toRgbString(), 'rgba(0, 0, 255, 0.00)', 'Stack - backgroundColor === color-mix(in --swop5c, red, blue)');
// An unparseable color-mix() is dropped, leaving the class rule in effect.
TKUnit.assertDeepEqual((stack.backgroundColor as Color).toRgbString(), 'rgba(247, 103, 149, 1.00)', 'Stack - backgroundColor ignores color-mix(in --swop5c, red, blue)');
}

export function test_css_variables() {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/color/color-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export class ColorBase implements IColor {
this._name = arg;
this._argb = argb;
} else if (isCssColorMixExpression(lowered)) {
this._argb = argbFromColorMix(lowered);
const argb = argbFromColorMix(lowered);
if (argb === null) {
throw new Error('Invalid color: ' + arg);
}
this._argb = argb;
} else if (arg[0].charAt(0) === '#' && (arg.length === 4 || arg.length === 5 || arg.length === 7 || arg.length === 9)) {
// we dont use the regexp as it is quite slow. Instead we expect it to be a valid hex format
// strange that it would not be. And if it is not a thrown error seems best
Expand Down
14 changes: 4 additions & 10 deletions packages/core/color/color-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,14 @@ export function isCssColorMixExpression(value: string) {
return value.includes('color-mix(');
}

export function argbFromColorMix(value: string): number {
export function argbFromColorMix(value: string): number | null {
const astComponentValue = parseComponentValue(tokenize({ css: value }));
const colorData = color(astComponentValue);

let argb: number;

if (colorData) {
const serialized = serializeRGB(colorData);
argb = argbFromRgbOrRgba(serialized.toString());
} else {
argb = -1;
if (!colorData) {
return null;
}

return argb;
return argbFromRgbOrRgba(serializeRGB(colorData).toString());
}

export function fromArgbToRgba(argb: number): { a: number; r: number; g: number; b: number } {
Expand Down
26 changes: 23 additions & 3 deletions packages/core/ui/core/properties/css-variable-expression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,34 @@ describe('_evaluateCssVariableExpression', () => {
expect(evaluate('var(--missing)')).toBe('unset');
});

it('substitutes variables nested in a function', () => {
expect(evaluate('color-mix(in srgb, var(--a) 35%, transparent)', { '--a': 'blue' })).toBe('color-mix(in srgb, blue 35%, transparent)');
expect(evaluate('color-mix(in srgb, var(--missing, red) 35%, transparent)')).toBe('color-mix(in srgb, red 35%, transparent)');
});

it('resolves nested fallbacks', () => {
expect(evaluate('var(--missing, var(--b))', { '--b': 'lime' })).toBe('lime');
expect(evaluate('var(--missing, var(--missing-too, yellow))')).toBe('yellow');
expect(evaluate('var(--missing, var(--missing-too))')).toBe('unset');
});

it('ignores an unresolved variable in the fallback of a resolved one', () => {
expect(evaluate('var(--a, var(--missing))', { '--a': 'blue' })).toBe('blue');
expect(evaluate('color-mix(in srgb, var(--a, var(--missing)) 35%, transparent)', { '--a': 'blue' })).toBe('color-mix(in srgb, blue 35%, transparent)');
});

it('unsets the whole value for an unresolved variable wherever it is nested', () => {
expect(evaluate('color-mix(in srgb, var(--missing) 35%, transparent)')).toBe('unset');
expect(evaluate('color-mix(in srgb, var(--missing, var(--missing-too)) 35%, transparent)')).toBe('unset');
expect(evaluate('calc(var(--missing) + 10)')).toBe('unset');
expect(evaluate('var(--size) var(--missing)', { '--size': '4' })).toBe('unset');
});

it('unsets the whole value for a variable defined as empty', () => {
expect(evaluate('var(--empty)', { '--empty': '' })).toBe('unset');
expect(evaluate('color-mix(in srgb, var(--empty) 35%, transparent)', { '--empty': '' })).toBe('unset');
});

it('keeps parentheses inside the fallback', () => {
expect(evaluate('var(--missing, rgb(0, 0, 255))')).toBe('rgb(0, 0, 255)');
expect(evaluate('linear-gradient(var(--missing, rgb(255, 0, 0) 0%, rgb(0, 0, 255) 100%))')).toBe('linear-gradient(rgb(255, 0, 0) 0%, rgb(0, 0, 255) 100%)');
Expand All @@ -44,9 +66,7 @@ describe('_evaluateCssVariableExpression', () => {
'--to': 'rgb(246, 51, 154)',
'--to-position': '100%',
};
expect(evaluate('linear-gradient(var(--via-stops, var(--position), var(--from) var(--from-position), var(--to) var(--to-position)))', variables)).toBe(
'linear-gradient(to right in oklab, rgb(98, 96, 255) 0%, rgb(246, 51, 154) 100%)',
);
expect(evaluate('linear-gradient(var(--via-stops, var(--position), var(--from) var(--from-position), var(--to) var(--to-position)))', variables)).toBe('linear-gradient(to right in oklab, rgb(98, 96, 255) 0%, rgb(246, 51, 154) 100%)');
});

it('leaves values without variables alone', () => {
Expand Down
10 changes: 7 additions & 3 deletions packages/core/ui/core/properties/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const cssSymbolPropertyMap = {};
const CSS_VARIABLE_NAME_RE = /^--[^,\s]+?$/;
const DIP_RE = /([0-9]+(\.[0-9]+)?)dip\b/g;
const UNSET_RE = /unset/g;
/** Stands in for an unresolved var() until the whole value is known; a NUL never occurs in css. */
const UNRESOLVED_VAR = '\u0000';
const INFINITY_RE = /infinity/g;

const inheritableProperties = new Array<InheritedProperty<any, any>>();
Expand Down Expand Up @@ -246,14 +248,16 @@ export function _evaluateCssVariableExpression(view: ViewBase, cssName: string,
cssVariableValue = _evaluateCssVariableExpression(view, cssName, fallback);
}

if (!cssVariableValue) {
cssVariableValue = 'unset';
if (!cssVariableValue || cssVariableValue === 'unset') {
cssVariableValue = UNRESOLVED_VAR;
}

output = `${output.substring(0, idx)}${cssVariableValue}${output.substring(endIdx + 1)}`;
}

return output;
// An unresolved var() invalidates the whole declaration, wherever it is nested,
// unless it sat in the fallback of a var() that resolved.
return output.includes(UNRESOLVED_VAR) ? 'unset' : output;
}

function findClosingParenthesis(value: string, openIdx: number): number {
Expand Down
5 changes: 2 additions & 3 deletions packages/core/ui/styling/css-color-mix.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ describe('css-color-mix', () => {
expect(color.toRgbString()).toBe('rgba(247, 112, 125, 1.00)');
});

it('color-mix(in --swop5c, red, blue)', () => {
const color = new Color('color-mix(in --swop5c, red, blue)');
expect(color.toRgbString()).toBe('rgba(0, 0, 255, 0.00)');
it.each(['color-mix(in --swop5c, red, blue)', 'color-mix(in srgb, unset 35%, transparent)', 'color-mix(in srgb, notacolor, red)', 'color-mix(red, blue)'])('throws for malformed %s', (value) => {
expect(() => new Color(value)).toThrow(`Invalid color: ${value}`);
});
});
9 changes: 9 additions & 0 deletions packages/core/ui/styling/style-scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,15 @@ describe('CssState.setPropertyValues', () => {
expect(view.style.marginTop).toEqual(styled('').view.style.marginTop);
});

it('unsets a color whose nested variable cannot be resolved', () => {
const { view } = styled('label { color: red; } label:highlighted { color: color-mix(in srgb, var(--missing) 35%, transparent); }');
view._cssState.onLoaded();
expect(view.style.color.toString()).toBe('#FF0000');

view.addPseudoClass('highlighted');
expect(view.style.color).toBeUndefined();
});

it('applies an inline shorthand holding a variable', () => {
const { view } = styled('label { --m: 6; }');
view._cssState.onLoaded();
Expand Down
Loading