diff --git a/CHANGELOG.md b/CHANGELOG.md index 56819570..b5162f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.27.0 +## 1.28.0 ### Dart API @@ -7,6 +7,85 @@ [HWB]: https://en.wikipedia.org/wiki/HWB_color_model +## 1.27.0 + +* Adds an overload to `map.merge()` that supports merging a nested map. + + `map.merge($map1, $keys..., $map2)`: The `$keys` form a path to the nested map + in `$map1`, into which `$map2` gets merged. + + See [the Sass documentation][map-merge] for more details. + + [map-merge]: https://sass-lang.com/documentation/modules/map#merge + +* Adds an overloaded `map.set()` function. + + `map.set($map, $key, $value)`: Adds to or updates `$map` with the specified + `$key` and `$value`. + + `map.set($map, $keys..., $value)`: Adds to or updates a map that is nested + within `$map`. The `$keys` form a path to the nested map in `$map`, into + which `$value` is inserted. + + See [the Sass documentation][map-set] for more details. + + [map-set]: https://sass-lang.com/documentation/modules/map#set + +* Add support for nested maps to `map.get()`. + For example, `map.get((a: (b: (c: d))), a, b, c)` would return `d`. + See [the documentation][map-get] for more details. + + [map-get]: https://sass-lang.com/documentation/modules/map#get + +* Add support for nested maps in `map.has-key`. + For example, `map.has-key((a: (b: (c: d))), a, b, c)` would return true. + See [the documentation][map-has-key] for more details. + + [map-has-key]: https://sass-lang.com/documentation/modules/map#has-key + +* Add a `map.deep-merge()` function. This works like `map.merge()`, except that + nested map values are *also* recursively merged. For example: + + ``` + map.deep-merge( + (color: (primary: red, secondary: blue), + (color: (secondary: teal) + ) // => (color: (primary: red, secondary: teal)) + ``` + + See [the Sass documentation][map-deep-merge] for more details. + + [map-deep-merge]: https://sass-lang.com/documentation/modules/map#deep-merge + +* Add a `map.deep-remove()` function. This allows you to remove keys from + nested maps by passing multiple keys. For example: + + ``` + map.deep-remove( + (color: (primary: red, secondary: blue)), + color, primary + ) // => (color: (secondary: blue)) + ``` + + See [the Sass documentation][map-deep-remove] for more details. + + [map-deep-remove]: https://sass-lang.com/documentation/modules/map#deep-remove + +* Fix a bug where custom property values in plain CSS were being parsed as + normal property values. + +### Dart API + +* Add a `Value.tryMap()` function which returns the `Value` as a `SassMap` if + it's a valid map, or `null` otherwise. This allows function authors to safely + 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 @@ -20,6 +99,9 @@ * Don't crash when writing `Infinity` in JS mode. +* Produce a better error message for positional arguments following named + arguments. + ## 1.26.10 * Fixes a bug where two adjacent combinators could cause an error. diff --git a/bin/sass.dart b/bin/sass.dart index f4391595..d74f39c6 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; import 'dart:isolate'; import 'package:path/path.dart' as p; diff --git a/lib/sass.dart b/lib/sass.dart index 992e1285..e45a610d 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -5,8 +5,6 @@ /// We strongly recommend importing this library with the prefix `sass`. library sass; -import 'dart:async'; - import 'package:source_maps/source_maps.dart'; import 'src/async_import_cache.dart'; diff --git a/lib/src/ast/css/declaration.dart b/lib/src/ast/css/declaration.dart index 01b26c3a..1bd7279f 100644 --- a/lib/src/ast/css/declaration.dart +++ b/lib/src/ast/css/declaration.dart @@ -24,5 +24,16 @@ 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` and [value] will + /// contain a [SassString]. + 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..e4bb7fc4 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,32 @@ 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 (parsedAsCustomProperty) { + if (!isCustomProperty) { + 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 ' + '(was `$value` of type ${value.value.runtimeType}).'); + } + } + } 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..6d6240ec 100644 --- a/lib/src/ast/sass/statement/declaration.dart +++ b/lib/src/ast/sass/statement/declaration.dart @@ -6,6 +6,7 @@ import 'package:source_span/source_span.dart'; import '../../../visitor/interface/statement.dart'; import '../expression.dart'; +import '../expression/string.dart'; import '../interpolation.dart'; import '../statement.dart'; import 'parent.dart'; @@ -20,10 +21,24 @@ 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}: ...`. + /// + /// If this is `true`, then `value` will be a [StringExpression]. + bool get isCustomProperty => name.initialPlain.startsWith('--'); + Declaration(this.name, this.span, {this.value, Iterable children}) - : super(children = children == null ? null : List.unmodifiable(children)); + : super( + children = children == null ? null : List.unmodifiable(children)) { + if (isCustomProperty && value is! StringExpression) { + throw ArgumentError( + 'Declarations whose names begin with "--" must have StringExpression ' + 'values (was `${value}` of type ${value.runtimeType}).'); + } + } T accept(StatementVisitor visitor) => visitor.visitDeclaration(this); - - String toString() => "$name: $value;"; } diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index 855f0f05..cc354a8c 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; import 'dart:convert'; import 'package:path/path.dart' as p; diff --git a/lib/src/async_environment.dart b/lib/src/async_environment.dart index 6a95da31..c8d73acc 100644 --- a/lib/src/async_environment.dart +++ b/lib/src/async_environment.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; import 'dart:collection'; import 'package:meta/meta.dart'; diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 03142a13..8725200a 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -2,8 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; - import 'package:collection/collection.dart'; import 'package:path/path.dart' as p; import 'package:tuple/tuple.dart'; diff --git a/lib/src/compile.dart b/lib/src/compile.dart index ea2793fd..0a0ecd5e 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 193c9bc1905022e881636f4d6359c906146abcb3 +// Checksum: bca3a79dd4a5c3905b07003b123172f3c876d2de // // ignore_for_file: unused_import diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 2e1cf4e1..f9d6eaa9 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_environment.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: d304e1c5208019bca99df0678c3fdb4d09776a2b +// Checksum: 9f4ee98a1c9e90d8d5277e0c2b0355460cda8788 // // ignore_for_file: unused_import diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 19a505e6..6e799118 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; import 'dart:convert'; import 'package:path/path.dart' as p; diff --git a/lib/src/executable/repl.dart b/lib/src/executable/repl.dart index 642d5984..cbf6097d 100644 --- a/lib/src/executable/repl.dart +++ b/lib/src/executable/repl.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; import 'dart:math' as math; import 'package:cli_repl/cli_repl.dart'; diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index 9e522411..76a1f228 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -7,6 +7,7 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import '../callable.dart'; +import '../exception.dart'; import '../module/built_in.dart'; import '../value.dart'; @@ -21,19 +22,89 @@ final global = UnmodifiableListView([ ]); /// The Sass map module. -final module = BuiltInModule("map", - functions: [_get, _merge, _remove, _keys, _values, _hasKey]); +final module = BuiltInModule("map", functions: [ + _get, + _set, + _merge, + _remove, + _keys, + _values, + _hasKey, + _deepMerge, + _deepRemove +]); -final _get = _function("get", r"$map, $key", (arguments) { +final _get = _function("get", r"$map, $key, $keys...", (arguments) { var map = arguments[0].assertMap("map"); - var key = arguments[1]; - return map.contents[key] ?? sassNull; + var keys = [arguments[1], ...arguments[2].asList]; + for (var key in keys.take(keys.length - 1)) { + var value = map.contents[key]; + if (value is SassMap) { + map = value; + } else { + return sassNull; + } + } + return map.contents[keys.last] ?? sassNull; }); -final _merge = _function("merge", r"$map1, $map2", (arguments) { +final _set = BuiltInCallable.overloadedFunction("set", { + r"$map, $key, $value": (arguments) { + var map = arguments[0].assertMap("map"); + return _modify(map, [arguments[1]], (_) => arguments[2]); + }, + r"$map, $args...": (arguments) { + var map = arguments[0].assertMap("map"); + var args = arguments[1].asList; + if (args.isEmpty) { + throw SassScriptException("Expected \$args to contain a key."); + } else if (args.length == 1) { + throw SassScriptException("Expected \$args to contain a value."); + } + return _modify(map, args.sublist(0, args.length - 1), (_) => args.last); + }, +}); + +final _merge = BuiltInCallable.overloadedFunction("merge", { + r"$map1, $map2": (arguments) { + var map1 = arguments[0].assertMap("map1"); + var map2 = arguments[1].assertMap("map2"); + return SassMap({...map1.contents, ...map2.contents}); + }, + r"$map1, $args...": (arguments) { + var map1 = arguments[0].assertMap("map1"); + var args = arguments[1].asList; + if (args.isEmpty) { + throw SassScriptException("Expected \$args to contain a key."); + } else if (args.length == 1) { + throw SassScriptException("Expected \$args to contain a map."); + } + var map2 = args.last.assertMap("map2"); + return _modify(map1, args.take(args.length - 1), (oldValue) { + var nestedMap = oldValue?.tryMap(); + if (nestedMap == null) return map2; + return SassMap({...nestedMap.contents, ...map2.contents}); + }); + }, +}); + +final _deepMerge = _function("deep-merge", r"$map1, $map2", (arguments) { var map1 = arguments[0].assertMap("map1"); var map2 = arguments[1].assertMap("map2"); - return SassMap({...map1.contents, ...map2.contents}); + return _deepMergeImpl(map1, map2); +}); + +final _deepRemove = + _function("deep-remove", r"$map, $key, $keys...", (arguments) { + var map = arguments[0].assertMap("map"); + var keys = [arguments[1], ...arguments[2].asList]; + return _modify(map, keys.take(keys.length - 1), (value) { + var nestedMap = value?.tryMap(); + if (nestedMap?.contents?.containsKey(keys.last) ?? false) { + return SassMap(Map.of(nestedMap.contents)..remove(keys.last)); + } + return value; + }); }); final _remove = BuiltInCallable.overloadedFunction("remove", { @@ -67,12 +138,100 @@ final _values = _function( (arguments) => SassList( arguments[0].assertMap("map").contents.values, ListSeparator.comma)); -final _hasKey = _function("has-key", r"$map, $key", (arguments) { +final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) { var map = arguments[0].assertMap("map"); - var key = arguments[1]; - return SassBoolean(map.contents.containsKey(key)); + var keys = [arguments[1], ...arguments[2].asList]; + for (var key in keys.take(keys.length - 1)) { + var value = map.contents[key]; + if (value is SassMap) { + map = value; + } else { + return sassFalse; + } + } + return SassBoolean(map.contents.containsKey(keys.last)); }); +/// Updates the specified value in [map] by applying the [modify] callback to +/// it, then returns the resulting map. +/// +/// If more than one key is provided, this means the map targeted for update is +/// nested within [map]. The multiple [keys] form a path of nested maps that +/// leads to the targeted map. If any value along the path is not a map, and +/// `modify(null)` returns null, this inserts a new map at that key and +/// overwrites the current value. Otherwise, this fails and returns [map] with +/// no changes. +/// +/// If no keys are provided, this passes [map] directly to modify and returns +/// the result. +Value _modify(SassMap map, Iterable keys, Value modify(Value old)) { + var keyIterator = keys.iterator; + SassMap _modifyNestedMap(SassMap map, [Value newValue]) { + var mutableMap = Map.of(map.contents); + var key = keyIterator.current; + + if (!keyIterator.moveNext()) { + mutableMap[key] = newValue ?? modify(mutableMap[key]); + return SassMap(mutableMap); + } + + var nestedMap = mutableMap[key]?.tryMap(); + if (nestedMap == null) { + // We pass null to `modify` here to indicate there's no existing value. + newValue = modify(null); + if (newValue == null) return SassMap(mutableMap); + } + + nestedMap ??= const SassMap.empty(); + mutableMap[key] = _modifyNestedMap(nestedMap, newValue); + return SassMap(mutableMap); + } + + return keyIterator.moveNext() ? _modifyNestedMap(map) : modify(map); +} + +/// Merges [map1] and [map2], with values in [map2] taking precedence. +/// +/// If both [map1] and [map2] have a map value associated with the same key, +/// this recursively merges those maps as well. +SassMap _deepMergeImpl(SassMap map1, SassMap map2) { + if (map2.contents.isEmpty) return map1; + + // Avoid making a mutable copy of `map2` if it would totally overwrite `map1` + // anyway. + var mutable = false; + var result = map2.contents; + void _ensureMutable() { + if (mutable) return; + mutable = true; + result = Map.of(result); + } + + // Because values in `map2` take precedence over `map1`, we just check if any + // entires in `map1` don't have corresponding keys in `map2`, or if they're + // maps that need to be merged in their own right. + map1.contents.forEach((key, value) { + var resultValue = result[key]; + if (resultValue == null) { + _ensureMutable(); + result[key] = value; + } else { + var resultMap = resultValue.tryMap(); + var valueMap = value.tryMap(); + + if (resultMap != null && valueMap != null) { + var merged = _deepMergeImpl(valueMap, resultMap); + if (identical(merged, resultMap)) return; + + _ensureMutable(); + result[key] = merged; + } + } + }); + + return mutable ? SassMap(result) : map2; +} + /// Like [new BuiltInCallable.function], but always sets the URL to `sass:map`. BuiltInCallable _function( String name, String arguments, Value callback(List arguments)) => diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index c86a1c9a..13887aca 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 8f54034c56e0d38fc8c90ad4d5f017628cab6190 +// Checksum: 5293a11e290c86829547ddd982ee3b1b1536dc73 // // ignore_for_file: unused_import diff --git a/lib/src/importer/node/interface.dart b/lib/src/importer/node/interface.dart index c7228bde..4ae9afb8 100644 --- a/lib/src/importer/node/interface.dart +++ b/lib/src/importer/node/interface.dart @@ -2,8 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; - import 'package:tuple/tuple.dart'; class NodeImporter { diff --git a/lib/src/io/interface.dart b/lib/src/io/interface.dart index 48815101..2835ee87 100644 --- a/lib/src/io/interface.dart +++ b/lib/src/io/interface.dart @@ -2,8 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; - import 'package:watcher/watcher.dart'; /// An output sink that writes to this process's standard error. diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index da95981b..3fea27a0 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -503,8 +503,11 @@ abstract class StylesheetParser extends Parser { /// This is only used in contexts where declarations are allowed but style /// rules are not, such as nested declarations. Otherwise, /// [_declarationOrStyleRule] is used instead. - @protected - Statement _propertyOrVariableDeclaration() { + /// + /// If [parseCustomProperties] is `true`, properties that begin with `--` will + /// be parsed using custom property parsing rules. + Statement _propertyOrVariableDeclaration( + {bool parseCustomProperties = true}) { var start = scanner.state; Interpolation name; @@ -533,6 +536,13 @@ abstract class StylesheetParser extends Parser { whitespace(); scanner.expectChar($colon); + + if (parseCustomProperties && name.initialPlain.startsWith('--')) { + var value = _interpolatedDeclarationValue(); + expectStatementSeparator("custom property"); + return Declaration(name, scanner.spanFrom(start), value: value); + } + whitespace(); if (lookingAtChildren()) { @@ -562,7 +572,7 @@ abstract class StylesheetParser extends Parser { /// Consumes a statement that's allowed within a declaration. Statement _declarationChild() { if (scanner.peekChar() == $at) return _declarationAtRule(); - return _propertyOrVariableDeclaration(); + return _propertyOrVariableDeclaration(parseCustomProperties: false); } // ## At Rules @@ -1581,7 +1591,8 @@ relase. For details, see http://bit.ly/moz-document. break; } } else if (named.isNotEmpty) { - scanner.expect("..."); + error("Positional arguments must come before keyword arguments.", + expression.span); } else { positional.add(expression); } diff --git a/lib/src/sync_package_resolver/node.dart b/lib/src/sync_package_resolver/node.dart index 7affa204..c0984dce 100644 --- a/lib/src/sync_package_resolver/node.dart +++ b/lib/src/sync_package_resolver/node.dart @@ -2,8 +2,6 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:async'; - class SyncPackageResolver { static final _error = UnsupportedError('SyncPackageResolver is not supported in JS.'); diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 4de346a6..69b96a50 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -2,7 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; import 'dart:math' as math; import 'package:charcode/charcode.dart'; diff --git a/lib/src/value.dart b/lib/src/value.dart index 320fe829..1bd532ca 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -98,6 +98,8 @@ abstract class Value implements ext.Value { SassMap assertMap([String name]) => throw _exception("$this is not a map.", name); + SassMap tryMap() => null; + SassNumber assertNumber([String name]) => throw _exception("$this is not a number.", name); diff --git a/lib/src/value/external/value.dart b/lib/src/value/external/value.dart index 50a0ea7c..2089726c 100644 --- a/lib/src/value/external/value.dart +++ b/lib/src/value/external/value.dart @@ -104,6 +104,10 @@ abstract class Value { /// (without the `$`). It's used for error reporting. SassMap assertMap([String name]); + /// Returns [this] as a [SassMap] if it is one (including empty lists, which + /// count as empty maps) or returns `null` if it's not. + SassMap tryMap(); + /// Throws a [SassScriptException] if [this] isn't a number. /// /// If this came from a function argument, [name] is the argument name diff --git a/lib/src/value/list.dart b/lib/src/value/list.dart index fc09eceb..1960e0c5 100644 --- a/lib/src/value/list.dart +++ b/lib/src/value/list.dart @@ -46,6 +46,8 @@ class SassList extends Value implements ext.SassList { SassMap assertMap([String name]) => asList.isEmpty ? const SassMap.empty() : super.assertMap(name); + SassMap tryMap() => asList.isEmpty ? const SassMap.empty() : null; + bool operator ==(Object other) => (other is SassList && other.separator == separator && diff --git a/lib/src/value/map.dart b/lib/src/value/map.dart index 9e187768..971b858c 100644 --- a/lib/src/value/map.dart +++ b/lib/src/value/map.dart @@ -32,6 +32,8 @@ class SassMap extends Value implements ext.SassMap { SassMap assertMap([String name]) => this; + SassMap tryMap() => this; + bool operator ==(Object other) => (other is SassMap && mapEquals(other.contents, contents)) || (contents.isEmpty && other is SassList && other.asList.isEmpty); 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); diff --git a/pubspec.yaml b/pubspec.yaml index aa86787d..48777c61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.27.0-dev +version: 1.28.0-dev description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/dart_api/value/boolean_test.dart b/test/dart_api/value/boolean_test.dart index abdcea4b..87543148 100644 --- a/test/dart_api/value/boolean_test.dart +++ b/test/dart_api/value/boolean_test.dart @@ -31,6 +31,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); @@ -56,6 +57,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); diff --git a/test/dart_api/value/color_test.dart b/test/dart_api/value/color_test.dart index d2eef137..4f88f60b 100644 --- a/test/dart_api/value/color_test.dart +++ b/test/dart_api/value/color_test.dart @@ -185,6 +185,7 @@ void main() { expect(value.assertBoolean, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); diff --git a/test/dart_api/value/function_test.dart b/test/dart_api/value/function_test.dart index 5a0f5547..bca96635 100644 --- a/test/dart_api/value/function_test.dart +++ b/test/dart_api/value/function_test.dart @@ -31,6 +31,7 @@ void main() { expect(value.assertBoolean, throwsSassScriptException); expect(value.assertColor, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); diff --git a/test/dart_api/value/list_test.dart b/test/dart_api/value/list_test.dart index 92734498..9db1dce8 100644 --- a/test/dart_api/value/list_test.dart +++ b/test/dart_api/value/list_test.dart @@ -110,6 +110,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); @@ -140,6 +141,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); @@ -167,6 +169,7 @@ void main() { test("counts as an empty map", () { expect(value.assertMap().contents, isEmpty); + expect(value.tryMap().contents, isEmpty); }); test("isn't any other type", () { diff --git a/test/dart_api/value/map_test.dart b/test/dart_api/value/map_test.dart index 2a8b19f3..23e241cb 100644 --- a/test/dart_api/value/map_test.dart +++ b/test/dart_api/value/map_test.dart @@ -128,6 +128,7 @@ void main() { test("is a map", () { expect(value.assertMap(), equals(value)); + expect(value.tryMap(), equals(value)); }); test("isn't any other type", () { diff --git a/test/dart_api/value/null_test.dart b/test/dart_api/value/null_test.dart index b95a5377..6e3e9295 100644 --- a/test/dart_api/value/null_test.dart +++ b/test/dart_api/value/null_test.dart @@ -27,6 +27,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); diff --git a/test/dart_api/value/number_test.dart b/test/dart_api/value/number_test.dart index 6e85ca46..f0640245 100644 --- a/test/dart_api/value/number_test.dart +++ b/test/dart_api/value/number_test.dart @@ -102,6 +102,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertString, throwsSassScriptException); }); }); diff --git a/test/dart_api/value/string_test.dart b/test/dart_api/value/string_test.dart index dfdae8b4..3e8c3642 100644 --- a/test/dart_api/value/string_test.dart +++ b/test/dart_api/value/string_test.dart @@ -37,6 +37,7 @@ void main() { expect(value.assertColor, throwsSassScriptException); expect(value.assertFunction, throwsSassScriptException); expect(value.assertMap, throwsSassScriptException); + expect(value.tryMap(), isNull); expect(value.assertNumber, throwsSassScriptException); });