Skip to content

Commit

Permalink
Add support for @media
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Sep 5, 2024
1 parent 717867b commit cf918bc
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 2 deletions.
6 changes: 6 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ declare namespace SassInternal {
readonly text: Interpolation;
}

class MediaRule extends ParentStatement<Statement[]> {
readonly query: Interpolation;
}

class Stylesheet extends ParentStatement<Statement[]> {}

class StyleRule extends ParentStatement<Statement[]> {
Expand Down Expand Up @@ -148,6 +152,7 @@ export type ErrorRule = SassInternal.ErrorRule;
export type ExtendRule = SassInternal.ExtendRule;
export type ForRule = SassInternal.ForRule;
export type LoudComment = SassInternal.LoudComment;
export type MediaRule = SassInternal.MediaRule;
export type Stylesheet = SassInternal.Stylesheet;
export type StyleRule = SassInternal.StyleRule;
export type Interpolation = SassInternal.Interpolation;
Expand All @@ -164,6 +169,7 @@ export interface StatementVisitorObject<T> {
visitExtendRule(node: ExtendRule): T;
visitForRule(node: ForRule): T;
visitLoudComment(node: LoudComment): T;
visitMediaRule(node: MediaRule): T;
visitStyleRule(node: StyleRule): T;
}

Expand Down
26 changes: 25 additions & 1 deletion pkg/sass-parser/lib/src/statement/generic-at-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,31 @@ export class GenericAtRule
private _nameInterpolation?: Interpolation;

get params(): string {
return this.paramsInterpolation?.toString() ?? '';
if (this.name !== 'media' || !this.paramsInterpolation) {
return this.paramsInterpolation?.toString() ?? '';
}

// @media has special parsing in Sass, and allows raw expressions within
// parens.
let result = '';
const rawText = this.paramsInterpolation.raws.text;
const rawExpressions = this.paramsInterpolation.raws.expressions;
for (let i = 0; i < this.paramsInterpolation.nodes.length; i++) {
const element = this.paramsInterpolation.nodes[i];
if (typeof element === 'string') {
const raw = rawText?.[i];
result += raw?.value === element ? raw.raw : element;
} else {
if (result.match(/(\([ \t\n\f\r]*|(:|[<>]?=)[ \t\n\f\r]*)$/)) {
result += element;
} else {
const raw = rawExpressions?.[i];
result +=
'#{' + (raw?.before ?? '') + element + (raw?.after ?? '') + '}';
}
}
}
return result;
}
set params(value: string | number | undefined) {
this.paramsInterpolation = value === '' ? undefined : value?.toString();
Expand Down
9 changes: 9 additions & 0 deletions pkg/sass-parser/lib/src/statement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
});
},
visitLoudComment: inner => new CssComment(undefined, inner),
visitMediaRule: inner => {
const rule = new GenericAtRule({
name: 'media',
paramsInterpolation: new Interpolation(undefined, inner.query),
source: new LazySource(inner),
});
appendInternalChildren(rule, inner.children);
return rule;
},
visitStyleRule: inner => new Rule(undefined, inner),
});

Expand Down
61 changes: 61 additions & 0 deletions pkg/sass-parser/lib/src/statement/media-rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import {GenericAtRule, StringExpression, scss} from '../..';

describe('a @media rule', () => {
let node: GenericAtRule;

describe('with no interpolation', () => {
beforeEach(
() =>
void (node = scss.parse('@media screen {}').nodes[0] as GenericAtRule)
);

it('has a name', () => expect(node.name).toBe('media'));

it('has a paramsInterpolation', () =>
expect(node).toHaveInterpolation('paramsInterpolation', 'screen'));

it('has matching params', () => expect(node.params).toBe('screen'));
});

// TODO: test a variable used directly without interpolation

describe('with interpolation', () => {
beforeEach(
() =>
void (node = scss.parse('@media (hover: #{hover}) {}')
.nodes[0] as GenericAtRule)
);

it('has a name', () => expect(node.name).toBe('media'));

it('has a paramsInterpolation', () => {
const params = node.paramsInterpolation!;
expect(params.nodes[0]).toBe('(');
expect(params).toHaveStringExpression(1, 'hover');
expect(params.nodes[2]).toBe(': ');
expect(params.nodes[3]).toBeInstanceOf(StringExpression);
expect((params.nodes[3] as StringExpression).text).toHaveStringExpression(
0,
'hover'
);
expect(params.nodes[4]).toBe(')');
});

it('has matching params', () =>
expect(node.params).toBe('(hover: #{hover})'));
});

describe('stringifies', () => {
// TODO: Use raws technology to include the actual original text between
// interpolations.
it('to SCSS', () =>
expect(
(node = scss.parse('@media #{screen} and (hover: #{hover}) {@foo}')
.nodes[0] as GenericAtRule).toString()
).toBe('@media #{screen} and (hover: #{hover}) {\n @foo\n}'));
});
});
2 changes: 1 addition & 1 deletion pkg/sass-parser/lib/src/stringifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class Stringifier extends PostCssStringifier {
const start =
`@${node.nameInterpolation}` +
(node.raws.afterName ?? (node.paramsInterpolation ? ' ' : '')) +
(node.paramsInterpolation ?? '');
node.params;
if (node.nodes) {
this.block(node, start);
} else {
Expand Down

0 comments on commit cf918bc

Please sign in to comment.