Skip to content

Commit

Permalink
Add support for parsing interpolated function calls (#2521)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlos (Goodwine) <2022649+Goodwine@users.noreply.github.com>
  • Loading branch information
nex3 and Goodwine authored Feb 25, 2025
1 parent c540875 commit fae0217
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 2 deletions.
4 changes: 4 additions & 0 deletions lib/src/js/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ void _updateAstPrototypes() {
getJSClass(
IfExpression(arguments, bogusSpan),
).defineGetter('arguments', (IfExpression self) => self.arguments);
getJSClass(
InterpolatedFunctionExpression(_interpolation, arguments, bogusSpan),
).defineGetter(
'arguments', (InterpolatedFunctionExpression self) => self.arguments);

_addSupportsConditionToInterpolation();

Expand Down
2 changes: 2 additions & 0 deletions pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

* Add support for parsing function calls.

* Add support for parsing interpolated function calls.

## 0.4.14

* Add support for parsing color expressions.
Expand Down
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export {
FunctionExpressionProps,
FunctionExpressionRaws,
} from './src/expression/function';
export {
InterpolatedFunctionExpression,
InterpolatedFunctionExpressionProps,
InterpolatedFunctionExpressionRaws,
} from './src/expression/interpolated-function';
export {
ListExpression,
ListExpressionProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`an interpolated function expression toJSON 1`] = `
{
"arguments": <(bar)>,
"inputs": [
{
"css": "@#{f#{o}o(bar)}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"name": <f#{o}o>,
"raws": {},
"sassType": "interpolated-function-call",
"source": <1:4-1:15 in 0>,
}
`;
3 changes: 3 additions & 0 deletions pkg/sass-parser/lib/src/expression/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {BinaryOperationExpression} from './binary-operation';
import {BooleanExpression} from './boolean';
import {ColorExpression} from './color';
import {FunctionExpression} from './function';
import {InterpolatedFunctionExpression} from './interpolated-function';
import {ListExpression} from './list';
import {MapExpression} from './map';
import {NumberExpression} from './number';
Expand All @@ -28,6 +29,8 @@ const visitor = sassInternal.createExpressionVisitor<Expression>({
name: 'if',
arguments: new ArgumentList(undefined, inner.arguments),
}),
visitInterpolatedFunctionExpression: inner =>
new InterpolatedFunctionExpression(undefined, inner),
visitListExpression: inner => new ListExpression(undefined, inner),
visitMapExpression: inner => new MapExpression(undefined, inner),
visitNumberExpression: inner => new NumberExpression(undefined, inner),
Expand Down
11 changes: 9 additions & 2 deletions pkg/sass-parser/lib/src/expression/from-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {Expression, ExpressionProps} from '.';
import {BinaryOperationExpression} from './binary-operation';
import {BooleanExpression} from './boolean';
import {ColorExpression} from './color';
import {FunctionExpression} from './function';
import {FunctionExpression, FunctionExpressionProps} from './function';
import {InterpolatedFunctionExpression} from './interpolated-function';
import {ListExpression} from './list';
import {MapExpression} from './map';
import {NumberExpression} from './number';
Expand All @@ -19,7 +20,13 @@ export function fromProps(props: ExpressionProps): Expression {
if ('left' in props) return new BinaryOperationExpression(props);
if ('separator' in props) return new ListExpression(props);
if ('nodes' in props) return new MapExpression(props);
if ('name' in props) return new FunctionExpression(props);
if ('name' in props) {
if (typeof props.name === 'string') {
return new FunctionExpression(props as FunctionExpressionProps);
} else {
return new InterpolatedFunctionExpression(props);
}
}
if ('value' in props) {
if (typeof props.value === 'boolean') return new BooleanExpression(props);
if (typeof props.value === 'number') return new NumberExpression(props);
Expand Down
7 changes: 7 additions & 0 deletions pkg/sass-parser/lib/src/expression/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import type {
import {BooleanExpression, BooleanExpressionProps} from './boolean';
import {ColorExpression, ColorExpressionProps} from './color';
import {FunctionExpression, FunctionExpressionProps} from './function';
import {
InterpolatedFunctionExpression,
InterpolatedFunctionExpressionProps,
} from './interpolated-function';
import {ListExpression, ListExpressionProps} from './list';
import {MapExpression, MapExpressionProps} from './map';
import {NumberExpression, NumberExpressionProps} from './number';
Expand All @@ -25,6 +29,7 @@ export type AnyExpression =
| BooleanExpression
| ColorExpression
| FunctionExpression
| InterpolatedFunctionExpression
| ListExpression
| MapExpression
| NumberExpression
Expand All @@ -40,6 +45,7 @@ export type ExpressionType =
| 'boolean'
| 'color'
| 'function-call'
| 'interpolated-function-call'
| 'list'
| 'map'
| 'number'
Expand All @@ -56,6 +62,7 @@ export type ExpressionProps =
| BooleanExpressionProps
| ColorExpressionProps
| FunctionExpressionProps
| InterpolatedFunctionExpressionProps
| ListExpressionProps
| MapExpressionProps
| NumberExpressionProps
Expand Down
191 changes: 191 additions & 0 deletions pkg/sass-parser/lib/src/expression/interpolated-function.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// 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 {
ArgumentList,
InterpolatedFunctionExpression,
Interpolation,
} from '../..';
import * as utils from '../../../test/utils';

describe('an interpolated function expression', () => {
let node: InterpolatedFunctionExpression;

function describeNode(
description: string,
create: () => InterpolatedFunctionExpression,
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has sassType interpolated-function-call', () =>
expect(node.sassType).toBe('interpolated-function-call'));

it('has a name', () =>
expect(node).toHaveInterpolation('name', 'f#{o}o'));

it('has an argument', () =>
expect(node.arguments.nodes[0]).toHaveStringExpression('value', 'bar'));
});
}

describeNode('parsed', () => utils.parseExpression('f#{o}o(bar)'));

describeNode(
'constructed manually',
() =>
new InterpolatedFunctionExpression({
name: ['f', {text: 'o'}, 'o'],
arguments: [{text: 'bar'}],
}),
);

describeNode('constructed from ExpressionProps', () =>
utils.fromExpressionProps({
name: ['f', {text: 'o'}, 'o'],
arguments: [{text: 'bar'}],
}),
);

describe('assigned new name', () => {
beforeEach(() => void (node = utils.parseExpression('f#{o}o(bar)')));

it("removes the old name's parent", () => {
const oldName = node.name;
node.name = [{text: 'baz'}];
expect(oldName.parent).toBeUndefined();
});

it("assigns the new name's parent", () => {
const name = new Interpolation([{text: 'baz'}]);
node.name = name;
expect(name.parent).toBe(node);
});

it('assigns the name explicitly', () => {
const name = new Interpolation([{text: 'baz'}]);
node.name = name;
expect(node.name).toBe(name);
});

it('assigns the expression as InterpolationProps', () => {
node.name = [{text: 'baz'}];
expect(node).toHaveInterpolation('name', '#{baz}');
});
});

describe('assigned new arguments', () => {
beforeEach(() => void (node = utils.parseExpression('f#{o}o(bar)')));

it("removes the old arguments' parent", () => {
const oldArguments = node.arguments;
node.arguments = [{text: 'qux'}];
expect(oldArguments.parent).toBeUndefined();
});

it("assigns the new arguments' parent", () => {
const args = new ArgumentList([{text: 'qux'}]);
node.arguments = args;
expect(args.parent).toBe(node);
});

it('assigns the arguments explicitly', () => {
const args = new ArgumentList([{text: 'qux'}]);
node.arguments = args;
expect(node.arguments).toBe(args);
});

it('assigns the expression as ArgumentProps', () => {
node.arguments = [{text: 'qux'}];
expect(node.arguments.nodes[0]).toHaveStringExpression('value', 'qux');
expect(node.arguments.parent).toBe(node);
});
});

it('stringifies', () =>
expect(
new InterpolatedFunctionExpression({
name: ['f', {text: 'o'}, 'o'],
arguments: [{text: 'bar'}],
}).toString(),
).toBe('f#{o}o(bar)'));

describe('clone', () => {
let original: InterpolatedFunctionExpression;
beforeEach(() => void (original = utils.parseExpression('f#{o}o(bar)')));

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

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

describe('has the same properties:', () => {
it('name', () => expect(clone).toHaveInterpolation('name', 'f#{o}o'));

it('arguments', () => {
expect(clone.arguments.nodes[0]).toHaveStringExpression(
'value',
'bar',
);
expect(clone.arguments.parent).toBe(clone);
});

it('raws', () => expect(clone.raws).toEqual({}));

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

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

for (const attr of ['raws'] as const) {
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
}
});
});

describe('overrides', () => {
describe('raws', () => {
it('defined', () =>
expect(original.clone({raws: {}}).raws).toEqual({}));

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

describe('name', () => {
it('defined', () =>
expect(original.clone({name: [{text: 'zip'}]})).toHaveInterpolation(
'name',
'#{zip}',
));

it('undefined', () =>
expect(original.clone({name: undefined})).toHaveInterpolation(
'name',
'f#{o}o',
));
});

describe('arguments', () => {
it('defined', () => {
const clone = original.clone({arguments: [{text: 'qux'}]});
expect(clone.arguments.nodes[0]).toHaveStringExpression(
'value',
'qux',
);
expect(clone.arguments.parent).toBe(clone);
});

it('undefined', () =>
expect(
original.clone({arguments: undefined}).arguments.nodes[0],
).toHaveStringExpression('value', 'bar'));
});
});
});

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

0 comments on commit fae0217

Please sign in to comment.