Skip to content

Commit 9c12eb4

Browse files
committed
Add support for plain-CSS @function rules
See #2197
1 parent b8b35e8 commit 9c12eb4

File tree

16 files changed

+144
-126
lines changed

16 files changed

+144
-126
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 1.94.0
2+
3+
* **Potentially breaking compatibility fix:** `@function` rules whose names
4+
begin with `--` are now parsed as unknown at-rules to support the plain CSS
5+
`@function` rule. Within this rule, the `result` property is parsed as raw
6+
CSS just like custom properties.
7+
8+
* **Potentially breaking compatibility fix:** `@mixin` rules whose names begin
9+
with `--` are now errors. These are not yet parsed as unknown at-rules because
10+
no browser currently supports CSS mixins.
11+
112
## 1.93.2
213

314
* No user-visible changes.

lib/src/ast/css/declaration.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ abstract interface class CssDeclaration implements CssNode {
3333
/// Returns whether this is a CSS Custom Property declaration.
3434
bool get isCustomProperty;
3535

36-
/// Whether this is was originally parsed as a custom property declaration, as
37-
/// opposed to using something like `#{--foo}: ...` to cause it to be parsed
38-
/// as a normal Sass declaration.
36+
/// Whether this property's value was originally parsed as SassScript, as
37+
/// opposed to a custom property which is parsed as an interpolated sequence
38+
/// of tokens.
3939
///
40-
/// If this is `true`, [isCustomProperty] will also be `true` and [value] will
41-
/// contain a [SassString].
42-
bool get parsedAsCustomProperty;
40+
/// If this is `false`, [value] will contain an unquoted [SassString].
41+
/// [isCustomProperty] will *usually* be true, but there are other properties
42+
/// that may not be parsed as SassScript, like `return` in a plain CSS
43+
/// `@function`.
44+
bool get parsedAsSassScript;
4345
}

lib/src/ast/css/modifiable/declaration.dart

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class ModifiableCssDeclaration extends ModifiableCssNode
1616
implements CssDeclaration {
1717
final CssValue<String> name;
1818
final CssValue<Value> value;
19-
final bool parsedAsCustomProperty;
19+
final bool parsedAsSassScript;
2020
final Trace? trace;
2121
final FileSpan valueSpanForMap;
2222
final FileSpan span;
@@ -28,19 +28,14 @@ final class ModifiableCssDeclaration extends ModifiableCssNode
2828
this.name,
2929
this.value,
3030
this.span, {
31-
required this.parsedAsCustomProperty,
31+
required this.parsedAsSassScript,
3232
this.trace,
3333
FileSpan? valueSpanForMap,
3434
}) : valueSpanForMap = valueSpanForMap ?? value.span {
35-
if (parsedAsCustomProperty) {
36-
if (!isCustomProperty) {
35+
if (!parsedAsSassScript) {
36+
if (value.value is! SassString) {
3737
throw ArgumentError(
38-
'parsedAsCustomProperty must be false if name doesn\'t begin with '
39-
'"--".',
40-
);
41-
} else if (value.value is! SassString) {
42-
throw ArgumentError(
43-
'If parsedAsCustomProperty is true, value must contain a SassString '
38+
'If parsedAsSassScript is false, value must contain a SassString '
4439
'(was `$value` of type ${value.value.runtimeType}).',
4540
);
4641
}

lib/src/ast/sass/statement/declaration.dart

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,31 @@ final class Declaration extends ParentStatement {
2828

2929
final FileSpan span;
3030

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

4346
/// Creates a declaration with no children.
44-
Declaration(this.name, this.value, this.span) : super(null);
47+
Declaration(this.name, this.value, this.span)
48+
: parsedAsSassScript = true,
49+
super(null);
50+
51+
/// Creates a declaration with no children whose value is not parsed as
52+
/// SassScript.
53+
Declaration.notSassScript(this.name, StringExpression this.value, this.span)
54+
: parsedAsSassScript = false,
55+
super(null);
4556

4657
/// Creates a declaration with children.
4758
///
@@ -51,7 +62,8 @@ final class Declaration extends ParentStatement {
5162
Iterable<Statement> children,
5263
this.span, {
5364
this.value,
54-
}) : super(List.unmodifiable(children));
65+
}) : parsedAsSassScript = true,
66+
super(List.unmodifiable(children));
5567

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

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

6375
if (value != null) {
64-
if (!isCustomProperty) buffer.writeCharCode($space);
76+
if (parsedAsSassScript) buffer.writeCharCode($space);
6577
buffer.write("$value");
6678
}
6779

lib/src/deprecation.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ enum Deprecation {
1515
// DO NOT EDIT. This section was generated from the language repo.
1616
// See tool/grind/generate_deprecations.dart for details.
1717
//
18-
// Checksum: 0d3df25297a4e76b865aee1e908baf355e26d665
18+
// Checksum: fc5363081eb9d8e9e7220b9d5c7fd928d67009fe
1919

2020
/// Deprecation for passing a string directly to meta.call().
2121
callString('call-string',

lib/src/parse/css.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ class CssParser extends ScssParser {
6767
"error" ||
6868
"extend" ||
6969
"for" ||
70-
"function" ||
7170
"if" ||
7271
"include" ||
7372
"mixin" ||
@@ -76,6 +75,7 @@ class CssParser extends ScssParser {
7675
"while" =>
7776
_forbiddenAtRule(start),
7877
"import" => _cssImportRule(start),
78+
"function" => _cssFunctionRule(start, name),
7979
"media" => mediaRule(start),
8080
"-moz-document" => mozDocumentRule(start, name),
8181
"supports" => supportsRule(start),
@@ -132,6 +132,22 @@ class CssParser extends ScssParser {
132132
], scanner.spanFrom(start));
133133
}
134134

135+
/// Consumes a plain CSS function declaration.
136+
///
137+
/// [start] should point before the `@`.
138+
Statement _cssFunctionRule(LineScannerState start, Interpolation atRuleName) {
139+
var afterRuleName = scanner.state;
140+
whitespace(consumeNewlines: true);
141+
142+
if (!scanner.matches('--')) {
143+
almostAnyValue();
144+
error(
145+
"This at-rule isn't allowed in plain CSS.", scanner.spanFrom(start));
146+
} else {
147+
return unknownAtRule(start, atRuleName);
148+
}
149+
}
150+
135151
ParenthesizedExpression parentheses() {
136152
// Expressions are only allowed within calculations, but we verify this at
137153
// evaluation time.

lib/src/parse/stylesheet.dart

Lines changed: 49 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ abstract class StylesheetParser extends Parser {
5050
/// Whether the parser is currently parsing an unknown rule.
5151
var _inUnknownAtRule = false;
5252

53+
/// Whether the parser is currently parsing a plain-CSS `@function` rule.
54+
var _inPlainCssFunction = false;
55+
5356
/// Whether the parser is currently parsing a style rule.
5457
var _inStyleRule = false;
5558

@@ -411,14 +414,19 @@ abstract class StylesheetParser extends Parser {
411414

412415
// Parse custom properties as declarations no matter what.
413416
var name = nameBuffer.interpolation(scanner.spanFrom(start, beforeColon));
414-
if (name.initialPlain.startsWith('--')) {
417+
var customProperty = name.initialPlain.startsWith('--');
418+
if (customProperty ||
419+
(_inPlainCssFunction &&
420+
(name.asPlain.andThen((name) => equalsIgnoreCase(name, 'result')) ??
421+
false))) {
415422
var value = StringExpression(
416423
atEndOfStatement()
417424
? Interpolation(const [], const [], scanner.emptySpan)
418425
: _interpolatedDeclarationValue(silentComments: false),
419426
);
420-
expectStatementSeparator("custom property");
421-
return Declaration(name, value, scanner.spanFrom(start));
427+
expectStatementSeparator(
428+
customProperty ? "custom property" : "@function result");
429+
return Declaration.notSassScript(name, value, scanner.spanFrom(start));
422430
}
423431

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

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

577-
if (parseCustomProperties && name.initialPlain.startsWith('--')) {
578-
var value = StringExpression(
579-
_interpolatedDeclarationValue(silentComments: false),
580-
);
581-
expectStatementSeparator("custom property");
582-
return Declaration(name, value, scanner.spanFrom(start));
582+
if (name.initialPlain.startsWith('--')) {
583+
error('Declarations whose names begin with "--" may not be nested.',
584+
name.span);
583585
}
584586

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

624626
// ## At Rules
625627

@@ -669,7 +671,7 @@ abstract class StylesheetParser extends Parser {
669671
if (!root) _disallowedAtRule(start);
670672
return _forwardRule(start);
671673
case "function":
672-
return _functionRule(start);
674+
return _functionRule(start, name);
673675
case "if":
674676
return _ifRule(start, child);
675677
case "import":
@@ -914,24 +916,16 @@ abstract class StylesheetParser extends Parser {
914916
/// Consumes a function declaration.
915917
///
916918
/// [start] should point before the `@`.
917-
FunctionRule _functionRule(LineScannerState start) {
919+
Statement _functionRule(LineScannerState start, Interpolation atRuleName) {
918920
whitespace(consumeNewlines: true);
919921
var precedingComment = lastSilentComment;
920922
lastSilentComment = null;
921923
var beforeName = scanner.state;
922-
var name = identifier();
923924

924-
if (name.startsWith('--')) {
925-
warnings.add((
926-
deprecation: Deprecation.cssFunctionMixin,
927-
message:
928-
'Sass @function names beginning with -- are deprecated for forward-'
929-
'compatibility with plain CSS functions.\n'
930-
'\n'
931-
'For details, see https://sass-lang.com/d/css-function-mixin',
932-
span: scanner.spanFrom(beforeName),
933-
));
934-
} else if (equalsIgnoreCase(name, 'type')) {
925+
if (scanner.matches('--')) return unknownAtRule(start, atRuleName);
926+
927+
var name = identifier();
928+
if (equalsIgnoreCase(name, 'type')) {
935929
error('This name is reserved for the plain-CSS function.',
936930
scanner.spanFrom(beforeName));
937931
}
@@ -1428,15 +1422,13 @@ abstract class StylesheetParser extends Parser {
14281422
var name = identifier();
14291423

14301424
if (name.startsWith('--')) {
1431-
warnings.add((
1432-
deprecation: Deprecation.cssFunctionMixin,
1433-
message:
1434-
'Sass @mixin names beginning with -- are deprecated for forward-'
1435-
'compatibility with plain CSS mixins.\n'
1436-
'\n'
1437-
'For details, see https://sass-lang.com/d/css-function-mixin',
1438-
span: scanner.spanFrom(beforeName),
1439-
));
1425+
error(
1426+
'Sass @mixin names beginning with -- are forbidden for forward-'
1427+
'compatibility with plain CSS mixins.\n'
1428+
'\n'
1429+
'For details, see https://sass-lang.com/d/css-function-mixin',
1430+
scanner.spanFrom(beforeName),
1431+
);
14401432
}
14411433

14421434
whitespace(consumeNewlines: false);
@@ -1736,22 +1728,27 @@ abstract class StylesheetParser extends Parser {
17361728
if (scanner.peekChar() != $exclamation && !atEndOfStatement()) {
17371729
value = _interpolatedDeclarationValue(allowOpenBrace: false);
17381730
}
1739-
1740-
AtRule rule;
1741-
if (lookingAtChildren()) {
1742-
rule = _withChildren(
1743-
_statement,
1744-
start,
1745-
(children, span) =>
1746-
AtRule(name, span, value: value, children: children),
1747-
);
1748-
} else {
1749-
expectStatementSeparator();
1750-
rule = AtRule(name, scanner.spanFrom(start), value: value);
1731+
var wasInPlainCssFunction = _inPlainCssFunction;
1732+
if (name.asPlain case var name? when equalsIgnoreCase(name, 'function')) {
1733+
_inPlainCssFunction = true;
17511734
}
17521735

1753-
_inUnknownAtRule = wasInUnknownAtRule;
1754-
return rule;
1736+
try {
1737+
if (lookingAtChildren()) {
1738+
return _withChildren(
1739+
_statement,
1740+
start,
1741+
(children, span) =>
1742+
AtRule(name, span, value: value, children: children),
1743+
);
1744+
} else {
1745+
expectStatementSeparator();
1746+
return AtRule(name, scanner.spanFrom(start), value: value);
1747+
}
1748+
} finally {
1749+
_inUnknownAtRule = wasInUnknownAtRule;
1750+
_inPlainCssFunction = wasInPlainCssFunction;
1751+
}
17551752
}
17561753

17571754
/// Throws a [StringScannerException] indicating that the at-rule starting at

0 commit comments

Comments
 (0)