diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f510e21..c6a936004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/lib/src/ast/css/declaration.dart b/lib/src/ast/css/declaration.dart index 1c7f4cfa3..66d1f07e2 100644 --- a/lib/src/ast/css/declaration.dart +++ b/lib/src/ast/css/declaration.dart @@ -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; } diff --git a/lib/src/ast/css/modifiable/declaration.dart b/lib/src/ast/css/modifiable/declaration.dart index 90660b279..d033beaaf 100644 --- a/lib/src/ast/css/modifiable/declaration.dart +++ b/lib/src/ast/css/modifiable/declaration.dart @@ -16,7 +16,7 @@ final class ModifiableCssDeclaration extends ModifiableCssNode implements CssDeclaration { final CssValue name; final CssValue value; - final bool parsedAsCustomProperty; + final bool parsedAsSassScript; final Trace? trace; final FileSpan valueSpanForMap; final FileSpan span; @@ -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}).', ); } diff --git a/lib/src/ast/sass/statement/declaration.dart b/lib/src/ast/sass/statement/declaration.dart index 2ba768b26..7ec64dc35 100644 --- a/lib/src/ast/sass/statement/declaration.dart +++ b/lib/src/ast/sass/statement/declaration.dart @@ -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. /// @@ -51,7 +62,8 @@ final class Declaration extends ParentStatement { Iterable children, this.span, { this.value, - }) : super(List.unmodifiable(children)); + }) : parsedAsSassScript = true, + super(List.unmodifiable(children)); T accept(StatementVisitor visitor) => visitor.visitDeclaration(this); @@ -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"); } diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index cbae673aa..69955ed04 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -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', diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index beb403204..c562be1d7 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -67,7 +67,6 @@ class CssParser extends ScssParser { "error" || "extend" || "for" || - "function" || "if" || "include" || "mixin" || @@ -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), @@ -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. diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 9849bbbfa..3ca039198 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -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; @@ -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)) { @@ -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; @@ -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); @@ -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 @@ -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": @@ -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)); } @@ -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); @@ -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 diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 1041ffff7..94cb0cd6c 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1344,9 +1344,11 @@ final class _EvaluateVisitor node.span, ); } - if (_declarationName != null && node.isCustomProperty) { + if (_declarationName != null && !node.parsedAsSassScript) { throw _exception( - 'Declarations whose names begin with "--" may not be nested.', + node.name.initialPlain.startsWith('--') + ? 'Declarations whose names begin with "--" may not be nested.' + : 'Declarations parsed as raw CSS may not be nested.', node.span, ); } @@ -1370,7 +1372,7 @@ final class _EvaluateVisitor name, CssValue(value, expression.span), node.span, - parsedAsCustomProperty: node.isCustomProperty, + parsedAsSassScript: node.parsedAsSassScript, trace: _stackTrace(node.span), valueSpanForMap: _sourceMap ? node.value.andThen(_expressionNode)?.span : null, @@ -2144,13 +2146,12 @@ final class _EvaluateVisitor if (node.originalName.startsWith('--') && mixin is UserDefinedCallable && !mixin.declaration.originalName.startsWith('--')) { - _warn( - 'Sass @mixin names beginning with -- are deprecated for forward-' + throw _exception( + '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', node.nameSpan, - Deprecation.cssFunctionMixin, ); } @@ -2902,7 +2903,7 @@ final class _EvaluateVisitor namespace: node.namespace, ), ); - if (function == null) { + if (function == null || node.originalName.startsWith('--')) { if (node.namespace != null) { throw _exception("Undefined function.", node.span); } @@ -2943,19 +2944,6 @@ final class _EvaluateVisitor PlainCssCallable(node.originalName); } - if (node.originalName.startsWith('--') && - function is UserDefinedCallable && - !function.declaration.originalName.startsWith('--')) { - _warn( - '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', - node.nameSpan, - Deprecation.cssFunctionMixin, - ); - } - var oldInFunction = _inFunction; _inFunction = true; var result = await _addErrorSpan( @@ -3951,7 +3939,7 @@ final class _EvaluateVisitor node.name, node.value, node.span, - parsedAsCustomProperty: node.parsedAsCustomProperty, + parsedAsSassScript: node.parsedAsSassScript, valueSpanForMap: node.valueSpanForMap, ), ); diff --git a/lib/src/visitor/clone_css.dart b/lib/src/visitor/clone_css.dart index 3bc3a4e13..3e8a49d94 100644 --- a/lib/src/visitor/clone_css.dart +++ b/lib/src/visitor/clone_css.dart @@ -50,7 +50,7 @@ final class _CloneCssVisitor implements CssVisitor { node.name, node.value, node.span, - parsedAsCustomProperty: node.parsedAsCustomProperty, + parsedAsSassScript: node.parsedAsSassScript, valueSpanForMap: node.valueSpanForMap, ); diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 82802e74f..8b0c6194a 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: b79ed685f78b8d5356795eb2d1112f830abaf58e +// Checksum: 44f7ea0b4bb32d21ae00b1677500b31dc321a6f9 // // ignore_for_file: unused_import @@ -1352,9 +1352,11 @@ final class _EvaluateVisitor node.span, ); } - if (_declarationName != null && node.isCustomProperty) { + if (_declarationName != null && !node.parsedAsSassScript) { throw _exception( - 'Declarations whose names begin with "--" may not be nested.', + node.name.initialPlain.startsWith('--') + ? 'Declarations whose names begin with "--" may not be nested.' + : 'Declarations parsed as raw CSS may not be nested.', node.span, ); } @@ -1378,7 +1380,7 @@ final class _EvaluateVisitor name, CssValue(value, expression.span), node.span, - parsedAsCustomProperty: node.isCustomProperty, + parsedAsSassScript: node.parsedAsSassScript, trace: _stackTrace(node.span), valueSpanForMap: _sourceMap ? node.value.andThen(_expressionNode)?.span : null, @@ -2151,13 +2153,12 @@ final class _EvaluateVisitor if (node.originalName.startsWith('--') && mixin is UserDefinedCallable && !mixin.declaration.originalName.startsWith('--')) { - _warn( - 'Sass @mixin names beginning with -- are deprecated for forward-' + throw _exception( + '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', node.nameSpan, - Deprecation.cssFunctionMixin, ); } @@ -2903,7 +2904,7 @@ final class _EvaluateVisitor namespace: node.namespace, ), ); - if (function == null) { + if (function == null || node.originalName.startsWith('--')) { if (node.namespace != null) { throw _exception("Undefined function.", node.span); } @@ -2944,19 +2945,6 @@ final class _EvaluateVisitor PlainCssCallable(node.originalName); } - if (node.originalName.startsWith('--') && - function is UserDefinedCallable && - !function.declaration.originalName.startsWith('--')) { - _warn( - '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', - node.nameSpan, - Deprecation.cssFunctionMixin, - ); - } - var oldInFunction = _inFunction; _inFunction = true; var result = _addErrorSpan( @@ -3952,7 +3940,7 @@ final class _EvaluateVisitor node.name, node.value, node.span, - parsedAsCustomProperty: node.parsedAsCustomProperty, + parsedAsSassScript: node.parsedAsSassScript, valueSpanForMap: node.valueSpanForMap, ), ); diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index fac8a6ef7..09e5efe81 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -363,7 +363,7 @@ final class _SerializeVisitor // If `node` is a custom property that was parsed as a normal Sass-syntax // property (such as `#{--foo}: ...`), we serialize its value using the // normal Sass property logic as well. - if (node.isCustomProperty && node.parsedAsCustomProperty) { + if (!node.parsedAsSassScript) { _for(node.value, () { if (_isCompressed) { _writeFoldedValue(node); diff --git a/pkg/sass-parser/CHANGELOG.md b/pkg/sass-parser/CHANGELOG.md index f3661143a..fad954c13 100644 --- a/pkg/sass-parser/CHANGELOG.md +++ b/pkg/sass-parser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.32 + +* No user-visible changes. + ## 0.4.31 * No user-visible changes. diff --git a/pkg/sass-parser/package.json b/pkg/sass-parser/package.json index 561931527..2512d3950 100644 --- a/pkg/sass-parser/package.json +++ b/pkg/sass-parser/package.json @@ -1,6 +1,6 @@ { "name": "sass-parser", - "version": "0.4.31", + "version": "0.4.32", "description": "A PostCSS-compatible wrapper of the official Sass parser", "repository": "sass/sass", "author": "Google Inc.", diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index b5626bbe6..9911c0837 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,8 @@ +## 15.13.0 + +* Add a `Declaration.notSassScript()` constructor for raw CSS declarations such + as custom properties and the `result` property in plain CSS `@function` rules. + ## 15.12.2 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 1e4eda01a..030fb74e5 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 15.12.2 +version: 15.13.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.6.0 <4.0.0" dependencies: - sass: 1.93.2 + sass: 1.94.0 dev_dependencies: dartdoc: ^8.0.14 diff --git a/pubspec.yaml b/pubspec.yaml index d90577e21..a95138083 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.93.2 +version: 1.94.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass