Skip to content

Commit

Permalink
Merge pull request #2337 from sass/even-more-statements
Browse files Browse the repository at this point in the history
Add support for a couple more Sass statements
  • Loading branch information
nex3 committed Sep 7, 2024
2 parents 717867b + d9e854a commit 6848763
Show file tree
Hide file tree
Showing 11 changed files with 855 additions and 5 deletions.
2 changes: 2 additions & 0 deletions lib/src/js/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ void _updateAstPrototypes() {
var file = SourceFile.fromString('');
getJSClass(file).defineMethod('getText',
(SourceFile self, int start, [int? end]) => self.getText(start, end));
getJSClass(file)
.defineGetter('codeUnits', (SourceFile self) => self.codeUnits);
var interpolation = Interpolation(const [], bogusSpan);
getJSClass(interpolation)
.defineGetter('asPlain', (Interpolation self) => self.asPlain);
Expand Down
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export {
} from './src/statement/generic-at-rule';
export {Root, RootProps, RootRaws} from './src/statement/root';
export {Rule, RuleProps, RuleRaws} from './src/statement/rule';
export {
SassComment,
SassCommentProps,
SassCommentRaws,
} from './src/statement/sass-comment';
export {
AnyStatement,
AtRule,
Expand Down
14 changes: 14 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface SourceFile {
/** Node-only extension that we use to avoid re-creating inputs. */
_postcssInput?: postcss.Input;

readonly codeUnits: number[];

getText(start: number, end?: number): string;
}

Expand Down Expand Up @@ -105,6 +107,14 @@ declare namespace SassInternal {
readonly text: Interpolation;
}

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

class SilentComment extends Statement {
readonly text: string;
}

class Stylesheet extends ParentStatement<Statement[]> {}

class StyleRule extends ParentStatement<Statement[]> {
Expand Down Expand Up @@ -148,6 +158,8 @@ 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 SilentComment = SassInternal.SilentComment;
export type Stylesheet = SassInternal.Stylesheet;
export type StyleRule = SassInternal.StyleRule;
export type Interpolation = SassInternal.Interpolation;
Expand All @@ -164,6 +176,8 @@ export interface StatementVisitorObject<T> {
visitExtendRule(node: ExtendRule): T;
visitForRule(node: ForRule): T;
visitLoudComment(node: LoudComment): T;
visitMediaRule(node: MediaRule): T;
visitSilentComment(node: SilentComment): T;
visitStyleRule(node: StyleRule): T;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a Sass-style comment toJSON 1`] = `
{
"inputs": [
{
"css": "// foo",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {
"before": "",
"beforeLines": [
"",
],
"left": " ",
},
"sassType": "sass-comment",
"source": <1:1-1:7 in 0>,
"text": "foo",
"type": "comment",
}
`;
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
21 changes: 18 additions & 3 deletions pkg/sass-parser/lib/src/statement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {LazySource} from '../lazy-source';
import {Node, NodeProps} from '../node';
import * as sassInternal from '../sass-internal';
import {CssComment, CssCommentProps} from './css-comment';
import {SassComment, SassCommentChildProps} from './sass-comment';
import {GenericAtRule, GenericAtRuleProps} from './generic-at-rule';
import {DebugRule, DebugRuleProps} from './debug-rule';
import {EachRule, EachRuleProps} from './each-rule';
Expand Down Expand Up @@ -45,7 +46,8 @@ export type StatementType =
| 'debug-rule'
| 'each-rule'
| 'for-rule'
| 'error-rule';
| 'error-rule'
| 'sass-comment';

/**
* All Sass statements that are also at-rules.
Expand All @@ -59,7 +61,7 @@ export type AtRule = DebugRule | EachRule | ErrorRule | ForRule | GenericAtRule;
*
* @category Statement
*/
export type Comment = CssComment;
export type Comment = CssComment | SassComment;

/**
* All Sass statements that are valid children of other statements.
Expand All @@ -85,7 +87,8 @@ export type ChildProps =
| ErrorRuleProps
| ForRuleProps
| GenericAtRuleProps
| RuleProps;
| RuleProps
| SassCommentChildProps;

/**
* The Sass eqivalent of PostCSS's `ContainerProps`.
Expand Down Expand Up @@ -149,6 +152,16 @@ 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;
},
visitSilentComment: inner => new SassComment(undefined, inner),
visitStyleRule: inner => new Rule(undefined, inner),
});

Expand Down Expand Up @@ -262,6 +275,8 @@ export function normalize(
result.push(new ErrorRule(node));
} else if ('text' in node || 'textInterpolation' in node) {
result.push(new CssComment(node as CssCommentProps));
} else if ('silentText' in node) {
result.push(new SassComment(node));
} else {
result.push(...postcssNormalizeAndConvertToSass(self, node, sample));
}
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}'));
});
});
Loading

0 comments on commit 6848763

Please sign in to comment.