Skip to content

Commit ddd4b23

Browse files
authored
Add support for parsing selector expressions (#2533)
1 parent d067c3a commit ddd4b23

File tree

8 files changed

+174
-7
lines changed

8 files changed

+174
-7
lines changed

pkg/sass-parser/CHANGELOG.md

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

55
* Add support for parsing parenthesized expressions.
66

7+
* Add support for parsing selector expressions.
8+
79
## 0.4.15
810

911
* Add support for parsing list expressions.

pkg/sass-parser/lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ export {
238238
WhileRuleProps,
239239
WhileRuleRaws,
240240
} from './src/statement/while-rule';
241+
export {
242+
SelectorExpression,
243+
SelectorExpressionProps,
244+
SelectorExpressionRaws,
245+
} from './src/expression/selector';
241246
export {
242247
StaticImport,
243248
StaticImportProps,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a selector expression toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@#{&}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"raws": {},
13+
"sassType": "selector-expr",
14+
"source": <1:4-1:5 in 0>,
15+
}
16+
`;

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import {MapExpression} from './map';
1616
import {NullExpression} from './null';
1717
import {NumberExpression} from './number';
1818
import {ParenthesizedExpression} from './parenthesized';
19+
import {SelectorExpression} from './selector';
1920
import {StringExpression} from './string';
2021

2122
/** The visitor to use to convert internal Sass nodes to JS. */
2223
const visitor = sassInternal.createExpressionVisitor<Expression>({
2324
visitBinaryOperationExpression: inner =>
2425
new BinaryOperationExpression(undefined, inner),
25-
visitStringExpression: inner => new StringExpression(undefined, inner),
2626
visitBooleanExpression: inner => new BooleanExpression(undefined, inner),
2727
visitColorExpression: inner => new ColorExpression(undefined, inner),
2828
visitFunctionExpression: inner => new FunctionExpression(undefined, inner),
@@ -39,6 +39,8 @@ const visitor = sassInternal.createExpressionVisitor<Expression>({
3939
visitNumberExpression: inner => new NumberExpression(undefined, inner),
4040
visitParenthesizedExpression: inner =>
4141
new ParenthesizedExpression(undefined, inner),
42+
visitSelectorExpression: inner => new SelectorExpression(undefined, inner),
43+
visitStringExpression: inner => new StringExpression(undefined, inner),
4244
});
4345

4446
/** Converts an internal expression AST node into an external one. */

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

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ParenthesizedExpression,
2323
ParenthesizedExpressionProps,
2424
} from './parenthesized';
25+
import type {SelectorExpression} from './selector';
2526
import type {StringExpression, StringExpressionProps} from './string';
2627

2728
/**
@@ -40,6 +41,7 @@ export type AnyExpression =
4041
| NullExpression
4142
| NumberExpression
4243
| ParenthesizedExpression
44+
| SelectorExpression
4345
| StringExpression;
4446

4547
/**
@@ -58,6 +60,7 @@ export type ExpressionType =
5860
| 'null'
5961
| 'number'
6062
| 'parenthesized'
63+
| 'selector-expr'
6164
| 'string';
6265

6366
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 {SelectorExpression} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a selector expression', () => {
9+
let node: SelectorExpression;
10+
11+
function describeNode(
12+
description: string,
13+
create: () => SelectorExpression,
14+
): void {
15+
describe(description, () => {
16+
beforeEach(() => void (node = create()));
17+
18+
it('has sassType selector', () =>
19+
expect(node.sassType).toBe('selector-expr'));
20+
});
21+
}
22+
23+
describeNode('parsed', () => utils.parseExpression('&'));
24+
25+
describe('constructed manually', () => {
26+
describeNode('without props', () => new SelectorExpression());
27+
28+
describeNode('with empty props', () => new SelectorExpression({}));
29+
});
30+
31+
it('stringifies', () =>
32+
expect(new SelectorExpression().toString()).toBe('&'));
33+
34+
describe('clone', () => {
35+
let original: SelectorExpression;
36+
37+
beforeEach(() => void (original = utils.parseExpression('&')));
38+
39+
describe('with no overrides', () => {
40+
let clone: SelectorExpression;
41+
42+
beforeEach(() => void (clone = original.clone()));
43+
44+
describe('has the same properties:', () => {
45+
it('raws', () => expect(clone.raws).toEqual({}));
46+
47+
it('source', () => expect(clone.source).toBe(original.source));
48+
});
49+
50+
it('creates a new self', () => expect(clone).not.toBe(original));
51+
});
52+
53+
describe('overrides', () => {
54+
describe('raws', () => {
55+
it('defined', () =>
56+
expect(original.clone({raws: {}}).raws).toEqual({}));
57+
58+
it('undefined', () =>
59+
expect(original.clone({raws: undefined}).raws).toEqual({}));
60+
});
61+
});
62+
});
63+
64+
it('toJSON', () => expect(utils.parseExpression('&')).toMatchSnapshot());
65+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 * as postcss from 'postcss';
6+
7+
import {LazySource} from '../lazy-source';
8+
import {NodeProps} from '../node';
9+
import type * as sassInternal from '../sass-internal';
10+
import * as utils from '../utils';
11+
import {Expression} from '.';
12+
13+
/**
14+
* The initializer properties for {@link SelectorExpression}.
15+
*
16+
* Unlike other expression types, this can't be initialized by properties alone,
17+
* since it doesn't have any properties to set.
18+
*
19+
* @category Expression
20+
*/
21+
export interface SelectorExpressionProps extends NodeProps {
22+
raws?: SelectorExpressionRaws;
23+
}
24+
25+
/**
26+
* Raws indicating how to precisely serialize a {@link SelectorExpression}.
27+
*
28+
* @category Expression
29+
*/
30+
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- No raws for a selector expression yet.
31+
export interface SelectorExpressionRaws {}
32+
33+
/**
34+
* An `&` expression representing the current selector in Sass.
35+
*
36+
* @category Expression
37+
*/
38+
export class SelectorExpression extends Expression {
39+
readonly sassType = 'selector-expr' as const;
40+
declare raws: SelectorExpressionRaws;
41+
42+
constructor(defaults?: SelectorExpressionProps);
43+
/** @hidden */
44+
constructor(_: undefined, inner: sassInternal.SelectorExpression);
45+
constructor(defaults?: object, inner?: sassInternal.SelectorExpression) {
46+
super(defaults);
47+
if (inner) this.source = new LazySource(inner);
48+
}
49+
50+
clone(overrides?: Partial<SelectorExpressionProps>): this {
51+
return utils.cloneNode(this, overrides, ['raws']);
52+
}
53+
54+
toJSON(): object;
55+
/** @hidden */
56+
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
57+
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
58+
return utils.toJSON(this, [], inputs);
59+
}
60+
61+
/** @hidden */
62+
toString(): string {
63+
return '&';
64+
}
65+
66+
/** @hidden */
67+
get nonStatementChildren(): ReadonlyArray<Expression> {
68+
return [];
69+
}
70+
}

pkg/sass-parser/lib/src/sass-internal.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,6 @@ declare namespace SassInternal {
359359
readonly pairs: DartPair<Expression, Expression>[];
360360
}
361361

362-
class StringExpression extends Expression {
363-
readonly text: Interpolation;
364-
readonly hasQuotes: boolean;
365-
}
366-
367362
class BooleanExpression extends Expression {
368363
readonly value: boolean;
369364
}
@@ -382,6 +377,13 @@ declare namespace SassInternal {
382377
class ParenthesizedExpression extends Expression {
383378
readonly expression: Expression;
384379
}
380+
381+
class SelectorExpression extends Expression {}
382+
383+
class StringExpression extends Expression {
384+
readonly text: Interpolation;
385+
readonly hasQuotes: boolean;
386+
}
385387
}
386388

387389
const sassInternal = (
@@ -437,12 +439,13 @@ export type InterpolatedFunctionExpression =
437439
export type ListExpression = SassInternal.ListExpression;
438440
export type ListSeparator = SassInternal.ListSeparator;
439441
export type MapExpression = SassInternal.MapExpression;
440-
export type StringExpression = SassInternal.StringExpression;
441442
export type BooleanExpression = SassInternal.BooleanExpression;
442443
export type ColorExpression = SassInternal.ColorExpression;
443444
export type NullExpression = SassInternal.NullExpression;
444445
export type NumberExpression = SassInternal.NumberExpression;
445446
export type ParenthesizedExpression = SassInternal.ParenthesizedExpression;
447+
export type SelectorExpression = SassInternal.SelectorExpression;
448+
export type StringExpression = SassInternal.StringExpression;
446449

447450
export interface StatementVisitorObject<T> {
448451
visitAtRootRule(node: AtRootRule): T;
@@ -484,6 +487,7 @@ export interface ExpressionVisitorObject<T> {
484487
visitNullExpression(node: NullExpression): T;
485488
visitNumberExpression(node: NumberExpression): T;
486489
visitParenthesizedExpression(node: ParenthesizedExpression): T;
490+
visitSelectorExpression(node: SelectorExpression): T;
487491
visitStringExpression(node: StringExpression): T;
488492
}
489493

0 commit comments

Comments
 (0)