Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 1.94.0

* **Potentially breaking compatibility fix:** `@function` rules whose names
begin with `--` are now parsed as unknown at-rules to support the plain CSS
`@function` rule. Within this rule, the `result` property is parsed as raw
CSS just like custom properties.

* **Potentially breaking compatibility fix:** `@mixin` rules whose names begin
with `--` are now errors. These are not yet parsed as unknown at-rules because
no browser currently supports CSS mixins.

## 1.93.2

* No user-visible changes.
Expand Down
14 changes: 8 additions & 6 deletions lib/src/ast/css/declaration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ abstract interface class CssDeclaration implements CssNode {
/// Returns whether this is a CSS Custom Property declaration.
bool get isCustomProperty;

/// Whether this is was originally parsed as a custom property declaration, as
/// opposed to using something like `#{--foo}: ...` to cause it to be parsed
/// as a normal Sass declaration.
/// Whether this property's value was originally parsed as SassScript, as
/// opposed to a custom property which is parsed as an interpolated sequence
/// of tokens.
///
/// If this is `true`, [isCustomProperty] will also be `true` and [value] will
/// contain a [SassString].
bool get parsedAsCustomProperty;
/// If this is `false`, [value] will contain an unquoted [SassString].
/// [isCustomProperty] will *usually* be true, but there are other properties
/// that may not be parsed as SassScript, like `return` in a plain CSS
/// `@function`.
bool get parsedAsSassScript;
}
15 changes: 5 additions & 10 deletions lib/src/ast/css/modifiable/declaration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final class ModifiableCssDeclaration extends ModifiableCssNode
implements CssDeclaration {
final CssValue<String> name;
final CssValue<Value> value;
final bool parsedAsCustomProperty;
final bool parsedAsSassScript;
final Trace? trace;
final FileSpan valueSpanForMap;
final FileSpan span;
Expand All @@ -28,19 +28,14 @@ final class ModifiableCssDeclaration extends ModifiableCssNode
this.name,
this.value,
this.span, {
required this.parsedAsCustomProperty,
required this.parsedAsSassScript,
this.trace,
FileSpan? valueSpanForMap,
}) : valueSpanForMap = valueSpanForMap ?? value.span {
if (parsedAsCustomProperty) {
if (!isCustomProperty) {
if (!parsedAsSassScript) {
if (value.value is! SassString) {
throw ArgumentError(
'parsedAsCustomProperty must be false if name doesn\'t begin with '
'"--".',
);
} else if (value.value is! SassString) {
throw ArgumentError(
'If parsedAsCustomProperty is true, value must contain a SassString '
'If parsedAsSassScript is false, value must contain a SassString '
'(was `$value` of type ${value.value.runtimeType}).',
);
}
Expand Down
26 changes: 19 additions & 7 deletions lib/src/ast/sass/statement/declaration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,31 @@ final class Declaration extends ParentStatement {

final FileSpan span;

/// Returns whether this is a CSS Custom Property declaration.
/// Returns whether this declaration's value was parsed as SassScript.
///
/// Note that this can return `false` for declarations that will ultimately be
/// This is `false` for custom properties as well as the `result` property of
/// the plain CSS `@function` rule.
///
/// Note that this can return `true` for declarations that will ultimately be
/// serialized as custom properties if they aren't *parsed as* custom
/// properties, such as `#{--foo}: ...`.
///
/// If this is `true`, then `value` will be a [StringExpression].
/// If this is `false`, then `value` will be an unquoted [StringExpression].
///
/// @nodoc
@internal
bool get isCustomProperty => name.initialPlain.startsWith('--');
final bool parsedAsSassScript;

/// Creates a declaration with no children.
Declaration(this.name, this.value, this.span) : super(null);
Declaration(this.name, this.value, this.span)
: parsedAsSassScript = true,
super(null);

/// Creates a declaration with no children whose value is not parsed as
/// SassScript.
Declaration.notSassScript(this.name, StringExpression this.value, this.span)
: parsedAsSassScript = false,
super(null);

/// Creates a declaration with children.
///
Expand All @@ -51,7 +62,8 @@ final class Declaration extends ParentStatement {
Iterable<Statement> children,
this.span, {
this.value,
}) : super(List.unmodifiable(children));
}) : parsedAsSassScript = true,
super(List.unmodifiable(children));

T accept<T>(StatementVisitor<T> visitor) => visitor.visitDeclaration(this);

Expand All @@ -61,7 +73,7 @@ final class Declaration extends ParentStatement {
buffer.writeCharCode($colon);

if (value != null) {
if (!isCustomProperty) buffer.writeCharCode($space);
if (parsedAsSassScript) buffer.writeCharCode($space);
buffer.write("$value");
}

Expand Down
2 changes: 1 addition & 1 deletion lib/src/deprecation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ enum Deprecation {
// DO NOT EDIT. This section was generated from the language repo.
// See tool/grind/generate_deprecations.dart for details.
//
// Checksum: 0d3df25297a4e76b865aee1e908baf355e26d665
// Checksum: fc5363081eb9d8e9e7220b9d5c7fd928d67009fe

/// Deprecation for passing a string directly to meta.call().
callString('call-string',
Expand Down
16 changes: 15 additions & 1 deletion lib/src/parse/css.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ class CssParser extends ScssParser {
"error" ||
"extend" ||
"for" ||
"function" ||
"if" ||
"include" ||
"mixin" ||
Expand All @@ -76,6 +75,7 @@ class CssParser extends ScssParser {
"while" =>
_forbiddenAtRule(start),
"import" => _cssImportRule(start),
"function" => _cssFunctionRule(start, name),
"media" => mediaRule(start),
"-moz-document" => mozDocumentRule(start, name),
"supports" => supportsRule(start),
Expand Down Expand Up @@ -132,6 +132,20 @@ class CssParser extends ScssParser {
], scanner.spanFrom(start));
}

/// Consumes a plain CSS function declaration.
///
/// [start] should point before the `@`.
Statement _cssFunctionRule(LineScannerState start, Interpolation atRuleName) {
whitespace(consumeNewlines: true);
if (!scanner.matches('--')) {
almostAnyValue();
error(
"This at-rule isn't allowed in plain CSS.", scanner.spanFrom(start));
} else {
return unknownAtRule(start, atRuleName);
}
}

ParenthesizedExpression parentheses() {
// Expressions are only allowed within calculations, but we verify this at
// evaluation time.
Expand Down
101 changes: 49 additions & 52 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ abstract class StylesheetParser extends Parser {
/// Whether the parser is currently parsing an unknown rule.
var _inUnknownAtRule = false;

/// Whether the parser is currently parsing a plain-CSS `@function` rule.
var _inPlainCssFunction = false;

/// Whether the parser is currently parsing a style rule.
var _inStyleRule = false;

Expand Down Expand Up @@ -411,14 +414,19 @@ abstract class StylesheetParser extends Parser {

// Parse custom properties as declarations no matter what.
var name = nameBuffer.interpolation(scanner.spanFrom(start, beforeColon));
if (name.initialPlain.startsWith('--')) {
var customProperty = name.initialPlain.startsWith('--');
if (customProperty ||
(_inPlainCssFunction &&
(name.asPlain.andThen((name) => equalsIgnoreCase(name, 'result')) ??
false))) {
var value = StringExpression(
atEndOfStatement()
? Interpolation(const [], const [], scanner.emptySpan)
: _interpolatedDeclarationValue(silentComments: false),
);
expectStatementSeparator("custom property");
return Declaration(name, value, scanner.spanFrom(start));
expectStatementSeparator(
customProperty ? "custom property" : "@function result");
return Declaration.notSassScript(name, value, scanner.spanFrom(start));
}

if (scanner.scanChar($colon)) {
Expand Down Expand Up @@ -542,15 +550,12 @@ abstract class StylesheetParser extends Parser {
/// Consumes either a property declaration or a namespaced variable
/// declaration.
///
/// This is only used in contexts where declarations are allowed but style
/// rules are not, such as nested declarations. Otherwise,
/// This is only used when nested beneath other declarations. Otherwise,
/// [_declarationOrStyleRule] is used instead.
///
/// If [parseCustomProperties] is `true`, properties that begin with `--` will
/// be parsed using custom property parsing rules.
Statement _propertyOrVariableDeclaration({
bool parseCustomProperties = true,
}) {
Statement _propertyOrVariableDeclaration() {
var start = scanner.state;

Interpolation name;
Expand All @@ -574,12 +579,9 @@ abstract class StylesheetParser extends Parser {
whitespace(consumeNewlines: false);
scanner.expectChar($colon);

if (parseCustomProperties && name.initialPlain.startsWith('--')) {
var value = StringExpression(
_interpolatedDeclarationValue(silentComments: false),
);
expectStatementSeparator("custom property");
return Declaration(name, value, scanner.spanFrom(start));
if (name.initialPlain.startsWith('--')) {
error('Declarations whose names begin with "--" may not be nested.',
name.span);
}

whitespace(consumeNewlines: false);
Expand Down Expand Up @@ -619,7 +621,7 @@ abstract class StylesheetParser extends Parser {
/// Consumes a statement that's allowed within a declaration.
Statement _declarationChild() => scanner.peekChar() == $at
? _declarationAtRule()
: _propertyOrVariableDeclaration(parseCustomProperties: false);
: _propertyOrVariableDeclaration();

// ## At Rules

Expand Down Expand Up @@ -669,7 +671,7 @@ abstract class StylesheetParser extends Parser {
if (!root) _disallowedAtRule(start);
return _forwardRule(start);
case "function":
return _functionRule(start);
return _functionRule(start, name);
case "if":
return _ifRule(start, child);
case "import":
Expand Down Expand Up @@ -914,24 +916,16 @@ abstract class StylesheetParser extends Parser {
/// Consumes a function declaration.
///
/// [start] should point before the `@`.
FunctionRule _functionRule(LineScannerState start) {
Statement _functionRule(LineScannerState start, Interpolation atRuleName) {
whitespace(consumeNewlines: true);
var precedingComment = lastSilentComment;
lastSilentComment = null;
var beforeName = scanner.state;
var name = identifier();

if (name.startsWith('--')) {
warnings.add((
deprecation: Deprecation.cssFunctionMixin,
message:
'Sass @function names beginning with -- are deprecated for forward-'
'compatibility with plain CSS functions.\n'
'\n'
'For details, see https://sass-lang.com/d/css-function-mixin',
span: scanner.spanFrom(beforeName),
));
} else if (equalsIgnoreCase(name, 'type')) {
if (scanner.matches('--')) return unknownAtRule(start, atRuleName);

var name = identifier();
if (equalsIgnoreCase(name, 'type')) {
error('This name is reserved for the plain-CSS function.',
scanner.spanFrom(beforeName));
}
Expand Down Expand Up @@ -1428,15 +1422,13 @@ abstract class StylesheetParser extends Parser {
var name = identifier();

if (name.startsWith('--')) {
warnings.add((
deprecation: Deprecation.cssFunctionMixin,
message:
'Sass @mixin names beginning with -- are deprecated for forward-'
'compatibility with plain CSS mixins.\n'
'\n'
'For details, see https://sass-lang.com/d/css-function-mixin',
span: scanner.spanFrom(beforeName),
));
error(
'Sass @mixin names beginning with -- are forbidden for forward-'
'compatibility with plain CSS mixins.\n'
'\n'
'For details, see https://sass-lang.com/d/css-function-mixin',
scanner.spanFrom(beforeName),
);
}

whitespace(consumeNewlines: false);
Expand Down Expand Up @@ -1736,22 +1728,27 @@ abstract class StylesheetParser extends Parser {
if (scanner.peekChar() != $exclamation && !atEndOfStatement()) {
value = _interpolatedDeclarationValue(allowOpenBrace: false);
}

AtRule rule;
if (lookingAtChildren()) {
rule = _withChildren(
_statement,
start,
(children, span) =>
AtRule(name, span, value: value, children: children),
);
} else {
expectStatementSeparator();
rule = AtRule(name, scanner.spanFrom(start), value: value);
var wasInPlainCssFunction = _inPlainCssFunction;
if (name.asPlain case var name? when equalsIgnoreCase(name, 'function')) {
_inPlainCssFunction = true;
}

_inUnknownAtRule = wasInUnknownAtRule;
return rule;
try {
if (lookingAtChildren()) {
return _withChildren(
_statement,
start,
(children, span) =>
AtRule(name, span, value: value, children: children),
);
} else {
expectStatementSeparator();
return AtRule(name, scanner.spanFrom(start), value: value);
}
} finally {
_inUnknownAtRule = wasInUnknownAtRule;
_inPlainCssFunction = wasInPlainCssFunction;
}
}

/// Throws a [StringScannerException] indicating that the at-rule starting at
Expand Down
Loading
Loading