Skip to content

Commit cf918bc

Browse files
committedSep 5, 2024
Add support for @media
1 parent 717867b commit cf918bc

File tree

5 files changed

+102
-2
lines changed

5 files changed

+102
-2
lines changed
 

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

+6
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ declare namespace SassInternal {
105105
readonly text: Interpolation;
106106
}
107107

108+
class MediaRule extends ParentStatement<Statement[]> {
109+
readonly query: Interpolation;
110+
}
111+
108112
class Stylesheet extends ParentStatement<Statement[]> {}
109113

110114
class StyleRule extends ParentStatement<Statement[]> {
@@ -148,6 +152,7 @@ export type ErrorRule = SassInternal.ErrorRule;
148152
export type ExtendRule = SassInternal.ExtendRule;
149153
export type ForRule = SassInternal.ForRule;
150154
export type LoudComment = SassInternal.LoudComment;
155+
export type MediaRule = SassInternal.MediaRule;
151156
export type Stylesheet = SassInternal.Stylesheet;
152157
export type StyleRule = SassInternal.StyleRule;
153158
export type Interpolation = SassInternal.Interpolation;
@@ -164,6 +169,7 @@ export interface StatementVisitorObject<T> {
164169
visitExtendRule(node: ExtendRule): T;
165170
visitForRule(node: ForRule): T;
166171
visitLoudComment(node: LoudComment): T;
172+
visitMediaRule(node: MediaRule): T;
167173
visitStyleRule(node: StyleRule): T;
168174
}
169175

‎pkg/sass-parser/lib/src/statement/generic-at-rule.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,31 @@ export class GenericAtRule
9898
private _nameInterpolation?: Interpolation;
9999

100100
get params(): string {
101-
return this.paramsInterpolation?.toString() ?? '';
101+
if (this.name !== 'media' || !this.paramsInterpolation) {
102+
return this.paramsInterpolation?.toString() ?? '';
103+
}
104+
105+
// @media has special parsing in Sass, and allows raw expressions within
106+
// parens.
107+
let result = '';
108+
const rawText = this.paramsInterpolation.raws.text;
109+
const rawExpressions = this.paramsInterpolation.raws.expressions;
110+
for (let i = 0; i < this.paramsInterpolation.nodes.length; i++) {
111+
const element = this.paramsInterpolation.nodes[i];
112+
if (typeof element === 'string') {
113+
const raw = rawText?.[i];
114+
result += raw?.value === element ? raw.raw : element;
115+
} else {
116+
if (result.match(/(\([ \t\n\f\r]*|(:|[<>]?=)[ \t\n\f\r]*)$/)) {
117+
result += element;
118+
} else {
119+
const raw = rawExpressions?.[i];
120+
result +=
121+
'#{' + (raw?.before ?? '') + element + (raw?.after ?? '') + '}';
122+
}
123+
}
124+
}
125+
return result;
102126
}
103127
set params(value: string | number | undefined) {
104128
this.paramsInterpolation = value === '' ? undefined : value?.toString();

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

+9
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,15 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
149149
});
150150
},
151151
visitLoudComment: inner => new CssComment(undefined, inner),
152+
visitMediaRule: inner => {
153+
const rule = new GenericAtRule({
154+
name: 'media',
155+
paramsInterpolation: new Interpolation(undefined, inner.query),
156+
source: new LazySource(inner),
157+
});
158+
appendInternalChildren(rule, inner.children);
159+
return rule;
160+
},
152161
visitStyleRule: inner => new Rule(undefined, inner),
153162
});
154163

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, StringExpression, scss} from '../..';
6+
7+
describe('a @media rule', () => {
8+
let node: GenericAtRule;
9+
10+
describe('with no interpolation', () => {
11+
beforeEach(
12+
() =>
13+
void (node = scss.parse('@media screen {}').nodes[0] as GenericAtRule)
14+
);
15+
16+
it('has a name', () => expect(node.name).toBe('media'));
17+
18+
it('has a paramsInterpolation', () =>
19+
expect(node).toHaveInterpolation('paramsInterpolation', 'screen'));
20+
21+
it('has matching params', () => expect(node.params).toBe('screen'));
22+
});
23+
24+
// TODO: test a variable used directly without interpolation
25+
26+
describe('with interpolation', () => {
27+
beforeEach(
28+
() =>
29+
void (node = scss.parse('@media (hover: #{hover}) {}')
30+
.nodes[0] as GenericAtRule)
31+
);
32+
33+
it('has a name', () => expect(node.name).toBe('media'));
34+
35+
it('has a paramsInterpolation', () => {
36+
const params = node.paramsInterpolation!;
37+
expect(params.nodes[0]).toBe('(');
38+
expect(params).toHaveStringExpression(1, 'hover');
39+
expect(params.nodes[2]).toBe(': ');
40+
expect(params.nodes[3]).toBeInstanceOf(StringExpression);
41+
expect((params.nodes[3] as StringExpression).text).toHaveStringExpression(
42+
0,
43+
'hover'
44+
);
45+
expect(params.nodes[4]).toBe(')');
46+
});
47+
48+
it('has matching params', () =>
49+
expect(node.params).toBe('(hover: #{hover})'));
50+
});
51+
52+
describe('stringifies', () => {
53+
// TODO: Use raws technology to include the actual original text between
54+
// interpolations.
55+
it('to SCSS', () =>
56+
expect(
57+
(node = scss.parse('@media #{screen} and (hover: #{hover}) {@foo}')
58+
.nodes[0] as GenericAtRule).toString()
59+
).toBe('@media #{screen} and (hover: #{hover}) {\n @foo\n}'));
60+
});
61+
});

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class Stringifier extends PostCssStringifier {
134134
const start =
135135
`@${node.nameInterpolation}` +
136136
(node.raws.afterName ?? (node.paramsInterpolation ? ' ' : '')) +
137-
(node.paramsInterpolation ?? '');
137+
node.params;
138138
if (node.nodes) {
139139
this.block(node, start);
140140
} else {

0 commit comments

Comments
 (0)
Please sign in to comment.