Skip to content

Commit 21819c3

Browse files
committedSep 4, 2024
Add support for @for rules
1 parent b60577e commit 21819c3

File tree

7 files changed

+688
-1
lines changed

7 files changed

+688
-1
lines changed
 

‎pkg/sass-parser/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export {
4444
ErrorRuleProps,
4545
ErrorRuleRaws,
4646
} from './src/statement/error-rule';
47+
export {ForRule, ForRuleProps, ForRuleRaws} from './src/statement/for-rule';
4748
export {
4849
GenericAtRule,
4950
GenericAtRuleProps,

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

+9
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ declare namespace SassInternal {
9494
readonly isOptional: boolean;
9595
}
9696

97+
class ForRule extends ParentStatement<Statement[]> {
98+
readonly variable: string;
99+
readonly from: Expression;
100+
readonly to: Expression;
101+
readonly isExclusive: boolean;
102+
}
103+
97104
class Stylesheet extends ParentStatement<Statement[]> {}
98105

99106
class StyleRule extends ParentStatement<Statement[]> {
@@ -135,6 +142,7 @@ export type DebugRule = SassInternal.DebugRule;
135142
export type EachRule = SassInternal.EachRule;
136143
export type ErrorRule = SassInternal.ErrorRule;
137144
export type ExtendRule = SassInternal.ExtendRule;
145+
export type ForRule = SassInternal.ForRule;
138146
export type Stylesheet = SassInternal.Stylesheet;
139147
export type StyleRule = SassInternal.StyleRule;
140148
export type Interpolation = SassInternal.Interpolation;
@@ -149,6 +157,7 @@ export interface StatementVisitorObject<T> {
149157
visitEachRule(node: EachRule): T;
150158
visitErrorRule(node: ErrorRule): T;
151159
visitExtendRule(node: ExtendRule): T;
160+
visitForRule(node: ForRule): T;
152161
visitStyleRule(node: StyleRule): T;
153162
}
154163

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`an @for rule toJSON 1`] = `
4+
{
5+
"fromExpression": <bar>,
6+
"inputs": [
7+
{
8+
"css": "@for $foo from bar to baz {}",
9+
"hasBOM": false,
10+
"id": "<input css _____>",
11+
},
12+
],
13+
"name": "for",
14+
"nodes": [],
15+
"params": "$foo from bar to baz",
16+
"raws": {},
17+
"sassType": "for-rule",
18+
"source": <1:1-1:29 in 0>,
19+
"to": "to",
20+
"toExpression": <baz>,
21+
"type": "atrule",
22+
"variable": "foo",
23+
}
24+
`;

‎pkg/sass-parser/lib/src/statement/for-rule.test.ts

+437
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
import type {AtRuleRaws} from 'postcss/lib/at-rule';
7+
8+
import {convertExpression} from '../expression/convert';
9+
import {Expression, ExpressionProps} from '../expression';
10+
import {fromProps} from '../expression/from-props';
11+
import {LazySource} from '../lazy-source';
12+
import type * as sassInternal from '../sass-internal';
13+
import * as utils from '../utils';
14+
import {
15+
ChildNode,
16+
ContainerProps,
17+
NewNode,
18+
Statement,
19+
StatementWithChildren,
20+
appendInternalChildren,
21+
normalize,
22+
} from '.';
23+
import {_AtRule} from './at-rule-internal';
24+
import {interceptIsClean} from './intercept-is-clean';
25+
import * as sassParser from '../..';
26+
27+
/**
28+
* The set of raws supported by {@link ForRule}.
29+
*
30+
* @category Statement
31+
*/
32+
export interface ForRuleRaws extends Omit<AtRuleRaws, 'params'> {
33+
/** The whitespace after {@link ForRule.variable}. */
34+
afterVariable?: string;
35+
36+
/** The whitespace after a {@link ForRule}'s `from` keyword. */
37+
afterFrom?: string;
38+
39+
/** The whitespace after {@link ForRule.fromExpression}. */
40+
afterFromExpression?: string;
41+
42+
/** The whitespace after a {@link ForRule}'s `to` or `through` keyword. */
43+
afterTo?: string;
44+
}
45+
46+
/**
47+
* The initializer properties for {@link ForRule}.
48+
*
49+
* @category Statement
50+
*/
51+
export type ForRuleProps = ContainerProps & {
52+
raws?: ForRuleRaws;
53+
variable: string;
54+
fromExpression: Expression | ExpressionProps;
55+
toExpression: Expression | ExpressionProps;
56+
to?: 'to' | 'through';
57+
};
58+
59+
/**
60+
* A `@for` rule. Extends [`postcss.AtRule`].
61+
*
62+
* [`postcss.AtRule`]: https://postcss.org/api/#atrule
63+
*
64+
* @category Statement
65+
*/
66+
export class ForRule
67+
extends _AtRule<Partial<ForRuleProps>>
68+
implements Statement
69+
{
70+
readonly sassType = 'for-rule' as const;
71+
declare parent: StatementWithChildren | undefined;
72+
declare raws: ForRuleRaws;
73+
declare nodes: ChildNode[];
74+
75+
/** The variabl names assigned for for iteration, without `"$"`. */
76+
declare variable: string;
77+
78+
/**
79+
* The keyword that appears before {@link toExpression}.
80+
*
81+
* If this is `"to"`, the loop is exclusive; if it's `"through"`, the loop is
82+
* inclusive. It defaults to `"to"` when creating a new `ForRule`.
83+
*/
84+
declare to: 'to' | 'through';
85+
86+
get name(): string {
87+
return 'for';
88+
}
89+
set name(value: string) {
90+
throw new Error("ForRule.name can't be overwritten.");
91+
}
92+
93+
get params(): string {
94+
return (
95+
`$${this.variable}${this.raws.afterVariable ?? ' '}from` +
96+
`${this.raws.afterFrom ?? ' '}${this.fromExpression}` +
97+
`${this.raws.afterFromExpression ?? ' '}${this.to}` +
98+
`${this.raws.afterTo ?? ' '}${this.toExpression}`
99+
);
100+
}
101+
set params(value: string | number | undefined) {
102+
throw new Error("ForRule.params can't be overwritten.");
103+
}
104+
105+
/** The expresison whose value is the starting point of the iteration. */
106+
get fromExpression(): Expression {
107+
return this._fromExpression!;
108+
}
109+
set fromExpression(fromExpression: Expression | ExpressionProps) {
110+
if (this._fromExpression) this._fromExpression.parent = undefined;
111+
if (!('sassType' in fromExpression)) {
112+
fromExpression = fromProps(fromExpression);
113+
}
114+
if (fromExpression) fromExpression.parent = this;
115+
this._fromExpression = fromExpression;
116+
}
117+
private _fromExpression?: Expression;
118+
119+
/** The expresison whose value is the ending point of the iteration. */
120+
get toExpression(): Expression {
121+
return this._toExpression!;
122+
}
123+
set toExpression(toExpression: Expression | ExpressionProps) {
124+
if (this._toExpression) this._toExpression.parent = undefined;
125+
if (!('sassType' in toExpression)) {
126+
toExpression = fromProps(toExpression);
127+
}
128+
if (toExpression) toExpression.parent = this;
129+
this._toExpression = toExpression;
130+
}
131+
private _toExpression?: Expression;
132+
133+
constructor(defaults: ForRuleProps);
134+
/** @hidden */
135+
constructor(_: undefined, inner: sassInternal.ForRule);
136+
constructor(defaults?: ForRuleProps, inner?: sassInternal.ForRule) {
137+
super(defaults as unknown as postcss.AtRuleProps);
138+
this.nodes ??= [];
139+
140+
if (inner) {
141+
this.source = new LazySource(inner);
142+
this.variable = inner.variable;
143+
this.to = inner.isExclusive ? 'to' : 'through';
144+
this.fromExpression = convertExpression(inner.from);
145+
this.toExpression = convertExpression(inner.to);
146+
appendInternalChildren(this, inner.children);
147+
}
148+
149+
this.to ??= 'to';
150+
}
151+
152+
clone(overrides?: Partial<ForRuleProps>): this {
153+
return utils.cloneNode(this, overrides, [
154+
'raws',
155+
'variable',
156+
'to',
157+
'fromExpression',
158+
'toExpression',
159+
]);
160+
}
161+
162+
toJSON(): object;
163+
/** @hidden */
164+
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
165+
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
166+
return utils.toJSON(
167+
this,
168+
[
169+
'name',
170+
'variable',
171+
'to',
172+
'fromExpression',
173+
'toExpression',
174+
'params',
175+
'nodes',
176+
],
177+
inputs
178+
);
179+
}
180+
181+
/** @hidden */
182+
toString(
183+
stringifier: postcss.Stringifier | postcss.Syntax = sassParser.scss
184+
.stringify
185+
): string {
186+
return super.toString(stringifier);
187+
}
188+
189+
/** @hidden */
190+
get nonStatementChildren(): ReadonlyArray<Expression> {
191+
return [this.fromExpression, this.toExpression];
192+
}
193+
194+
/** @hidden */
195+
normalize(node: NewNode, sample?: postcss.Node): ChildNode[] {
196+
return normalize(this, node, sample);
197+
}
198+
}
199+
200+
interceptIsClean(ForRule);

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {GenericAtRule, GenericAtRuleProps} from './generic-at-rule';
1212
import {DebugRule, DebugRuleProps} from './debug-rule';
1313
import {EachRule, EachRuleProps} from './each-rule';
1414
import {ErrorRule, ErrorRuleProps} from './error-rule';
15+
import {ForRule, ForRuleProps} from './for-rule';
1516
import {Root} from './root';
1617
import {Rule, RuleProps} from './rule';
1718

@@ -41,14 +42,15 @@ export type StatementType =
4142
| 'atrule'
4243
| 'debug-rule'
4344
| 'each-rule'
45+
| 'for-rule'
4446
| 'error-rule';
4547

4648
/**
4749
* All Sass statements that are also at-rules.
4850
*
4951
* @category Statement
5052
*/
51-
export type AtRule = DebugRule | EachRule | ErrorRule | GenericAtRule;
53+
export type AtRule = DebugRule | EachRule | ErrorRule | ForRule | GenericAtRule;
5254

5355
/**
5456
* All Sass statements that are valid children of other statements.
@@ -71,6 +73,7 @@ export type ChildProps =
7173
| DebugRuleProps
7274
| EachRuleProps
7375
| ErrorRuleProps
76+
| ForRuleProps
7477
| GenericAtRuleProps
7578
| RuleProps;
7679

@@ -125,6 +128,7 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
125128
visitDebugRule: inner => new DebugRule(undefined, inner),
126129
visitErrorRule: inner => new ErrorRule(undefined, inner),
127130
visitEachRule: inner => new EachRule(undefined, inner),
131+
visitForRule: inner => new ForRule(undefined, inner),
128132
visitExtendRule: inner => {
129133
const paramsInterpolation = new Interpolation(undefined, inner.selector);
130134
if (inner.isOptional) paramsInterpolation.append('!optional');
@@ -241,6 +245,8 @@ export function normalize(
241245
result.push(new DebugRule(node));
242246
} else if ('eachExpression' in node) {
243247
result.push(new EachRule(node));
248+
} else if ('fromExpression' in node) {
249+
result.push(new ForRule(node));
244250
} else if ('errorExpression' in node) {
245251
result.push(new ErrorRule(node));
246252
} else {

‎pkg/sass-parser/lib/src/stringifier.ts

+10
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ export class Stringifier extends PostCssStringifier {
102102
);
103103
}
104104

105+
private ['for-rule'](node: EachRule): void {
106+
this.block(
107+
node,
108+
'@for' +
109+
(node.raws.afterName ?? ' ') +
110+
node.params +
111+
(node.raws.between ?? '')
112+
);
113+
}
114+
105115
private atrule(node: GenericAtRule, semicolon: boolean): void {
106116
// In the @at-root shorthand, stringify `@at-root {.foo {...}}` as
107117
// `@at-root .foo {...}`.

0 commit comments

Comments
 (0)
Please sign in to comment.