Skip to content

Commit b60577e

Browse files
committed
Add support for @extend rules
1 parent 3544cef commit b60577e

File tree

5 files changed

+83
-7
lines changed

5 files changed

+83
-7
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,8 @@ export class Interpolation extends Node {
112112
*/
113113
get asPlain(): string | null {
114114
if (this.nodes.length === 0) return '';
115-
if (this.nodes.length !== 1) return null;
116-
if (typeof this.nodes[0] !== 'string') return null;
117-
return this.nodes[0] as string;
115+
if (this.nodes.some(node => typeof node !== 'string')) return null;
116+
return this.nodes.join('');
118117
}
119118

120119
/**

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ declare namespace SassInternal {
8989
readonly expression: Expression;
9090
}
9191

92+
class ExtendRule extends Statement {
93+
readonly selector: Interpolation;
94+
readonly isOptional: boolean;
95+
}
96+
9297
class Stylesheet extends ParentStatement<Statement[]> {}
9398

9499
class StyleRule extends ParentStatement<Statement[]> {
@@ -129,6 +134,7 @@ export type AtRule = SassInternal.AtRule;
129134
export type DebugRule = SassInternal.DebugRule;
130135
export type EachRule = SassInternal.EachRule;
131136
export type ErrorRule = SassInternal.ErrorRule;
137+
export type ExtendRule = SassInternal.ExtendRule;
132138
export type Stylesheet = SassInternal.Stylesheet;
133139
export type StyleRule = SassInternal.StyleRule;
134140
export type Interpolation = SassInternal.Interpolation;
@@ -142,6 +148,7 @@ export interface StatementVisitorObject<T> {
142148
visitDebugRule(node: DebugRule): T;
143149
visitEachRule(node: EachRule): T;
144150
visitErrorRule(node: ErrorRule): T;
151+
visitExtendRule(node: ExtendRule): T;
145152
visitStyleRule(node: StyleRule): T;
146153
}
147154

pkg/sass-parser/lib/src/statement/at-root-rule.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('an @at-root rule', () => {
1212
() => void (node = scss.parse('@at-root {}').nodes[0] as GenericAtRule)
1313
);
1414

15-
it('has a name', () => expect(node.name.toString()).toBe('at-root'));
15+
it('has a name', () => expect(node.name).toBe('at-root'));
1616

1717
it('has no paramsInterpolation', () =>
1818
expect(node.paramsInterpolation).toBeUndefined());
@@ -27,7 +27,7 @@ describe('an @at-root rule', () => {
2727
.nodes[0] as GenericAtRule)
2828
);
2929

30-
it('has a name', () => expect(node.name.toString()).toBe('at-root'));
30+
it('has a name', () => expect(node.name).toBe('at-root'));
3131

3232
it('has a paramsInterpolation', () =>
3333
expect(node).toHaveInterpolation('paramsInterpolation', '(with: rule)'));
@@ -44,7 +44,7 @@ describe('an @at-root rule', () => {
4444
.nodes[0] as GenericAtRule)
4545
);
4646

47-
it('has a name', () => expect(node.name.toString()).toBe('at-root'));
47+
it('has a name', () => expect(node.name).toBe('at-root'));
4848

4949
it('has a paramsInterpolation', () => {
5050
const params = node.paramsInterpolation!;
@@ -63,7 +63,7 @@ describe('an @at-root rule', () => {
6363
void (node = scss.parse('@at-root .foo {}').nodes[0] as GenericAtRule)
6464
);
6565

66-
it('has a name', () => expect(node.name.toString()).toBe('at-root'));
66+
it('has a name', () => expect(node.name).toBe('at-root'));
6767

6868
it('has no paramsInterpolation', () =>
6969
expect(node.paramsInterpolation).toBeUndefined());
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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, Rule, scss} from '../..';
6+
7+
describe('an @extend rule', () => {
8+
let node: GenericAtRule;
9+
10+
describe('with no interpolation', () => {
11+
beforeEach(
12+
() =>
13+
void (node = (scss.parse('.foo {@extend .bar}').nodes[0] as Rule)
14+
.nodes[0] as GenericAtRule)
15+
);
16+
17+
it('has a name', () => expect(node.name).toBe('extend'));
18+
19+
it('has a paramsInterpolation', () =>
20+
expect(node).toHaveInterpolation('paramsInterpolation', '.bar'));
21+
22+
it('has matching params', () => expect(node.params).toBe('.bar'));
23+
});
24+
25+
describe('with interpolation', () => {
26+
beforeEach(
27+
() =>
28+
void (node = (scss.parse('.foo {@extend .#{bar}}').nodes[0] as Rule)
29+
.nodes[0] as GenericAtRule)
30+
);
31+
32+
it('has a name', () => expect(node.name).toBe('extend'));
33+
34+
it('has a paramsInterpolation', () => {
35+
const params = node.paramsInterpolation!;
36+
expect(params.nodes[0]).toBe('.');
37+
expect(params).toHaveStringExpression(1, 'bar');
38+
});
39+
40+
it('has matching params', () => expect(node.params).toBe('.#{bar}'));
41+
});
42+
43+
describe('with !optional', () => {
44+
beforeEach(
45+
() =>
46+
void (node = (
47+
scss.parse('.foo {@extend .bar !optional}').nodes[0] as Rule
48+
).nodes[0] as GenericAtRule)
49+
);
50+
51+
it('has a name', () => expect(node.name).toBe('extend'));
52+
53+
it('has a paramsInterpolation', () =>
54+
expect(node).toHaveInterpolation(
55+
'paramsInterpolation',
56+
'.bar !optional'
57+
));
58+
59+
it('has matching params', () => expect(node.params).toBe('.bar !optional'));
60+
});
61+
});

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,15 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
125125
visitDebugRule: inner => new DebugRule(undefined, inner),
126126
visitErrorRule: inner => new ErrorRule(undefined, inner),
127127
visitEachRule: inner => new EachRule(undefined, inner),
128+
visitExtendRule: inner => {
129+
const paramsInterpolation = new Interpolation(undefined, inner.selector);
130+
if (inner.isOptional) paramsInterpolation.append('!optional');
131+
return new GenericAtRule({
132+
name: 'extend',
133+
paramsInterpolation,
134+
source: new LazySource(inner),
135+
});
136+
},
128137
visitStyleRule: inner => new Rule(undefined, inner),
129138
});
130139

0 commit comments

Comments
 (0)