diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b29b105..8d6b06cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,11 @@ retrieve maps even if they're internally stored as empty lists, without having to catch exceptions from `Value.assertMap()`. +## 1.26.12 + +* Fix a bug where nesting properties beneath a Sass-syntax custom property + (written as `#{--foo}: ...`) would crash. + ## 1.26.11 * **Potentially breaking bug fix:** `selector.nest()` now throws an error diff --git a/lib/src/ast/css/declaration.dart b/lib/src/ast/css/declaration.dart index 01b26c3a..7edaf2af 100644 --- a/lib/src/ast/css/declaration.dart +++ b/lib/src/ast/css/declaration.dart @@ -24,5 +24,15 @@ abstract class CssDeclaration extends CssNode { /// the variable was used. Otherwise, this is identical to [value.span]. FileSpan get valueSpanForMap; - T accept(CssVisitor visitor) => visitor.visitCssDeclaration(this); + /// 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. + /// + /// If this is `true`, [isCustomProperty] will also be `true`. + bool get parsedAsCustomProperty; + + T accept(CssVisitor visitor); } diff --git a/lib/src/ast/css/modifiable/declaration.dart b/lib/src/ast/css/modifiable/declaration.dart index fb7dc2b3..8d464550 100644 --- a/lib/src/ast/css/modifiable/declaration.dart +++ b/lib/src/ast/css/modifiable/declaration.dart @@ -2,6 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../../../value.dart'; @@ -15,13 +16,26 @@ class ModifiableCssDeclaration extends ModifiableCssNode implements CssDeclaration { final CssValue name; final CssValue value; + final bool parsedAsCustomProperty; final FileSpan valueSpanForMap; final FileSpan span; + bool get isCustomProperty => name.value.startsWith('--'); + + /// Returns a new CSS declaration with the given properties. ModifiableCssDeclaration(this.name, this.value, this.span, - {FileSpan valueSpanForMap}) - : valueSpanForMap = valueSpanForMap ?? span; + {@required bool parsedAsCustomProperty, FileSpan valueSpanForMap}) + : parsedAsCustomProperty = parsedAsCustomProperty, + valueSpanForMap = valueSpanForMap ?? span { + if (!isCustomProperty && parsedAsCustomProperty) { + throw ArgumentError( + 'sassSyntaxCustomProperty must be false if name doesn\'t begin with ' + '"--".'); + } + } T accept(ModifiableCssVisitor visitor) => visitor.visitCssDeclaration(this); + + String toString() => "$name: $value;"; } diff --git a/lib/src/ast/sass/statement/declaration.dart b/lib/src/ast/sass/statement/declaration.dart index 269d4080..a720b998 100644 --- a/lib/src/ast/sass/statement/declaration.dart +++ b/lib/src/ast/sass/statement/declaration.dart @@ -20,10 +20,15 @@ class Declaration extends ParentStatement { final FileSpan span; + /// Returns whether this is a CSS Custom Property declaration. + /// + /// Note that this can return `false` for declarations that will ultimately be + /// serialized as custom properties if they aren't *parsed as* custom + /// properties, such as `#{--foo}: ...`. + bool get isCustomProperty => name.initialPlain.startsWith('--'); + Declaration(this.name, this.span, {this.value, Iterable children}) : super(children = children == null ? null : List.unmodifiable(children)); T accept(StatementVisitor visitor) => visitor.visitDeclaration(this); - - String toString() => "$name: $value;"; } diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 56a25b8f..4cb43ad1 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1039,8 +1039,9 @@ class _EvaluateVisitor if (cssValue != null && (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { _parent.addChild(ModifiableCssDeclaration(name, cssValue, node.span, + parsedAsCustomProperty: node.isCustomProperty, valueSpanForMap: _expressionNode(node.value)?.span)); - } else if (name.value.startsWith('--')) { + } else if (name.value.startsWith('--') && node.children == null) { throw _exception( "Custom property values may not be empty.", node.value.span); } @@ -2559,6 +2560,7 @@ class _EvaluateVisitor Future visitCssDeclaration(CssDeclaration node) async { _parent.addChild(ModifiableCssDeclaration(node.name, node.value, node.span, + parsedAsCustomProperty: node.isCustomProperty, valueSpanForMap: node.valueSpanForMap)); } diff --git a/lib/src/visitor/clone_css.dart b/lib/src/visitor/clone_css.dart index 95211f24..ae4fe115 100644 --- a/lib/src/visitor/clone_css.dart +++ b/lib/src/visitor/clone_css.dart @@ -44,6 +44,7 @@ class _CloneCssVisitor implements CssVisitor { ModifiableCssDeclaration visitCssDeclaration(CssDeclaration node) => ModifiableCssDeclaration(node.name, node.value, node.span, + parsedAsCustomProperty: node.parsedAsCustomProperty, valueSpanForMap: node.valueSpanForMap); ModifiableCssImport visitCssImport(CssImport node) => diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 8dd10751..d8e6e71f 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: f6fe6645ccec58216ef623851bd2594de291a360 +// Checksum: 651a7e9f78b68bfd440241304301cf78711553a4 // // ignore_for_file: unused_import @@ -1041,8 +1041,9 @@ class _EvaluateVisitor if (cssValue != null && (!cssValue.value.isBlank || _isEmptyList(cssValue.value))) { _parent.addChild(ModifiableCssDeclaration(name, cssValue, node.span, + parsedAsCustomProperty: node.isCustomProperty, valueSpanForMap: _expressionNode(node.value)?.span)); - } else if (name.value.startsWith('--')) { + } else if (name.value.startsWith('--') && node.children == null) { throw _exception( "Custom property values may not be empty.", node.value.span); } @@ -2541,6 +2542,7 @@ class _EvaluateVisitor void visitCssDeclaration(CssDeclaration node) { _parent.addChild(ModifiableCssDeclaration(node.name, node.value, node.span, + parsedAsCustomProperty: node.isCustomProperty, valueSpanForMap: node.valueSpanForMap)); } diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 450126f0..e5654fef 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -333,7 +333,10 @@ class _SerializeVisitor _write(node.name); _buffer.writeCharCode($colon); - if (_isParsedCustomProperty(node)) { + // 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) { _for(node.value, () { if (_isCompressed) { _writeFoldedValue(node); @@ -355,20 +358,6 @@ class _SerializeVisitor } } - /// Returns whether [node] is a custom property that was parsed as a custom - /// property (rather than being dynamically generated, as in `#{--foo}: ...`). - /// - /// We only re-indent custom property values that were parsed as custom - /// properties, which we detect as unquoted strings. It's possible to have - /// false positives here, since someone could write `#{--foo}: unquoted`, but - /// that's unlikely enough that we can spare the extra time a no-op - /// reindenting will take. - bool _isParsedCustomProperty(CssDeclaration node) { - if (!node.name.value.startsWith("--")) return false; - var value = node.value.value; - return value is SassString && !value.hasQuotes; - } - /// Emits the value of [node], with all newlines followed by whitespace void _writeFoldedValue(CssDeclaration node) { var scanner = StringScanner((node.value.value as SassString).text);