Skip to content

Commit

Permalink
Add support for map expressions (#2517)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlos (Goodwine) <[email protected]>
  • Loading branch information
nex3 and Goodwine authored Feb 20, 2025
1 parent 1b58aa9 commit 7c4ff8f
Show file tree
Hide file tree
Showing 13 changed files with 1,707 additions and 0 deletions.
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

0 comments on commit 7c4ff8f

Please sign in to comment.