Skip to content

Commit 7c4ff8f

Browse files
nex3Goodwine
andauthored
Add support for map expressions (#2517)
Co-authored-by: Carlos (Goodwine) <[email protected]>
1 parent 1b58aa9 commit 7c4ff8f

13 files changed

+1707
-0
lines changed

pkg/sass-parser/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
* Add support for parsing list expressions.
44

5+
* Add support for parsing map expressions.
6+
57
## 0.4.14
68

79
* Add support for parsing color expressions.

pkg/sass-parser/lib/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ export {
7777
ListSeparator,
7878
NewNodeForListExpression,
7979
} from './src/expression/list';
80+
export {
81+
MapEntry,
82+
MapEntryProps,
83+
MapEntryRaws,
84+
} from './src/expression/map-entry';
85+
export {
86+
MapExpression,
87+
MapExpressionProps,
88+
MapExpressionRaws,
89+
NewNodeForMapExpression,
90+
} from './src/expression/map';
8091
export {
8192
NumberExpression,
8293
NumberExpressionProps,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a map entry toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@#{(baz: qux)}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"key": <baz>,
13+
"raws": {},
14+
"sassType": "map-entry",
15+
"value": <qux>,
16+
}
17+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a map expression toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@#{(foo: bar, baz: bang)}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"nodes": [
13+
<foo: bar>,
14+
<baz: bang>,
15+
],
16+
"raws": {},
17+
"sassType": "map",
18+
"source": <1:4-1:25 in 0>,
19+
}
20+
`;

pkg/sass-parser/lib/src/expression/convert.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {BinaryOperationExpression} from './binary-operation';
99
import {BooleanExpression} from './boolean';
1010
import {ColorExpression} from './color';
1111
import {ListExpression} from './list';
12+
import {MapExpression} from './map';
1213
import {NumberExpression} from './number';
1314
import {StringExpression} from './string';
1415

@@ -20,6 +21,7 @@ const visitor = sassInternal.createExpressionVisitor<Expression>({
2021
visitBooleanExpression: inner => new BooleanExpression(undefined, inner),
2122
visitColorExpression: inner => new ColorExpression(undefined, inner),
2223
visitListExpression: inner => new ListExpression(undefined, inner),
24+
visitMapExpression: inner => new MapExpression(undefined, inner),
2325
visitNumberExpression: inner => new NumberExpression(undefined, inner),
2426
});
2527

pkg/sass-parser/lib/src/expression/from-props.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {BinaryOperationExpression} from './binary-operation';
88
import {BooleanExpression} from './boolean';
99
import {ColorExpression} from './color';
1010
import {ListExpression} from './list';
11+
import {MapExpression} from './map';
1112
import {NumberExpression} from './number';
1213
import {StringExpression} from './string';
1314

@@ -16,6 +17,7 @@ export function fromProps(props: ExpressionProps): Expression {
1617
if ('text' in props) return new StringExpression(props);
1718
if ('left' in props) return new BinaryOperationExpression(props);
1819
if ('separator' in props) return new ListExpression(props);
20+
if ('nodes' in props) return new MapExpression(props);
1921
if ('value' in props) {
2022
if (typeof props.value === 'boolean') return new BooleanExpression(props);
2123
if (typeof props.value === 'number') return new NumberExpression(props);

pkg/sass-parser/lib/src/expression/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
import {BooleanExpression, BooleanExpressionProps} from './boolean';
1111
import {ColorExpression, ColorExpressionProps} from './color';
1212
import {ListExpression, ListExpressionProps} from './list';
13+
import {MapExpression, MapExpressionProps} from './map';
1314
import {NumberExpression, NumberExpressionProps} from './number';
1415
import type {StringExpression, StringExpressionProps} from './string';
1516

@@ -23,6 +24,7 @@ export type AnyExpression =
2324
| BooleanExpression
2425
| ColorExpression
2526
| ListExpression
27+
| MapExpression
2628
| NumberExpression
2729
| StringExpression;
2830

@@ -36,6 +38,7 @@ export type ExpressionType =
3638
| 'boolean'
3739
| 'color'
3840
| 'list'
41+
| 'map'
3942
| 'number'
4043
| 'string';
4144

@@ -50,6 +53,7 @@ export type ExpressionProps =
5053
| BooleanExpressionProps
5154
| ColorExpressionProps
5255
| ListExpressionProps
56+
| MapExpressionProps
5357
| NumberExpressionProps
5458
| StringExpressionProps;
5559

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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 {MapEntry, MapExpression, StringExpression} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a map entry', () => {
9+
let node: MapEntry;
10+
beforeEach(
11+
() =>
12+
void (node = new MapEntry({
13+
key: {text: 'foo'},
14+
value: {text: 'bar'},
15+
})),
16+
);
17+
18+
function describeNode(description: string, create: () => MapEntry): void {
19+
describe(description, () => {
20+
beforeEach(() => (node = create()));
21+
22+
it('has a sassType', () =>
23+
expect(node.sassType.toString()).toBe('map-entry'));
24+
25+
it('has a key', () => expect(node).toHaveStringExpression('key', 'foo'));
26+
27+
it('has a value', () =>
28+
expect(node).toHaveStringExpression('value', 'bar'));
29+
});
30+
}
31+
32+
describeNode(
33+
'parsed',
34+
() => (utils.parseExpression('(foo: bar)') as MapExpression).nodes[0],
35+
);
36+
37+
describe('constructed manually', () => {
38+
describe('with an array', () => {
39+
describeNode(
40+
'with two Expressions',
41+
() =>
42+
new MapEntry([
43+
new StringExpression({text: 'foo'}),
44+
new StringExpression({text: 'bar'}),
45+
]),
46+
);
47+
48+
describeNode(
49+
'with two ExpressionProps',
50+
() => new MapEntry([{text: 'foo'}, {text: 'bar'}]),
51+
);
52+
53+
describeNode(
54+
'with mixed Expressions and ExpressionProps',
55+
() =>
56+
new MapEntry([{text: 'foo'}, new StringExpression({text: 'bar'})]),
57+
);
58+
});
59+
60+
describe('with an object', () => {
61+
describeNode(
62+
'with two Expressions',
63+
() =>
64+
new MapEntry({
65+
key: new StringExpression({text: 'foo'}),
66+
value: new StringExpression({text: 'bar'}),
67+
}),
68+
);
69+
70+
describeNode(
71+
'with ExpressionProps',
72+
() => new MapEntry({key: {text: 'foo'}, value: {text: 'bar'}}),
73+
);
74+
});
75+
});
76+
77+
it('assigned a new key', () => {
78+
const old = node.key;
79+
node.key = {text: 'baz'};
80+
expect(old.parent).toBeUndefined();
81+
expect(node).toHaveStringExpression('key', 'baz');
82+
});
83+
84+
it('assigned a new value', () => {
85+
const old = node.value;
86+
node.value = {text: 'baz'};
87+
expect(old.parent).toBeUndefined();
88+
expect(node).toHaveStringExpression('value', 'baz');
89+
});
90+
91+
describe('stringifies', () => {
92+
describe('to SCSS', () => {
93+
it('with default raws', () =>
94+
expect(
95+
new MapEntry({
96+
key: {text: 'foo'},
97+
value: {text: 'bar'},
98+
}).toString(),
99+
).toBe('foo: bar'));
100+
101+
// raws.before is only used as part of a MapExpression
102+
it('ignores before', () =>
103+
expect(
104+
new MapEntry({
105+
key: {text: 'foo'},
106+
value: {text: 'bar'},
107+
raws: {before: '/**/'},
108+
}).toString(),
109+
).toBe('foo: bar'));
110+
111+
it('with between', () =>
112+
expect(
113+
new MapEntry({
114+
key: {text: 'foo'},
115+
value: {text: 'bar'},
116+
raws: {between: ' : '},
117+
}).toString(),
118+
).toBe('foo : bar'));
119+
120+
// raws.after is only used as part of a Configuration
121+
it('ignores after', () =>
122+
expect(
123+
new MapEntry({
124+
key: {text: 'foo'},
125+
value: {text: 'bar'},
126+
raws: {after: '/**/'},
127+
}).toString(),
128+
).toBe('foo: bar'));
129+
});
130+
});
131+
132+
describe('clone()', () => {
133+
let original: MapEntry;
134+
beforeEach(() => {
135+
original = (utils.parseExpression('(foo: bar)') as MapExpression)
136+
.nodes[0];
137+
original.raws.between = ' : ';
138+
});
139+
140+
describe('with no overrides', () => {
141+
let clone: MapEntry;
142+
beforeEach(() => void (clone = original.clone()));
143+
144+
describe('has the same properties:', () => {
145+
it('key', () => expect(clone).toHaveStringExpression('key', 'foo'));
146+
147+
it('value', () => expect(clone).toHaveStringExpression('value', 'bar'));
148+
});
149+
150+
describe('creates a new', () => {
151+
it('self', () => expect(clone).not.toBe(original));
152+
153+
for (const attr of ['key', 'value', 'raws'] as const) {
154+
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
155+
}
156+
});
157+
});
158+
159+
describe('overrides', () => {
160+
describe('raws', () => {
161+
it('defined', () =>
162+
expect(original.clone({raws: {before: ' '}}).raws).toEqual({
163+
before: ' ',
164+
}));
165+
166+
it('undefined', () =>
167+
expect(original.clone({raws: undefined}).raws).toEqual({
168+
between: ' : ',
169+
}));
170+
});
171+
172+
describe('key', () => {
173+
it('defined', () =>
174+
expect(original.clone({key: {text: 'baz'}})).toHaveStringExpression(
175+
'key',
176+
'baz',
177+
));
178+
179+
it('undefined', () =>
180+
expect(original.clone({key: undefined})).toHaveStringExpression(
181+
'key',
182+
'foo',
183+
));
184+
});
185+
186+
describe('value', () => {
187+
it('defined', () =>
188+
expect(original.clone({value: {text: 'baz'}})).toHaveStringExpression(
189+
'value',
190+
'baz',
191+
));
192+
193+
it('undefined', () =>
194+
expect(original.clone({value: undefined})).toHaveStringExpression(
195+
'value',
196+
'bar',
197+
));
198+
});
199+
});
200+
});
201+
202+
it('toJSON', () =>
203+
expect(
204+
(utils.parseExpression('(baz: qux)') as MapExpression).nodes[0],
205+
).toMatchSnapshot());
206+
});

0 commit comments

Comments
 (0)