Skip to content

Commit 879f484

Browse files
nex3Goodwine
andauthored
Add support for color literals (#2503)
Co-authored-by: Carlos (Goodwine) <[email protected]>
1 parent ae4b757 commit 879f484

18 files changed

+470
-20
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.84.1-dev
2+
3+
* No user-visible changes.
4+
15
## 1.84.0
26

37
* Allow newlines in whitespace in the indented syntax.

pkg/sass-parser/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.4.14-dev
2+
3+
* Add support for parsing color expressions.
4+
15
## 0.4.13
26

37
* No user-visible changes.

pkg/sass-parser/lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export {
6565
BooleanExpressionProps,
6666
BooleanExpressionRaws,
6767
} from './src/expression/boolean';
68+
export {
69+
ColorExpression,
70+
ColorExpressionProps,
71+
ColorExpressionRaws,
72+
} from './src/expression/color';
6873
export {
6974
NumberExpression,
7075
NumberExpressionProps,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a color expression toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@#{#00f}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"raws": {},
13+
"sassType": "color",
14+
"source": <1:4-1:8 in 0>,
15+
"value": {
16+
"alpha": 1,
17+
"channels": [
18+
0,
19+
0,
20+
255,
21+
],
22+
"space": "rgb",
23+
},
24+
}
25+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright 2025 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {SassColor} from 'sass';
6+
7+
import {ColorExpression} from '../..';
8+
import * as utils from '../../../test/utils';
9+
10+
const blue = new SassColor({space: 'rgb', red: 0, green: 0, blue: 255});
11+
12+
describe('a color expression', () => {
13+
let node: ColorExpression;
14+
15+
describe('with no alpha', () => {
16+
function describeNode(
17+
description: string,
18+
create: () => ColorExpression,
19+
): void {
20+
describe(description, () => {
21+
beforeEach(() => void (node = create()));
22+
23+
it('has sassType color', () => expect(node.sassType).toBe('color'));
24+
25+
it('is a color', () => expect(node.value).toEqual(blue));
26+
});
27+
}
28+
29+
describe('parsed', () => {
30+
describeNode('hex', () => utils.parseExpression('#00f'));
31+
32+
describeNode('keyword', () => utils.parseExpression('blue'));
33+
});
34+
35+
describeNode(
36+
'constructed manually',
37+
() => new ColorExpression({value: blue}),
38+
);
39+
40+
describeNode('constructed from ExpressionProps', () =>
41+
utils.fromExpressionProps({value: blue}),
42+
);
43+
});
44+
45+
describe('with alpha', () => {
46+
function describeNode(
47+
description: string,
48+
create: () => ColorExpression,
49+
): void {
50+
describe(description, () => {
51+
beforeEach(() => void (node = create()));
52+
53+
it('has sassType color', () => expect(node.sassType).toBe('color'));
54+
55+
it('is a color', () =>
56+
expect(node.value).toEqual(
57+
new SassColor({
58+
space: 'rgb',
59+
red: 10,
60+
green: 20,
61+
blue: 30,
62+
alpha: 0.4,
63+
}),
64+
));
65+
});
66+
}
67+
68+
describeNode('parsed', () => utils.parseExpression('#0a141E66'));
69+
70+
describeNode(
71+
'constructed manually',
72+
() =>
73+
new ColorExpression({
74+
value: new SassColor({
75+
space: 'rgb',
76+
red: 10,
77+
green: 20,
78+
blue: 30,
79+
alpha: 0.4,
80+
}),
81+
}),
82+
);
83+
84+
describeNode('constructed from ExpressionProps', () =>
85+
utils.fromExpressionProps({
86+
value: new SassColor({
87+
space: 'rgb',
88+
red: 10,
89+
green: 20,
90+
blue: 30,
91+
alpha: 0.4,
92+
}),
93+
}),
94+
);
95+
});
96+
97+
describe('throws an error for non-RGB colors', () => {
98+
beforeEach(() => void (node = utils.parseExpression('#123')));
99+
100+
it('in the constructor', () =>
101+
expect(
102+
() =>
103+
new ColorExpression({
104+
value: new SassColor({
105+
space: 'hsl',
106+
hue: 180,
107+
saturation: 50,
108+
lightness: 50,
109+
}),
110+
}),
111+
).toThrow());
112+
113+
it('in the property', () =>
114+
expect(() => {
115+
node.value = new SassColor({
116+
space: 'hsl',
117+
hue: 180,
118+
saturation: 50,
119+
lightness: 50,
120+
});
121+
}).toThrow());
122+
123+
it('in clone', () =>
124+
expect(() =>
125+
node.clone({
126+
value: new SassColor({
127+
space: 'hsl',
128+
hue: 180,
129+
saturation: 50,
130+
lightness: 50,
131+
}),
132+
}),
133+
).toThrow());
134+
});
135+
136+
it('assigned new value', () => {
137+
const node = utils.parseExpression('#123') as ColorExpression;
138+
node.value = new SassColor({
139+
space: 'rgb',
140+
red: 10,
141+
green: 20,
142+
blue: 30,
143+
alpha: 0.4,
144+
});
145+
expect(node.value).toEqual(
146+
new SassColor({
147+
space: 'rgb',
148+
red: 10,
149+
green: 20,
150+
blue: 30,
151+
alpha: 0.4,
152+
}),
153+
);
154+
});
155+
156+
describe('stringifies', () => {
157+
it('without alpha', () =>
158+
expect(utils.parseExpression('#abc').toString()).toBe('#aabbcc'));
159+
160+
it('with alpha', () =>
161+
expect(utils.parseExpression('#abcd').toString()).toBe('#aabbccdd'));
162+
163+
describe('raws', () => {
164+
it('with the same raw value as the expression', () =>
165+
expect(
166+
new ColorExpression({
167+
value: blue,
168+
raws: {value: {raw: 'blue', value: blue}},
169+
}).toString(),
170+
).toBe('blue'));
171+
172+
it('with a different raw value than the expression', () =>
173+
expect(
174+
new ColorExpression({
175+
value: new SassColor({space: 'rgb', red: 10, green: 20, blue: 30}),
176+
raws: {value: {raw: 'blue', value: blue}},
177+
}).toString(),
178+
).toBe('#0a141e'));
179+
});
180+
});
181+
182+
describe('clone', () => {
183+
let original: ColorExpression;
184+
185+
beforeEach(() => {
186+
original = utils.parseExpression('#00f');
187+
// TODO: remove this once raws are properly parsed.
188+
original.raws.value = {raw: 'blue', value: blue};
189+
});
190+
191+
describe('with no overrides', () => {
192+
let clone: ColorExpression;
193+
194+
beforeEach(() => void (clone = original.clone()));
195+
196+
describe('has the same properties:', () => {
197+
it('value', () => expect(clone.value).toEqual(blue));
198+
199+
it('raws', () => {
200+
expect(clone.raws.value!.raw).toBe('blue');
201+
expect(clone.raws.value!.value).toEqual(blue);
202+
});
203+
204+
it('source', () => expect(clone.source).toBe(original.source));
205+
});
206+
207+
it('creates a new self', () => expect(clone).not.toBe(original));
208+
});
209+
210+
describe('overrides', () => {
211+
describe('value', () => {
212+
it('defined', () =>
213+
expect(
214+
original.clone({
215+
value: new SassColor({
216+
space: 'rgb',
217+
red: 10,
218+
green: 20,
219+
blue: 30,
220+
}),
221+
}).value,
222+
).toEqual(
223+
new SassColor({space: 'rgb', red: 10, green: 20, blue: 30}),
224+
));
225+
226+
it('undefined', () =>
227+
expect(original.clone({value: undefined}).value).toEqual(blue));
228+
});
229+
230+
describe('raws', () => {
231+
it('defined', () =>
232+
expect(
233+
original.clone({raws: {value: {raw: '#0000FF', value: blue}}}).raws
234+
.value!.raw,
235+
).toBe('#0000FF'));
236+
237+
it('undefined', () =>
238+
expect(original.clone({raws: undefined}).raws.value!.raw).toBe(
239+
'blue',
240+
));
241+
});
242+
});
243+
});
244+
245+
it('toJSON', () => expect(utils.parseExpression('#00f')).toMatchSnapshot());
246+
});

0 commit comments

Comments
 (0)