Skip to content

Add support for parsing selector expressions #2533

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

Merged
merged 3 commits into from
Mar 6, 2025
Merged
Changes from 1 commit
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
@@ -4,6 +4,8 @@

* Add support for parsing parenthesized expressions.

* Add support for parsing selector expressions.

## 0.4.15

* Add support for parsing list expressions.
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
@@ -238,6 +238,11 @@ export {
WhileRuleProps,
WhileRuleRaws,
} from './src/statement/while-rule';
export {
SelectorExpression,
SelectorExpressionProps,
SelectorExpressionRaws,
} from './src/expression/selector';
export {
StaticImport,
StaticImportProps,
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a selector expression toJSON 1`] = `
{
"inputs": [
{
"css": "@#{&}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "selector-expr",
"source": <1:4-1:5 in 0>,
}
`;
4 changes: 3 additions & 1 deletion pkg/sass-parser/lib/src/expression/convert.ts
Original file line number Diff line number Diff line change
@@ -16,13 +16,13 @@ import {MapExpression} from './map';
import {NullExpression} from './null';
import {NumberExpression} from './number';
import {ParenthesizedExpression} from './parenthesized';
import {SelectorExpression} from './selector';
import {StringExpression} from './string';

/** The visitor to use to convert internal Sass nodes to JS. */
const visitor = sassInternal.createExpressionVisitor<Expression>({
visitBinaryOperationExpression: inner =>
new BinaryOperationExpression(undefined, inner),
visitStringExpression: inner => new StringExpression(undefined, inner),
visitBooleanExpression: inner => new BooleanExpression(undefined, inner),
visitColorExpression: inner => new ColorExpression(undefined, inner),
visitFunctionExpression: inner => new FunctionExpression(undefined, inner),
@@ -39,6 +39,8 @@ const visitor = sassInternal.createExpressionVisitor<Expression>({
visitNumberExpression: inner => new NumberExpression(undefined, inner),
visitParenthesizedExpression: inner =>
new ParenthesizedExpression(undefined, inner),
visitSelectorExpression: inner => new SelectorExpression(undefined, inner),
visitStringExpression: inner => new StringExpression(undefined, inner),
});

/** Converts an internal expression AST node into an external one. */
3 changes: 3 additions & 0 deletions pkg/sass-parser/lib/src/expression/index.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,7 @@ import {
ParenthesizedExpression,
ParenthesizedExpressionProps,
} from './parenthesized';
import type {SelectorExpression} from './selector';
import type {StringExpression, StringExpressionProps} from './string';

/**
@@ -40,6 +41,7 @@ export type AnyExpression =
| NullExpression
| NumberExpression
| ParenthesizedExpression
| SelectorExpression
| StringExpression;

/**
@@ -58,6 +60,7 @@ export type ExpressionType =
| 'null'
| 'number'
| 'parenthesized'
| 'selector-expr'
| 'string';

/**
63 changes: 63 additions & 0 deletions pkg/sass-parser/lib/src/expression/selector.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 {SelectorExpression} from '../..';
import * as utils from '../../../test/utils';

describe('a selector expression', () => {
let node: SelectorExpression;

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

it('has sassType selector', () => expect(node.sassType).toBe('selector-expr'));

Check failure on line 18 in pkg/sass-parser/lib/src/expression/selector.test.ts

GitHub Actions / test / sass-parser Static Analysis

Insert `⏎·······`
});
}

describeNode('parsed', () => utils.parseExpression('&'));

describe('constructed manually', () => {
describeNode('without props', () => new SelectorExpression());

describeNode('with empty props', () => new SelectorExpression({}));
});

it('stringifies', () => expect(new SelectorExpression().toString()).toBe('&'));

Check failure on line 30 in pkg/sass-parser/lib/src/expression/selector.test.ts

GitHub Actions / test / sass-parser Static Analysis

Insert `⏎···`

describe('clone', () => {
let original: SelectorExpression;

beforeEach(() => void (original = utils.parseExpression('&')));

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

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

describe('has the same properties:', () => {
it('raws', () => expect(clone.raws).toEqual({}));

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

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

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

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

it('toJSON', () => expect(utils.parseExpression('&')).toMatchSnapshot());
});
70 changes: 70 additions & 0 deletions pkg/sass-parser/lib/src/expression/selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 * as postcss from 'postcss';

import {LazySource} from '../lazy-source';
import {NodeProps} from '../node';
import type * as sassInternal from '../sass-internal';
import * as utils from '../utils';
import {Expression} from '.';

/**
* The initializer properties for {@link SelectorExpression}.
*
* Unlike other expression types, this can't be initialized by properties alone,
* since it doesn't have any properties to set.
*
* @category Expression
*/
export interface SelectorExpressionProps extends NodeProps {
raws?: SelectorExpressionRaws;
}

/**
* Raws indicating how to precisely serialize a {@link SelectorExpression}.
*
* @category Expression
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- No raws for a selector expression yet.
export interface SelectorExpressionRaws {}

/**
* An expression representing the current selector in Sass.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: to match the Dart library sass_api docs, mention this is specifically &

*
* @category Expression
*/
export class SelectorExpression extends Expression {
readonly sassType = 'selector-expr' as const;
declare raws: SelectorExpressionRaws;

constructor(defaults?: SelectorExpressionProps);
/** @hidden */
constructor(_: undefined, inner: sassInternal.SelectorExpression);
constructor(defaults?: object, inner?: sassInternal.SelectorExpression) {
super(defaults);
if (inner) this.source = new LazySource(inner);
}

clone(overrides?: Partial<SelectorExpressionProps>): this {
return utils.cloneNode(this, overrides, ['raws']);
}

toJSON(): object;
/** @hidden */
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
return utils.toJSON(this, [], inputs);
}

/** @hidden */
toString(): string {
return '&';
}

/** @hidden */
get nonStatementChildren(): ReadonlyArray<Expression> {
return [];
}
}
16 changes: 10 additions & 6 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
@@ -359,11 +359,6 @@ declare namespace SassInternal {
readonly pairs: DartPair<Expression, Expression>[];
}

class StringExpression extends Expression {
readonly text: Interpolation;
readonly hasQuotes: boolean;
}

class BooleanExpression extends Expression {
readonly value: boolean;
}
@@ -382,6 +377,13 @@ declare namespace SassInternal {
class ParenthesizedExpression extends Expression {
readonly expression: Expression;
}

class SelectorExpression extends Expression {}

class StringExpression extends Expression {
readonly text: Interpolation;
readonly hasQuotes: boolean;
}
}

const sassInternal = (
@@ -437,12 +439,13 @@ export type InterpolatedFunctionExpression =
export type ListExpression = SassInternal.ListExpression;
export type ListSeparator = SassInternal.ListSeparator;
export type MapExpression = SassInternal.MapExpression;
export type StringExpression = SassInternal.StringExpression;
export type BooleanExpression = SassInternal.BooleanExpression;
export type ColorExpression = SassInternal.ColorExpression;
export type NullExpression = SassInternal.NullExpression;
export type NumberExpression = SassInternal.NumberExpression;
export type ParenthesizedExpression = SassInternal.ParenthesizedExpression;
export type SelectorExpression = SassInternal.SelectorExpression;
export type StringExpression = SassInternal.StringExpression;

export interface StatementVisitorObject<T> {
visitAtRootRule(node: AtRootRule): T;
@@ -484,6 +487,7 @@ export interface ExpressionVisitorObject<T> {
visitNullExpression(node: NullExpression): T;
visitNumberExpression(node: NumberExpression): T;
visitParenthesizedExpression(node: ParenthesizedExpression): T;
visitSelectorExpression(node: SelectorExpression): T;
visitStringExpression(node: StringExpression): T;
}