Skip to content

Commit 2825244

Browse files
committed
Add boolean expressions to the Sass parser
1 parent d58e219 commit 2825244

File tree

10 files changed

+270
-3
lines changed

10 files changed

+270
-3
lines changed

pkg/sass-parser/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 0.2.5
22

33
* Add support for parsing the `@supports` rule.
4+
* Add `BooleanExpression` and `NumberExpression`.
45

56
## 0.2.4
67

pkg/sass-parser/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,13 @@ There are a few cases where an operation that's valid in PostCSS won't work with
257257

258258
* Trying to add child nodes to a Sass statement that doesn't support children
259259
like `@use` or `@error` is not supported.
260+
261+
## Contributing
262+
263+
Before sending out a pull request, please run the following commands from the
264+
`sass-parser` directory:
265+
266+
* `npm run check` - Runs `eslint`, and then tries to compile the package with
267+
`tsc`.
268+
269+
* `npm run test` - Runs all tests in the package.

pkg/sass-parser/lib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export {
2727
StringExpressionProps,
2828
StringExpressionRaws,
2929
} from './src/expression/string';
30+
export {
31+
BooleanExpression,
32+
BooleanExpressionProps,
33+
BooleanExpressionRaws,
34+
} from './src/expression/boolean';
3035
export {
3136
Interpolation,
3237
InterpolationProps,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a boolean expression toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@#{true}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"raws": {},
13+
"sassType": "boolean",
14+
"source": <1:4-1:8 in 0>,
15+
"value": true,
16+
}
17+
`;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2024 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 {BooleanExpression} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a boolean expression', () => {
9+
let node: BooleanExpression;
10+
11+
describe('true', () => {
12+
function describeNode(
13+
description: string,
14+
create: () => BooleanExpression
15+
): void {
16+
describe(description, () => {
17+
beforeEach(() => void (node = create()));
18+
19+
it('has sassType boolean', () => expect(node.sassType).toBe('boolean'));
20+
21+
it('is true', () => expect(node.value).toBe(true));
22+
});
23+
}
24+
25+
describeNode('parsed', () => utils.parseExpression('true'));
26+
27+
describeNode(
28+
'constructed manually',
29+
() =>
30+
new BooleanExpression({
31+
value: true,
32+
})
33+
);
34+
35+
describeNode('constructed from ExpressionProps', () =>
36+
utils.fromExpressionProps({
37+
value: true,
38+
})
39+
);
40+
});
41+
42+
describe('false', () => {
43+
function describeNode(
44+
description: string,
45+
create: () => BooleanExpression
46+
): void {
47+
describe(description, () => {
48+
beforeEach(() => void (node = create()));
49+
50+
it('has sassType boolean', () => expect(node.sassType).toBe('boolean'));
51+
52+
it('is false', () => expect(node.value).toBe(false));
53+
});
54+
}
55+
56+
describeNode('parsed', () => utils.parseExpression('false'));
57+
58+
describeNode(
59+
'constructed manually',
60+
() =>
61+
new BooleanExpression({
62+
value: false,
63+
})
64+
);
65+
66+
describeNode('constructed from ExpressionProps', () =>
67+
utils.fromExpressionProps({
68+
value: false,
69+
})
70+
);
71+
});
72+
73+
describe('assigned new', () => {
74+
beforeEach(() => void (node = utils.parseExpression('true')));
75+
76+
it('value', () => {
77+
node.value = false;
78+
expect(node.value).toBe(false);
79+
});
80+
});
81+
82+
describe('stringifies', () => {
83+
it('true', () => {
84+
expect(utils.parseExpression('true').toString()).toBe('true');
85+
});
86+
87+
it('false', () => {
88+
expect(utils.parseExpression('false').toString()).toBe('false');
89+
});
90+
});
91+
92+
describe('clone', () => {
93+
let original: BooleanExpression;
94+
95+
beforeEach(() => {
96+
original = utils.parseExpression('true');
97+
});
98+
99+
describe('with no overrides', () => {
100+
let clone: BooleanExpression;
101+
102+
beforeEach(() => void (clone = original.clone()));
103+
104+
describe('has the same properties:', () => {
105+
it('value', () => expect(clone.value).toBe(true));
106+
107+
it('raws', () => expect(clone.raws).toEqual({}));
108+
109+
it('source', () => expect(clone.source).toBe(original.source));
110+
});
111+
112+
describe('creates a new', () => {
113+
it('self', () => expect(clone).not.toBe(original));
114+
});
115+
});
116+
117+
describe('overrides', () => {
118+
describe('value', () => {
119+
it('defined', () =>
120+
expect(original.clone({value: false}).value).toBe(false));
121+
122+
it('undefined', () =>
123+
expect(original.clone({value: undefined}).value).toBe(true));
124+
});
125+
126+
describe('raws', () => {
127+
it('defined', () =>
128+
expect(original.clone({raws: {}}).raws).toEqual({}));
129+
130+
it('undefined', () =>
131+
expect(original.clone({raws: undefined}).raws).toEqual({}));
132+
});
133+
});
134+
});
135+
136+
it('toJSON', () => expect(utils.parseExpression('true')).toMatchSnapshot());
137+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2024 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 type * as sassInternal from '../sass-internal';
9+
import * as utils from '../utils';
10+
import {Expression} from '.';
11+
12+
/**
13+
* The initializer properties for {@link BooleanExpression}.
14+
*
15+
* @category Expression
16+
*/
17+
export interface BooleanExpressionProps {
18+
value: boolean;
19+
raws?: BooleanExpressionRaws;
20+
}
21+
22+
/**
23+
* Raws indicating how to precisely serialize a {@link BooleanExpression}.
24+
*
25+
* @category Expression
26+
*/
27+
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- No raws for a boolean expression yet.
28+
export interface BooleanExpressionRaws {}
29+
30+
/**
31+
* An expression representing a boolean literal in Sass.
32+
*
33+
* @category Expression
34+
*/
35+
export class BooleanExpression extends Expression {
36+
readonly sassType = 'boolean' as const;
37+
declare raws: BooleanExpressionRaws;
38+
39+
/** The boolean value of this expression. */
40+
get value(): boolean {
41+
return this._value;
42+
}
43+
set value(value: boolean) {
44+
// TODO - postcss/postcss#1957: Mark this as dirty
45+
this._value = value;
46+
}
47+
private _value!: boolean;
48+
49+
constructor(defaults: BooleanExpressionProps);
50+
/** @hidden */
51+
constructor(_: undefined, inner: sassInternal.BooleanExpression);
52+
constructor(defaults?: object, inner?: sassInternal.BooleanExpression) {
53+
super(defaults);
54+
if (inner) {
55+
this.source = new LazySource(inner);
56+
this.value = inner.value;
57+
} else {
58+
this.value ??= false;
59+
}
60+
}
61+
62+
clone(overrides?: Partial<BooleanExpressionProps>): this {
63+
return utils.cloneNode(this, overrides, ['raws', 'value']);
64+
}
65+
66+
toJSON(): object;
67+
/** @hidden */
68+
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
69+
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
70+
return utils.toJSON(this, ['value'], inputs);
71+
}
72+
73+
/** @hidden */
74+
toString(): string {
75+
return this.value ? 'true' : 'false';
76+
}
77+
78+
/** @hidden */
79+
get nonStatementChildren(): ReadonlyArray<Expression> {
80+
return [];
81+
}
82+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import * as sassInternal from '../sass-internal';
77
import {BinaryOperationExpression} from './binary-operation';
88
import {StringExpression} from './string';
99
import {Expression} from '.';
10+
import {BooleanExpression} from './boolean';
1011

1112
/** The visitor to use to convert internal Sass nodes to JS. */
1213
const visitor = sassInternal.createExpressionVisitor<Expression>({
1314
visitBinaryOperationExpression: inner =>
1415
new BinaryOperationExpression(undefined, inner),
1516
visitStringExpression: inner => new StringExpression(undefined, inner),
17+
visitBooleanExpression: inner => new BooleanExpression(undefined, inner),
1618
});
1719

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

pkg/sass-parser/lib/src/expression/from-props.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import {BinaryOperationExpression} from './binary-operation';
66
import {Expression, ExpressionProps} from '.';
77
import {StringExpression} from './string';
8+
import {BooleanExpression} from './boolean';
89

910
/** Constructs an expression from {@link ExpressionProps}. */
1011
export function fromProps(props: ExpressionProps): Expression {
1112
if ('text' in props) return new StringExpression(props);
1213
if ('left' in props) return new BinaryOperationExpression(props);
14+
if ('value' in props) return new BooleanExpression(props);
1315
throw new Error(`Unknown node type: ${props}`);
1416
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@ import type {
77
BinaryOperationExpression,
88
BinaryOperationExpressionProps,
99
} from './binary-operation';
10+
import {BooleanExpressionProps} from './boolean';
1011
import type {StringExpression, StringExpressionProps} from './string';
1112

1213
/**
1314
* The union type of all Sass expressions.
1415
*
1516
* @category Expression
1617
*/
17-
export type AnyExpression = BinaryOperationExpression | StringExpression;
18+
export type AnyExpression =
19+
| BinaryOperationExpression
20+
| StringExpression
21+
| BooleanExpressionProps;
1822

1923
/**
2024
* Sass expression types.
2125
*
2226
* @category Expression
2327
*/
24-
export type ExpressionType = 'binary-operation' | 'string';
28+
export type ExpressionType = 'binary-operation' | 'string' | 'boolean';
2529

2630
/**
2731
* The union type of all properties that can be used to construct Sass
@@ -31,7 +35,8 @@ export type ExpressionType = 'binary-operation' | 'string';
3135
*/
3236
export type ExpressionProps =
3337
| BinaryOperationExpressionProps
34-
| StringExpressionProps;
38+
| StringExpressionProps
39+
| BooleanExpressionProps;
3540

3641
/**
3742
* The superclass of Sass expression nodes.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ declare namespace SassInternal {
191191
readonly text: Interpolation;
192192
readonly hasQuotes: boolean;
193193
}
194+
195+
class BooleanExpression extends Expression {
196+
readonly value: boolean;
197+
}
194198
}
195199

196200
const sassInternal = (
@@ -218,6 +222,7 @@ export type Interpolation = SassInternal.Interpolation;
218222
export type Expression = SassInternal.Expression;
219223
export type BinaryOperationExpression = SassInternal.BinaryOperationExpression;
220224
export type StringExpression = SassInternal.StringExpression;
225+
export type BooleanExpression = SassInternal.BooleanExpression;
221226

222227
export interface StatementVisitorObject<T> {
223228
visitAtRootRule(node: AtRootRule): T;
@@ -237,6 +242,7 @@ export interface StatementVisitorObject<T> {
237242
export interface ExpressionVisitorObject<T> {
238243
visitBinaryOperationExpression(node: BinaryOperationExpression): T;
239244
visitStringExpression(node: StringExpression): T;
245+
visitBooleanExpression(node: BooleanExpression): T;
240246
}
241247

242248
export const parse = sassInternal.parse;

0 commit comments

Comments
 (0)