Skip to content

Commit a9254df

Browse files
authored
Add sass-parser support for the @while rule (#2410)
1 parent df77b66 commit a9254df

File tree

8 files changed

+428
-11
lines changed

8 files changed

+428
-11
lines changed

pkg/sass-parser/CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## 0.4.3-dev
22

3-
* No user-visible changes.
3+
* Add support for parsing the `@while` rule.
44

55
## 0.4.2
66

pkg/sass-parser/lib/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ export {
102102
VariableDeclarationRaws,
103103
} from './src/statement/variable-declaration';
104104
export {WarnRule, WarnRuleProps, WarnRuleRaws} from './src/statement/warn-rule';
105+
export {
106+
WhileRule,
107+
WhileRuleProps,
108+
WhileRuleRaws,
109+
} from './src/statement/while-rule';
105110

106111
/** Options that can be passed to the Sass parsers to control their behavior. */
107112
export type SassParserOptions = Pick<postcss.ProcessOptions, 'from' | 'map'>;

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

+6
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ declare namespace SassInternal {
192192
readonly expression: Expression;
193193
}
194194

195+
class WhileRule extends ParentStatement<Statement[]> {
196+
readonly condition: Expression;
197+
}
198+
195199
class ConfiguredVariable extends SassNode {
196200
readonly name: string;
197201
readonly expression: Expression;
@@ -252,6 +256,7 @@ export type SupportsRule = SassInternal.SupportsRule;
252256
export type UseRule = SassInternal.UseRule;
253257
export type VariableDeclaration = SassInternal.VariableDeclaration;
254258
export type WarnRule = SassInternal.WarnRule;
259+
export type WhileRule = SassInternal.WhileRule;
255260
export type ConfiguredVariable = SassInternal.ConfiguredVariable;
256261
export type Interpolation = SassInternal.Interpolation;
257262
export type Expression = SassInternal.Expression;
@@ -276,6 +281,7 @@ export interface StatementVisitorObject<T> {
276281
visitUseRule(node: UseRule): T;
277282
visitVariableDeclaration(node: VariableDeclaration): T;
278283
visitWarnRule(node: WarnRule): T;
284+
visitWhileRule(node: WhileRule): T;
279285
}
280286

281287
export interface ExpressionVisitorObject<T> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a @while rule toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@while foo {}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"name": "while",
13+
"nodes": [],
14+
"params": "foo",
15+
"raws": {},
16+
"sassType": "while-rule",
17+
"source": <1:1-1:14 in 0>,
18+
"type": "atrule",
19+
"whileCondition": <foo>,
20+
}
21+
`;

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

+10-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
VariableDeclarationProps,
2424
} from './variable-declaration';
2525
import {WarnRule, WarnRuleProps} from './warn-rule';
26+
import {WhileRule, WhileRuleProps} from './while-rule';
2627

2728
// TODO: Replace this with the corresponding Sass types once they're
2829
// implemented.
@@ -56,7 +57,8 @@ export type StatementType =
5657
| 'use-rule'
5758
| 'sass-comment'
5859
| 'variable-declaration'
59-
| 'warn-rule';
60+
| 'warn-rule'
61+
| 'while-rule';
6062

6163
/**
6264
* All Sass statements that are also at-rules.
@@ -70,7 +72,8 @@ export type AtRule =
7072
| ForRule
7173
| GenericAtRule
7274
| UseRule
73-
| WarnRule;
75+
| WarnRule
76+
| WhileRule;
7477

7578
/**
7679
* All Sass statements that are comments.
@@ -107,7 +110,8 @@ export type ChildProps =
107110
| SassCommentChildProps
108111
| UseRuleProps
109112
| VariableDeclarationProps
110-
| WarnRuleProps;
113+
| WarnRuleProps
114+
| WhileRuleProps;
111115

112116
/**
113117
* The Sass eqivalent of PostCSS's `ContainerProps`.
@@ -197,6 +201,7 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
197201
visitUseRule: inner => new UseRule(undefined, inner),
198202
visitVariableDeclaration: inner => new VariableDeclaration(undefined, inner),
199203
visitWarnRule: inner => new WarnRule(undefined, inner),
204+
visitWhileRule: inner => new WhileRule(undefined, inner),
200205
});
201206

202207
/** Appends parsed versions of `internal`'s children to `container`. */
@@ -317,6 +322,8 @@ export function normalize(
317322
result.push(new VariableDeclaration(node));
318323
} else if ('warnExpression' in node) {
319324
result.push(new WarnRule(node));
325+
} else if ('whileCondition' in node) {
326+
result.push(new WhileRule(node));
320327
} else {
321328
result.push(...postcssNormalizeAndConvertToSass(self, node, sample));
322329
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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 {GenericAtRule, StringExpression, WhileRule, sass, scss} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a @while rule', () => {
9+
let node: WhileRule;
10+
describe('with empty children', () => {
11+
function describeNode(description: string, create: () => WhileRule): void {
12+
describe(description, () => {
13+
beforeEach(() => void (node = create()));
14+
15+
it('has a name', () => expect(node.name.toString()).toBe('while'));
16+
17+
it('has an expression', () =>
18+
expect(node).toHaveStringExpression('whileCondition', 'foo'));
19+
20+
it('has matching params', () => expect(node.params).toBe('foo'));
21+
22+
it('has empty nodes', () => expect(node.nodes).toEqual([]));
23+
});
24+
}
25+
26+
describeNode(
27+
'parsed as SCSS',
28+
() => scss.parse('@while foo {}').nodes[0] as WhileRule,
29+
);
30+
31+
describeNode(
32+
'parsed as Sass',
33+
() => sass.parse('@while foo').nodes[0] as WhileRule,
34+
);
35+
36+
describeNode(
37+
'constructed manually',
38+
() =>
39+
new WhileRule({
40+
whileCondition: {text: 'foo'},
41+
}),
42+
);
43+
44+
describeNode('constructed from ChildProps', () =>
45+
utils.fromChildProps({
46+
whileCondition: {text: 'foo'},
47+
}),
48+
);
49+
});
50+
51+
describe('with a child', () => {
52+
function describeNode(description: string, create: () => WhileRule): void {
53+
describe(description, () => {
54+
beforeEach(() => void (node = create()));
55+
56+
it('has a name', () => expect(node.name.toString()).toBe('while'));
57+
58+
it('has an expression', () =>
59+
expect(node).toHaveStringExpression('whileCondition', 'foo'));
60+
61+
it('has matching params', () => expect(node.params).toBe('foo'));
62+
63+
it('has a child node', () => {
64+
expect(node.nodes).toHaveLength(1);
65+
expect(node.nodes[0]).toBeInstanceOf(GenericAtRule);
66+
expect(node.nodes[0]).toHaveProperty('name', 'child');
67+
});
68+
});
69+
}
70+
71+
describeNode(
72+
'parsed as SCSS',
73+
() => scss.parse('@while foo {@child}').nodes[0] as WhileRule,
74+
);
75+
76+
describeNode(
77+
'parsed as Sass',
78+
() => sass.parse('@while foo\n @child').nodes[0] as WhileRule,
79+
);
80+
81+
describeNode(
82+
'constructed manually',
83+
() =>
84+
new WhileRule({
85+
whileCondition: {text: 'foo'},
86+
nodes: [{name: 'child'}],
87+
}),
88+
);
89+
90+
describeNode('constructed from ChildProps', () =>
91+
utils.fromChildProps({
92+
whileCondition: {text: 'foo'},
93+
nodes: [{name: 'child'}],
94+
}),
95+
);
96+
});
97+
98+
describe('throws an error when assigned a new', () => {
99+
beforeEach(
100+
() => void (node = new WhileRule({whileCondition: {text: 'foo'}})),
101+
);
102+
103+
it('name', () => expect(() => (node.name = 'bar')).toThrow());
104+
105+
it('params', () => expect(() => (node.params = 'true')).toThrow());
106+
});
107+
108+
describe('assigned a new expression', () => {
109+
beforeEach(() => {
110+
node = scss.parse('@while foo {}').nodes[0] as WhileRule;
111+
});
112+
113+
it("removes the old expression's parent", () => {
114+
const oldExpression = node.whileCondition;
115+
node.whileCondition = {text: 'bar'};
116+
expect(oldExpression.parent).toBeUndefined();
117+
});
118+
119+
it("assigns the new expression's parent", () => {
120+
const expression = new StringExpression({text: 'bar'});
121+
node.whileCondition = expression;
122+
expect(expression.parent).toBe(node);
123+
});
124+
125+
it('assigns the expression explicitly', () => {
126+
const expression = new StringExpression({text: 'bar'});
127+
node.whileCondition = expression;
128+
expect(node.whileCondition).toBe(expression);
129+
});
130+
131+
it('assigns the expression as ExpressionProps', () => {
132+
node.whileCondition = {text: 'bar'};
133+
expect(node).toHaveStringExpression('whileCondition', 'bar');
134+
});
135+
});
136+
137+
describe('stringifies', () => {
138+
describe('to SCSS', () => {
139+
it('with default raws', () =>
140+
expect(
141+
new WhileRule({
142+
whileCondition: {text: 'foo'},
143+
}).toString(),
144+
).toBe('@while foo {}'));
145+
146+
it('with afterName', () =>
147+
expect(
148+
new WhileRule({
149+
whileCondition: {text: 'foo'},
150+
raws: {afterName: '/**/'},
151+
}).toString(),
152+
).toBe('@while/**/foo {}'));
153+
154+
it('with between', () =>
155+
expect(
156+
new WhileRule({
157+
whileCondition: {text: 'foo'},
158+
raws: {between: '/**/'},
159+
}).toString(),
160+
).toBe('@while foo/**/{}'));
161+
});
162+
});
163+
164+
describe('clone', () => {
165+
let original: WhileRule;
166+
beforeEach(() => {
167+
original = scss.parse('@while foo {}').nodes[0] as WhileRule;
168+
// TODO: remove this once raws are properly parsed
169+
original.raws.between = ' ';
170+
});
171+
172+
describe('with no overrides', () => {
173+
let clone: WhileRule;
174+
beforeEach(() => void (clone = original.clone()));
175+
176+
describe('has the same properties:', () => {
177+
it('params', () => expect(clone.params).toBe('foo'));
178+
179+
it('whileCondition', () =>
180+
expect(clone).toHaveStringExpression('whileCondition', 'foo'));
181+
182+
it('raws', () => expect(clone.raws).toEqual({between: ' '}));
183+
184+
it('source', () => expect(clone.source).toBe(original.source));
185+
});
186+
187+
describe('creates a new', () => {
188+
it('self', () => expect(clone).not.toBe(original));
189+
190+
for (const attr of ['whileCondition', 'raws'] as const) {
191+
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
192+
}
193+
});
194+
});
195+
196+
describe('overrides', () => {
197+
describe('raws', () => {
198+
it('defined', () =>
199+
expect(original.clone({raws: {afterName: ' '}}).raws).toEqual({
200+
afterName: ' ',
201+
}));
202+
203+
it('undefined', () =>
204+
expect(original.clone({raws: undefined}).raws).toEqual({
205+
between: ' ',
206+
}));
207+
});
208+
209+
describe('whileCondition', () => {
210+
describe('defined', () => {
211+
let clone: WhileRule;
212+
beforeEach(() => {
213+
clone = original.clone({whileCondition: {text: 'bar'}});
214+
});
215+
216+
it('changes params', () => expect(clone.params).toBe('bar'));
217+
218+
it('changes whileCondition', () =>
219+
expect(clone).toHaveStringExpression('whileCondition', 'bar'));
220+
});
221+
222+
describe('undefined', () => {
223+
let clone: WhileRule;
224+
beforeEach(() => {
225+
clone = original.clone({whileCondition: undefined});
226+
});
227+
228+
it('preserves params', () => expect(clone.params).toBe('foo'));
229+
230+
it('preserves whileCondition', () =>
231+
expect(clone).toHaveStringExpression('whileCondition', 'foo'));
232+
});
233+
});
234+
});
235+
});
236+
237+
it('toJSON', () =>
238+
expect(scss.parse('@while foo {}').nodes[0]).toMatchSnapshot());
239+
});

0 commit comments

Comments
 (0)