Skip to content

Commit

Permalink
Add sass-parser support for variable declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Oct 23, 2024
1 parent 473ddf9 commit fc892fc
Show file tree
Hide file tree
Showing 12 changed files with 1,009 additions and 17 deletions.
10 changes: 9 additions & 1 deletion lib/src/ast/sass/statement/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ final class Stylesheet extends ParentStatement<List<Statement>> {
@internal
final List<ParseTimeWarning> parseTimeWarnings;

/// The set of (normalized) global variable names defined by this stylesheet
/// to the spans where they're defined.
@internal
final Map<String, FileSpan> globalVariables;

Stylesheet(Iterable<Statement> children, FileSpan span)
: this.internal(children, span, []);

Expand All @@ -62,8 +67,11 @@ final class Stylesheet extends ParentStatement<List<Statement>> {
@internal
Stylesheet.internal(Iterable<Statement> children, this.span,
List<ParseTimeWarning> parseTimeWarnings,
{this.plainCss = false})
{this.plainCss = false, Map<String, FileSpan>? globalVariables})
: parseTimeWarnings = UnmodifiableListView(parseTimeWarnings),
globalVariables = globalVariables == null
? const {}
: Map.unmodifiable(globalVariables),
super(List.unmodifiable(children)) {
loop:
for (var child in this.children) {
Expand Down
15 changes: 4 additions & 11 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ abstract class StylesheetParser extends Parser {
var _inExpression = false;

/// A map from all variable names that are assigned with `!global` in the
/// current stylesheet to the nodes where they're defined.
/// current stylesheet to the spans where they're defined.
///
/// These are collected at parse time because they affect the variables
/// exposed by the module generated for this stylesheet, *even if they aren't
/// evaluated*. This allows us to ensure that the stylesheet always exposes
/// the same set of variable names no matter how it's evaluated.
final _globalVariables = <String, VariableDeclaration>{};
final _globalVariables = <String, FileSpan>{};

/// Warnings discovered while parsing that should be emitted during
/// evaluation once a proper logger is available.
Expand Down Expand Up @@ -100,15 +100,8 @@ abstract class StylesheetParser extends Parser {
});
scanner.expectDone();

/// Ensure that all global variable assignments produce a variable in this
/// stylesheet, even if they aren't evaluated. See sass/language#50.
statements.addAll(_globalVariables.values.map((declaration) =>
VariableDeclaration(declaration.name,
NullExpression(declaration.expression.span), declaration.span,
guarded: true)));

return Stylesheet.internal(statements, scanner.spanFrom(start), warnings,
plainCss: plainCss);
plainCss: plainCss, globalVariables: _globalVariables);
});
}

Expand Down Expand Up @@ -288,7 +281,7 @@ abstract class StylesheetParser extends Parser {
guarded: guarded,
global: global,
comment: precedingComment);
if (global) _globalVariables.putIfAbsent(name, () => declaration);
if (global) _globalVariables.putIfAbsent(name, () => declaration.span);
return declaration;
}

Expand Down
8 changes: 8 additions & 0 deletions lib/src/visitor/async_evaluate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,14 @@ final class _EvaluateVisitor
for (var child in node.children) {
await child.accept(this);
}

// Make sure all global variables declared in a module always appear in the
// module's definition, even if their assignments aren't reached.
for (var (name, span) in node.globalVariables.pairs) {
visitVariableDeclaration(
VariableDeclaration(name, NullExpression(span), span, guarded: true));
}

return null;
}

Expand Down
10 changes: 9 additions & 1 deletion lib/src/visitor/evaluate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: e7260fedcd4f374ba517a93d038c3c53586c9622
// Checksum: 396c8f169d95c601598b8c3be1f4b948ca22effa
//
// ignore_for_file: unused_import

Expand Down Expand Up @@ -1005,6 +1005,14 @@ final class _EvaluateVisitor
for (var child in node.children) {
child.accept(this);
}

// Make sure all global variables declared in a module always appear in the
// module's definition, even if their assignments aren't reached.
for (var (name, span) in node.globalVariables.pairs) {
visitVariableDeclaration(
VariableDeclaration(name, NullExpression(span), span, guarded: true));
}

return null;
}

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 @@ -96,6 +96,11 @@ export {
StatementType,
StatementWithChildren,
} from './src/statement';
export {
VariableDeclaration,
VariableDeclarationProps,
VariableDeclarationRaws,
} from './src/statement/variable-declaration';

/** Options that can be passed to the Sass parsers to control their behavior. */
export type SassParserOptions = Pick<postcss.ProcessOptions, 'from' | 'map'>;
Expand Down
10 changes: 10 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ declare namespace SassInternal {
readonly configuration: ConfiguredVariable[];
}

class VariableDeclaration extends Statement {
readonly namespace: string | null;
readonly name: string;
readonly expression: Expression;
readonly isGuarded: boolean;
readonly isGlobal: boolean;
}

class ConfiguredVariable extends SassNode {
readonly name: string;
readonly expression: Expression;
Expand Down Expand Up @@ -238,6 +246,7 @@ export type Stylesheet = SassInternal.Stylesheet;
export type StyleRule = SassInternal.StyleRule;
export type SupportsRule = SassInternal.SupportsRule;
export type UseRule = SassInternal.UseRule;
export type VariableDeclaration = SassInternal.VariableDeclaration;
export type ConfiguredVariable = SassInternal.ConfiguredVariable;
export type Interpolation = SassInternal.Interpolation;
export type Expression = SassInternal.Expression;
Expand All @@ -260,6 +269,7 @@ export interface StatementVisitorObject<T> {
visitStyleRule(node: StyleRule): T;
visitSupportsRule(node: SupportsRule): T;
visitUseRule(node: UseRule): T;
visitVariableDeclaration(node: VariableDeclaration): T;
}

export interface ExpressionVisitorObject<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a variable declaration toJSON 1`] = `
{
"expression": <"bar">,
"global": false,
"guarded": false,
"inputs": [
{
"css": "baz.$foo: "bar"",
"hasBOM": false,
"id": "<input css _____>",
},
],
"namespace": "baz",
"raws": {},
"sassType": "variable-declaration",
"source": <1:1-1:16 in 0>,
"type": "decl",
"variableName": "foo",
}
`;
77 changes: 77 additions & 0 deletions pkg/sass-parser/lib/src/statement/declaration-internal.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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 * as postcss from 'postcss';

import {Rule} from './rule';
import {Root} from './root';
import {AtRule, ChildNode, Comment, Declaration, NewNode} from '.';

/**
* A fake intermediate class to convince TypeScript to use Sass types for
* various upstream methods.
*
* @hidden
*/
export class _Declaration<Props> extends postcss.Declaration {
// Override the PostCSS container types to constrain them to Sass types only.
// Unfortunately, there's no way to abstract this out, because anything
// mixin-like returns an intersection type which doesn't actually override
// parent methods. See microsoft/TypeScript#59394.

after(newNode: NewNode): this;
append(...nodes: NewNode[]): this;
assign(overrides: Partial<Props>): this;
before(newNode: NewNode): this;
cloneAfter(overrides?: Partial<Props>): this;
cloneBefore(overrides?: Partial<Props>): this;
each(
callback: (node: ChildNode, index: number) => false | void
): false | undefined;
every(
condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean
): boolean;
insertAfter(oldNode: postcss.ChildNode | number, newNode: NewNode): this;
insertBefore(oldNode: postcss.ChildNode | number, newNode: NewNode): this;
next(): ChildNode | undefined;
prepend(...nodes: NewNode[]): this;
prev(): ChildNode | undefined;
replaceWith(...nodes: NewNode[]): this;
root(): Root;
some(
condition: (node: ChildNode, index: number, nodes: ChildNode[]) => boolean
): boolean;
walk(
callback: (node: ChildNode, index: number) => false | void
): false | undefined;
walkAtRules(
nameFilter: RegExp | string,
callback: (atRule: AtRule, index: number) => false | void
): false | undefined;
walkAtRules(
callback: (atRule: AtRule, index: number) => false | void
): false | undefined;
walkComments(
callback: (comment: Comment, indexed: number) => false | void
): false | undefined;
walkComments(
callback: (comment: Comment, indexed: number) => false | void
): false | undefined;
walkDecls(
propFilter: RegExp | string,
callback: (decl: Declaration, index: number) => false | void
): false | undefined;
walkDecls(
callback: (decl: Declaration, index: number) => false | void
): false | undefined;
walkRules(
selectorFilter: RegExp | string,
callback: (rule: Rule, index: number) => false | void
): false | undefined;
walkRules(
callback: (rule: Rule, index: number) => false | void
): false | undefined;
get first(): ChildNode | undefined;
get last(): ChildNode | undefined;
}
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/src/statement/declaration-internal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 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.

exports._Declaration = require('postcss').Declaration;
17 changes: 13 additions & 4 deletions pkg/sass-parser/lib/src/statement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {ForRule, ForRuleProps} from './for-rule';
import {Root} from './root';
import {Rule, RuleProps} from './rule';
import {UseRule, UseRuleProps} from './use-rule';
import {
VariableDeclaration,
VariableDeclarationProps,
} from './variable-declaration';

// TODO: Replace this with the corresponding Sass types once they're
// implemented.
Expand All @@ -28,7 +32,7 @@ export {Declaration} from 'postcss';
*
* @category Statement
*/
export type AnyStatement = Comment | Root | Rule | GenericAtRule;
export type AnyStatement = Comment | Root | Rule | AtRule | VariableDeclaration;

/**
* Sass statement types.
Expand All @@ -49,7 +53,8 @@ export type StatementType =
| 'for-rule'
| 'error-rule'
| 'use-rule'
| 'sass-comment';
| 'sass-comment'
| 'variable-declaration';

/**
* All Sass statements that are also at-rules.
Expand Down Expand Up @@ -78,7 +83,7 @@ export type Comment = CssComment | SassComment;
*
* @category Statement
*/
export type ChildNode = Rule | AtRule | Comment;
export type ChildNode = Rule | AtRule | Comment | VariableDeclaration;

/**
* The properties that can be used to construct {@link ChildNode}s.
Expand All @@ -97,7 +102,8 @@ export type ChildProps =
| GenericAtRuleProps
| RuleProps
| SassCommentChildProps
| UseRuleProps;
| UseRuleProps
| VariableDeclarationProps;

/**
* The Sass eqivalent of PostCSS's `ContainerProps`.
Expand Down Expand Up @@ -185,6 +191,7 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
return rule;
},
visitUseRule: inner => new UseRule(undefined, inner),
visitVariableDeclaration: inner => new VariableDeclaration(undefined, inner),
});

/** Appends parsed versions of `internal`'s children to `container`. */
Expand Down Expand Up @@ -301,6 +308,8 @@ export function normalize(
result.push(new SassComment(node));
} else if ('useUrl' in node) {
result.push(new UseRule(node));
} else if ('variableName' in node) {
result.push(new VariableDeclaration(node));
} else {
result.push(...postcssNormalizeAndConvertToSass(self, node, sample));
}
Expand Down
Loading

0 comments on commit fc892fc

Please sign in to comment.