Skip to content

Commit fae0217

Browse files
nex3Goodwine
andauthored
Add support for parsing interpolated function calls (#2521)
Co-authored-by: Carlos (Goodwine) <[email protected]>
1 parent c540875 commit fae0217

10 files changed

+355
-2
lines changed

lib/src/js/parser.dart

+4
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ void _updateAstPrototypes() {
115115
getJSClass(
116116
IfExpression(arguments, bogusSpan),
117117
).defineGetter('arguments', (IfExpression self) => self.arguments);
118+
getJSClass(
119+
InterpolatedFunctionExpression(_interpolation, arguments, bogusSpan),
120+
).defineGetter(
121+
'arguments', (InterpolatedFunctionExpression self) => self.arguments);
118122

119123
_addSupportsConditionToInterpolation();
120124

pkg/sass-parser/CHANGELOG.md

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

77
* Add support for parsing function calls.
88

9+
* Add support for parsing interpolated function calls.
10+
911
## 0.4.14
1012

1113
* Add support for parsing color expressions.

pkg/sass-parser/lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ export {
7575
FunctionExpressionProps,
7676
FunctionExpressionRaws,
7777
} from './src/expression/function';
78+
export {
79+
InterpolatedFunctionExpression,
80+
InterpolatedFunctionExpressionProps,
81+
InterpolatedFunctionExpressionRaws,
82+
} from './src/expression/interpolated-function';
7883
export {
7984
ListExpression,
8085
ListExpressionProps,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`an interpolated function expression toJSON 1`] = `
4+
{
5+
"arguments": <(bar)>,
6+
"inputs": [
7+
{
8+
"css": "@#{f#{o}o(bar)}",
9+
"hasBOM": false,
10+
"id": "<input css _____>",
11+
},
12+
],
13+
"name": <f#{o}o>,
14+
"raws": {},
15+
"sassType": "interpolated-function-call",
16+
"source": <1:4-1:15 in 0>,
17+
}
18+
`;

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

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {BinaryOperationExpression} from './binary-operation';
1010
import {BooleanExpression} from './boolean';
1111
import {ColorExpression} from './color';
1212
import {FunctionExpression} from './function';
13+
import {InterpolatedFunctionExpression} from './interpolated-function';
1314
import {ListExpression} from './list';
1415
import {MapExpression} from './map';
1516
import {NumberExpression} from './number';
@@ -28,6 +29,8 @@ const visitor = sassInternal.createExpressionVisitor<Expression>({
2829
name: 'if',
2930
arguments: new ArgumentList(undefined, inner.arguments),
3031
}),
32+
visitInterpolatedFunctionExpression: inner =>
33+
new InterpolatedFunctionExpression(undefined, inner),
3134
visitListExpression: inner => new ListExpression(undefined, inner),
3235
visitMapExpression: inner => new MapExpression(undefined, inner),
3336
visitNumberExpression: inner => new NumberExpression(undefined, inner),

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {Expression, ExpressionProps} from '.';
77
import {BinaryOperationExpression} from './binary-operation';
88
import {BooleanExpression} from './boolean';
99
import {ColorExpression} from './color';
10-
import {FunctionExpression} from './function';
10+
import {FunctionExpression, FunctionExpressionProps} from './function';
11+
import {InterpolatedFunctionExpression} from './interpolated-function';
1112
import {ListExpression} from './list';
1213
import {MapExpression} from './map';
1314
import {NumberExpression} from './number';
@@ -19,7 +20,13 @@ export function fromProps(props: ExpressionProps): Expression {
1920
if ('left' in props) return new BinaryOperationExpression(props);
2021
if ('separator' in props) return new ListExpression(props);
2122
if ('nodes' in props) return new MapExpression(props);
22-
if ('name' in props) return new FunctionExpression(props);
23+
if ('name' in props) {
24+
if (typeof props.name === 'string') {
25+
return new FunctionExpression(props as FunctionExpressionProps);
26+
} else {
27+
return new InterpolatedFunctionExpression(props);
28+
}
29+
}
2330
if ('value' in props) {
2431
if (typeof props.value === 'boolean') return new BooleanExpression(props);
2532
if (typeof props.value === 'number') return new NumberExpression(props);

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

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import type {
1010
import {BooleanExpression, BooleanExpressionProps} from './boolean';
1111
import {ColorExpression, ColorExpressionProps} from './color';
1212
import {FunctionExpression, FunctionExpressionProps} from './function';
13+
import {
14+
InterpolatedFunctionExpression,
15+
InterpolatedFunctionExpressionProps,
16+
} from './interpolated-function';
1317
import {ListExpression, ListExpressionProps} from './list';
1418
import {MapExpression, MapExpressionProps} from './map';
1519
import {NumberExpression, NumberExpressionProps} from './number';
@@ -25,6 +29,7 @@ export type AnyExpression =
2529
| BooleanExpression
2630
| ColorExpression
2731
| FunctionExpression
32+
| InterpolatedFunctionExpression
2833
| ListExpression
2934
| MapExpression
3035
| NumberExpression
@@ -40,6 +45,7 @@ export type ExpressionType =
4045
| 'boolean'
4146
| 'color'
4247
| 'function-call'
48+
| 'interpolated-function-call'
4349
| 'list'
4450
| 'map'
4551
| 'number'
@@ -56,6 +62,7 @@ export type ExpressionProps =
5662
| BooleanExpressionProps
5763
| ColorExpressionProps
5864
| FunctionExpressionProps
65+
| InterpolatedFunctionExpressionProps
5966
| ListExpressionProps
6067
| MapExpressionProps
6168
| NumberExpressionProps
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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 {
6+
ArgumentList,
7+
InterpolatedFunctionExpression,
8+
Interpolation,
9+
} from '../..';
10+
import * as utils from '../../../test/utils';
11+
12+
describe('an interpolated function expression', () => {
13+
let node: InterpolatedFunctionExpression;
14+
15+
function describeNode(
16+
description: string,
17+
create: () => InterpolatedFunctionExpression,
18+
): void {
19+
describe(description, () => {
20+
beforeEach(() => void (node = create()));
21+
22+
it('has sassType interpolated-function-call', () =>
23+
expect(node.sassType).toBe('interpolated-function-call'));
24+
25+
it('has a name', () =>
26+
expect(node).toHaveInterpolation('name', 'f#{o}o'));
27+
28+
it('has an argument', () =>
29+
expect(node.arguments.nodes[0]).toHaveStringExpression('value', 'bar'));
30+
});
31+
}
32+
33+
describeNode('parsed', () => utils.parseExpression('f#{o}o(bar)'));
34+
35+
describeNode(
36+
'constructed manually',
37+
() =>
38+
new InterpolatedFunctionExpression({
39+
name: ['f', {text: 'o'}, 'o'],
40+
arguments: [{text: 'bar'}],
41+
}),
42+
);
43+
44+
describeNode('constructed from ExpressionProps', () =>
45+
utils.fromExpressionProps({
46+
name: ['f', {text: 'o'}, 'o'],
47+
arguments: [{text: 'bar'}],
48+
}),
49+
);
50+
51+
describe('assigned new name', () => {
52+
beforeEach(() => void (node = utils.parseExpression('f#{o}o(bar)')));
53+
54+
it("removes the old name's parent", () => {
55+
const oldName = node.name;
56+
node.name = [{text: 'baz'}];
57+
expect(oldName.parent).toBeUndefined();
58+
});
59+
60+
it("assigns the new name's parent", () => {
61+
const name = new Interpolation([{text: 'baz'}]);
62+
node.name = name;
63+
expect(name.parent).toBe(node);
64+
});
65+
66+
it('assigns the name explicitly', () => {
67+
const name = new Interpolation([{text: 'baz'}]);
68+
node.name = name;
69+
expect(node.name).toBe(name);
70+
});
71+
72+
it('assigns the expression as InterpolationProps', () => {
73+
node.name = [{text: 'baz'}];
74+
expect(node).toHaveInterpolation('name', '#{baz}');
75+
});
76+
});
77+
78+
describe('assigned new arguments', () => {
79+
beforeEach(() => void (node = utils.parseExpression('f#{o}o(bar)')));
80+
81+
it("removes the old arguments' parent", () => {
82+
const oldArguments = node.arguments;
83+
node.arguments = [{text: 'qux'}];
84+
expect(oldArguments.parent).toBeUndefined();
85+
});
86+
87+
it("assigns the new arguments' parent", () => {
88+
const args = new ArgumentList([{text: 'qux'}]);
89+
node.arguments = args;
90+
expect(args.parent).toBe(node);
91+
});
92+
93+
it('assigns the arguments explicitly', () => {
94+
const args = new ArgumentList([{text: 'qux'}]);
95+
node.arguments = args;
96+
expect(node.arguments).toBe(args);
97+
});
98+
99+
it('assigns the expression as ArgumentProps', () => {
100+
node.arguments = [{text: 'qux'}];
101+
expect(node.arguments.nodes[0]).toHaveStringExpression('value', 'qux');
102+
expect(node.arguments.parent).toBe(node);
103+
});
104+
});
105+
106+
it('stringifies', () =>
107+
expect(
108+
new InterpolatedFunctionExpression({
109+
name: ['f', {text: 'o'}, 'o'],
110+
arguments: [{text: 'bar'}],
111+
}).toString(),
112+
).toBe('f#{o}o(bar)'));
113+
114+
describe('clone', () => {
115+
let original: InterpolatedFunctionExpression;
116+
beforeEach(() => void (original = utils.parseExpression('f#{o}o(bar)')));
117+
118+
describe('with no overrides', () => {
119+
let clone: InterpolatedFunctionExpression;
120+
121+
beforeEach(() => void (clone = original.clone()));
122+
123+
describe('has the same properties:', () => {
124+
it('name', () => expect(clone).toHaveInterpolation('name', 'f#{o}o'));
125+
126+
it('arguments', () => {
127+
expect(clone.arguments.nodes[0]).toHaveStringExpression(
128+
'value',
129+
'bar',
130+
);
131+
expect(clone.arguments.parent).toBe(clone);
132+
});
133+
134+
it('raws', () => expect(clone.raws).toEqual({}));
135+
136+
it('source', () => expect(clone.source).toBe(original.source));
137+
});
138+
139+
describe('creates a new', () => {
140+
it('self', () => expect(clone).not.toBe(original));
141+
142+
for (const attr of ['raws'] as const) {
143+
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
144+
}
145+
});
146+
});
147+
148+
describe('overrides', () => {
149+
describe('raws', () => {
150+
it('defined', () =>
151+
expect(original.clone({raws: {}}).raws).toEqual({}));
152+
153+
it('undefined', () =>
154+
expect(original.clone({raws: undefined}).raws).toEqual({}));
155+
});
156+
157+
describe('name', () => {
158+
it('defined', () =>
159+
expect(original.clone({name: [{text: 'zip'}]})).toHaveInterpolation(
160+
'name',
161+
'#{zip}',
162+
));
163+
164+
it('undefined', () =>
165+
expect(original.clone({name: undefined})).toHaveInterpolation(
166+
'name',
167+
'f#{o}o',
168+
));
169+
});
170+
171+
describe('arguments', () => {
172+
it('defined', () => {
173+
const clone = original.clone({arguments: [{text: 'qux'}]});
174+
expect(clone.arguments.nodes[0]).toHaveStringExpression(
175+
'value',
176+
'qux',
177+
);
178+
expect(clone.arguments.parent).toBe(clone);
179+
});
180+
181+
it('undefined', () =>
182+
expect(
183+
original.clone({arguments: undefined}).arguments.nodes[0],
184+
).toHaveStringExpression('value', 'bar'));
185+
});
186+
});
187+
});
188+
189+
it('toJSON', () =>
190+
expect(utils.parseExpression('f#{o}o(bar)')).toMatchSnapshot());
191+
});

0 commit comments

Comments
 (0)