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 map expressions #2517

Merged
merged 2 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Add support for parsing list expressions.

* Add support for parsing map expressions.

## 0.4.14

* Add support for parsing color expressions.
Expand Down
11 changes: 11 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ export {
ListSeparator,
NewNodeForListExpression,
} from './src/expression/list';
export {
MapEntry,
MapEntryProps,
MapEntryRaws,
} from './src/expression/map-entry';
export {
MapExpression,
MapExpressionProps,
MapExpressionRaws,
NewNodeForMapExpression,
} from './src/expression/map';
export {
NumberExpression,
NumberExpressionProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a map entry toJSON 1`] = `
{
"inputs": [
{
"css": "@#{(baz: qux)}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"key": <baz>,
"raws": {},
"sassType": "map-entry",
"value": <qux>,
}
`;
20 changes: 20 additions & 0 deletions pkg/sass-parser/lib/src/expression/__snapshots__/map.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a map expression toJSON 1`] = `
{
"inputs": [
{
"css": "@#{(foo: bar, baz: bang)}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"nodes": [
<foo: bar>,
<baz: bang>,
],
"raws": {},
"sassType": "map",
"source": <1:4-1:25 in 0>,
}
`;
2 changes: 2 additions & 0 deletions pkg/sass-parser/lib/src/expression/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {BinaryOperationExpression} from './binary-operation';
import {BooleanExpression} from './boolean';
import {ColorExpression} from './color';
import {ListExpression} from './list';
import {MapExpression} from './map';
import {NumberExpression} from './number';
import {StringExpression} from './string';

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

Expand Down
2 changes: 2 additions & 0 deletions pkg/sass-parser/lib/src/expression/from-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {BinaryOperationExpression} from './binary-operation';
import {BooleanExpression} from './boolean';
import {ColorExpression} from './color';
import {ListExpression} from './list';
import {MapExpression} from './map';
import {NumberExpression} from './number';
import {StringExpression} from './string';

Expand All @@ -16,6 +17,7 @@ export function fromProps(props: ExpressionProps): Expression {
if ('text' in props) return new StringExpression(props);
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 ('value' in props) {
if (typeof props.value === 'boolean') return new BooleanExpression(props);
if (typeof props.value === 'number') return new NumberExpression(props);
Expand Down
4 changes: 4 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,7 @@ import type {
import {BooleanExpression, BooleanExpressionProps} from './boolean';
import {ColorExpression, ColorExpressionProps} from './color';
import {ListExpression, ListExpressionProps} from './list';
import {MapExpression, MapExpressionProps} from './map';
import {NumberExpression, NumberExpressionProps} from './number';
import type {StringExpression, StringExpressionProps} from './string';

Expand All @@ -23,6 +24,7 @@ export type AnyExpression =
| BooleanExpression
| ColorExpression
| ListExpression
| MapExpression
| NumberExpression
| StringExpression;

Expand All @@ -36,6 +38,7 @@ export type ExpressionType =
| 'boolean'
| 'color'
| 'list'
| 'map'
| 'number'
| 'string';

Expand All @@ -50,6 +53,7 @@ export type ExpressionProps =
| BooleanExpressionProps
| ColorExpressionProps
| ListExpressionProps
| MapExpressionProps
| NumberExpressionProps
| StringExpressionProps;

Expand Down
206 changes: 206 additions & 0 deletions pkg/sass-parser/lib/src/expression/map-entry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// 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 {MapEntry, MapExpression, StringExpression} from '../..';
import * as utils from '../../../test/utils';

describe('a map entry', () => {
let node: MapEntry;
beforeEach(
() =>
void (node = new MapEntry({
key: {text: 'foo'},
value: {text: 'bar'},
})),
);

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

it('has a sassType', () =>
expect(node.sassType.toString()).toBe('map-entry'));

it('has a key', () => expect(node).toHaveStringExpression('key', 'foo'));

it('has a value', () =>
expect(node).toHaveStringExpression('value', 'bar'));
});
}

describeNode(
'parsed',
() => (utils.parseExpression('(foo: bar)') as MapExpression).nodes[0],
);

describe('constructed manually', () => {
describe('with an array', () => {
describeNode(
'with two Expressions',
() =>
new MapEntry([
new StringExpression({text: 'foo'}),
new StringExpression({text: 'bar'}),
]),
);

describeNode(
'with two ExpressionProps',
() => new MapEntry([{text: 'foo'}, {text: 'bar'}]),
);

describeNode(
'with mixed Expressions and ExpressionProps',
() =>
new MapEntry([{text: 'foo'}, new StringExpression({text: 'bar'})]),
);
});

describe('with an object', () => {
describeNode(
'with two Expressions',
() =>
new MapEntry({
key: new StringExpression({text: 'foo'}),
value: new StringExpression({text: 'bar'}),
}),
);

describeNode(
'with ExpressionProps',
() => new MapEntry({key: {text: 'foo'}, value: {text: 'bar'}}),
);
});
});

it('assigned a new key', () => {
const old = node.key;
node.key = {text: 'baz'};
expect(old.parent).toBeUndefined();
expect(node).toHaveStringExpression('key', 'baz');
});

it('assigned a new value', () => {
const old = node.value;
node.value = {text: 'baz'};
expect(old.parent).toBeUndefined();
expect(node).toHaveStringExpression('value', 'baz');
});

describe('stringifies', () => {
describe('to SCSS', () => {
it('with default raws', () =>
expect(
new MapEntry({
key: {text: 'foo'},
value: {text: 'bar'},
}).toString(),
).toBe('foo: bar'));

// raws.before is only used as part of a MapExpression
it('ignores before', () =>
expect(
new MapEntry({
key: {text: 'foo'},
value: {text: 'bar'},
raws: {before: '/**/'},
}).toString(),
).toBe('foo: bar'));

it('with between', () =>
expect(
new MapEntry({
key: {text: 'foo'},
value: {text: 'bar'},
raws: {between: ' : '},
}).toString(),
).toBe('foo : bar'));

// raws.after is only used as part of a Configuration
it('ignores after', () =>
expect(
new MapEntry({
key: {text: 'foo'},
value: {text: 'bar'},
raws: {after: '/**/'},
}).toString(),
).toBe('foo: bar'));
});
});

describe('clone()', () => {
let original: MapEntry;
beforeEach(() => {
original = (utils.parseExpression('(foo: bar)') as MapExpression)
.nodes[0];
original.raws.between = ' : ';
});

describe('with no overrides', () => {
let clone: MapEntry;
beforeEach(() => void (clone = original.clone()));

describe('has the same properties:', () => {
it('key', () => expect(clone).toHaveStringExpression('key', 'foo'));

it('value', () => expect(clone).toHaveStringExpression('value', 'bar'));
});

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

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

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

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

describe('key', () => {
it('defined', () =>
expect(original.clone({key: {text: 'baz'}})).toHaveStringExpression(
'key',
'baz',
));

it('undefined', () =>
expect(original.clone({key: undefined})).toHaveStringExpression(
'key',
'foo',
));
});

describe('value', () => {
it('defined', () =>
expect(original.clone({value: {text: 'baz'}})).toHaveStringExpression(
'value',
'baz',
));

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

it('toJSON', () =>
expect(
(utils.parseExpression('(baz: qux)') as MapExpression).nodes[0],
).toMatchSnapshot());
});
Loading