Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for color literals #2503

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export {
BooleanExpressionProps,
BooleanExpressionRaws,
} from './src/expression/boolean';
export {
ColorExpression,
ColorExpressionProps,
ColorExpressionRaws,
} from './src/expression/color';
export {
NumberExpression,
NumberExpressionProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a color expression toJSON 1`] = `
{
"inputs": [
{
"css": "@#{#00f}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intended for @ to be here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, because we didn't have variable statements early on the way we parse expressions is by creating an interpolation in an unknown at-rule.

"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "color",
"source": <1:4-1:8 in 0>,
"value": {
"alpha": 1,
"channels": [
0,
0,
255,
],
"space": "rgb",
},
}
`;
246 changes: 246 additions & 0 deletions pkg/sass-parser/lib/src/expression/color.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// Copyright 2025 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import {SassColor} from 'sass';

import {ColorExpression} from '../..';
import * as utils from '../../../test/utils';

const blue = new SassColor({space: 'rgb', red: 0, green: 0, blue: 255});

describe('a color expression', () => {
let node: ColorExpression;

describe('with no alpha', () => {
function describeNode(
description: string,
create: () => ColorExpression,
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has sassType color', () => expect(node.sassType).toBe('color'));

it('is a color', () => expect(node.value).toEqual(blue));
});
}

describe('parsed', () => {
describeNode('hex', () => utils.parseExpression('#00f'));

describeNode('keyword', () => utils.parseExpression('blue'));
});

describeNode(
'constructed manually',
() => new ColorExpression({value: blue}),
);

describeNode('constructed from ExpressionProps', () =>
utils.fromExpressionProps({value: blue}),
);
});

describe('with alpha', () => {
function describeNode(
description: string,
create: () => ColorExpression,
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has sassType color', () => expect(node.sassType).toBe('color'));

it('is a color', () =>
expect(node.value).toEqual(
new SassColor({
space: 'rgb',
red: 10,
green: 20,
blue: 30,
alpha: 0.4,
}),
));
});
}

describeNode('parsed', () => utils.parseExpression('#0a141E66'));

describeNode(
'constructed manually',
() =>
new ColorExpression({
value: new SassColor({
space: 'rgb',
red: 10,
green: 20,
blue: 30,
alpha: 0.4,
}),
}),
);

describeNode('constructed from ExpressionProps', () =>
utils.fromExpressionProps({
value: new SassColor({
space: 'rgb',
red: 10,
green: 20,
blue: 30,
alpha: 0.4,
}),
}),
);
});

describe('throws an error for non-RGB colors', () => {
beforeEach(() => void (node = utils.parseExpression('#123')));

it('in the constructor', () =>
expect(
() =>
new ColorExpression({
value: new SassColor({
space: 'hsl',
hue: 180,
saturation: 50,
lightness: 50,
}),
}),
).toThrow());

it('in the property', () =>
expect(() => {
node.value = new SassColor({
space: 'hsl',
hue: 180,
saturation: 50,
lightness: 50,
});
}).toThrow());

it('in clone', () =>
expect(() =>
node.clone({
value: new SassColor({
space: 'hsl',
hue: 180,
saturation: 50,
lightness: 50,
}),
}),
).toThrow());
});

it('assigned new value', () => {
const node = utils.parseExpression('#123') as ColorExpression;
node.value = new SassColor({
space: 'rgb',
red: 10,
green: 20,
blue: 30,
alpha: 0.4,
});
expect(node.value).toEqual(
new SassColor({
space: 'rgb',
red: 10,
green: 20,
blue: 30,
alpha: 0.4,
}),
);
});

describe('stringifies', () => {
it('without alpha', () =>
expect(utils.parseExpression('#abc').toString()).toBe('#aabbcc'));

it('with alpha', () =>
expect(utils.parseExpression('#abcd').toString()).toBe('#aabbccdd'));

describe('raws', () => {
it('with the same raw value as the expression', () =>
expect(
new ColorExpression({
value: blue,
raws: {value: {raw: 'blue', value: blue}},
}).toString(),
).toBe('blue'));

it('with a different raw value than the expression', () =>
expect(
new ColorExpression({
value: new SassColor({space: 'rgb', red: 10, green: 20, blue: 30}),
raws: {value: {raw: 'blue', value: blue}},
}).toString(),
).toBe('#0a141e'));
});
});

describe('clone', () => {
let original: ColorExpression;

beforeEach(() => {
original = utils.parseExpression('#00f');
// TODO: remove this once raws are properly parsed.
original.raws.value = {raw: 'blue', value: blue};
});

describe('with no overrides', () => {
let clone: ColorExpression;

beforeEach(() => void (clone = original.clone()));

describe('has the same properties:', () => {
it('value', () => expect(clone.value).toEqual(blue));

it('raws', () => {
expect(clone.raws.value!.raw).toBe('blue');
expect(clone.raws.value!.value).toEqual(blue);
});

it('source', () => expect(clone.source).toBe(original.source));
});

it('creates a new self', () => expect(clone).not.toBe(original));
});

describe('overrides', () => {
describe('value', () => {
it('defined', () =>
expect(
original.clone({
value: new SassColor({
space: 'rgb',
red: 10,
green: 20,
blue: 30,
}),
}).value,
).toEqual(
new SassColor({space: 'rgb', red: 10, green: 20, blue: 30}),
));

it('undefined', () =>
expect(original.clone({value: undefined}).value).toEqual(blue));
});

describe('raws', () => {
it('defined', () =>
expect(
original.clone({raws: {value: {raw: '#0000FF', value: blue}}}).raws
.value!.raw,
).toBe('#0000FF'));

it('undefined', () =>
expect(original.clone({raws: undefined}).raws.value!.raw).toBe(
'blue',
));
});
});
});

it('toJSON', () => expect(utils.parseExpression('#00f')).toMatchSnapshot());
});
Loading