From 028b2f6a0197dc6c1e834ad51168d796d2c3e91c Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 15 Sep 2020 16:23:01 -0700 Subject: [PATCH 1/6] Add a map.deep-merge() function (#1077) (#1080) This also adds a Value.tryMap() function, which was useful for implementing this and may be more generally useful to users as well. See sass/sass#2836 See sass/sass-spec#1560 --- CHANGELOG.md | 23 ++++++++++++ lib/src/functions/map.dart | 50 +++++++++++++++++++++++++- lib/src/value.dart | 2 ++ lib/src/value/external/value.dart | 4 +++ lib/src/value/list.dart | 2 ++ lib/src/value/map.dart | 2 ++ pubspec.yaml | 2 +- test/dart_api/value/boolean_test.dart | 2 ++ test/dart_api/value/color_test.dart | 1 + test/dart_api/value/function_test.dart | 1 + test/dart_api/value/list_test.dart | 3 ++ test/dart_api/value/map_test.dart | 1 + test/dart_api/value/null_test.dart | 1 + test/dart_api/value/number_test.dart | 1 + test/dart_api/value/string_test.dart | 1 + 15 files changed, 94 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b82dd9..86b0168d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 1.27.0 + +* 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 + +### 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.11 * **Potentially breaking bug fix:** `selector.nest()` now throws an error diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index 9e522411..fd7b8ff3 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -22,7 +22,7 @@ final global = UnmodifiableListView([ /// The Sass map module. final module = BuiltInModule("map", - functions: [_get, _merge, _remove, _keys, _values, _hasKey]); + functions: [_get, _merge, _remove, _keys, _values, _hasKey, _deepMerge]); final _get = _function("get", r"$map, $key", (arguments) { var map = arguments[0].assertMap("map"); @@ -36,6 +36,12 @@ final _merge = _function("merge", r"$map1, $map2", (arguments) { return SassMap({...map1.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 _deepMergeImpl(map1, map2); +}); + final _remove = BuiltInCallable.overloadedFunction("remove", { // Because the signature below has an explicit `$key` argument, it doesn't // allow zero keys to be passed. We want to allow that case, so we add an @@ -73,6 +79,48 @@ final _hasKey = _function("has-key", r"$map, $key", (arguments) { return SassBoolean(map.contents.containsKey(key)); }); +/// 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/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/pubspec.yaml b/pubspec.yaml index 03d66083..aa86787d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.26.11-dev +version: 1.27.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 6b296c01..bef97a7e 100644 --- a/test/dart_api/value/color_test.dart +++ b/test/dart_api/value/color_test.dart @@ -139,6 +139,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); }); From 6b66241e95c00317038d71426bc4bfe41c9b2b4e Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Wed, 16 Sep 2020 13:23:52 -0700 Subject: [PATCH 2/6] Add support for nested maps to map-get (#1076) --- CHANGELOG.md | 8 +++++++- lib/src/functions/map.dart | 14 +++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86b0168d..c5c8578d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ ## 1.27.0 +* 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 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), diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index fd7b8ff3..1a6bd00d 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -24,10 +24,18 @@ final global = UnmodifiableListView([ final module = BuiltInModule("map", functions: [_get, _merge, _remove, _keys, _values, _hasKey, _deepMerge]); -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) { From f5e3a5a6699ebdd82ee6b03714ac9b0b72d585e9 Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Wed, 16 Sep 2020 14:41:23 -0700 Subject: [PATCH 3/6] Add support for nested maps to has-key (#1075) --- CHANGELOG.md | 6 ++++++ lib/src/functions/map.dart | 14 +++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c8578d..5448bf0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ [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: diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index 1a6bd00d..a93882bc 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -81,10 +81,18 @@ 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)); }); /// Merges [map1] and [map2], with values in [map2] taking precedence. From 5b7471664d22787012f60367538145a06cee407e Mon Sep 17 00:00:00 2001 From: Awjin Ahn Date: Wed, 16 Sep 2020 17:47:35 -0500 Subject: [PATCH 4/6] Add a map.set() function (#1078) --- CHANGELOG.md | 14 +++++++++++++ lib/src/functions/map.dart | 43 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5448bf0a..37ea31f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ ## 1.27.0 +* 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, $args...)`: Adds to or updates a map that is nested within + `$map`. `$args` contains multiple keys, followed by a value. The keys + form a path to the nested map in `$map` where the value is to be added or + updated. + + 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. diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index a93882bc..3362b721 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,8 +22,9 @@ final global = UnmodifiableListView([ ]); /// The Sass map module. -final module = BuiltInModule("map", - functions: [_get, _merge, _remove, _keys, _values, _hasKey, _deepMerge]); +final module = BuiltInModule("map", functions: [ + _get, _set, _merge, _remove, _keys, _values, _hasKey, _deepMerge // +]); final _get = _function("get", r"$map, $key, $keys...", (arguments) { var map = arguments[0].assertMap("map"); @@ -38,6 +40,23 @@ final _get = _function("get", r"$map, $key, $keys...", (arguments) { return map.contents[keys.last] ?? sassNull; }); +final _set = BuiltInCallable.overloadedFunction("set", { + r"$map, $key, $value": (arguments) { + var map = arguments[0].assertMap("map"); + return _setImpl(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 _setImpl(map, args.sublist(0, args.length - 1), args.last); + }, +}); + final _merge = _function("merge", r"$map1, $map2", (arguments) { var map1 = arguments[0].assertMap("map1"); var map2 = arguments[1].assertMap("map2"); @@ -95,6 +114,26 @@ final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) { return SassBoolean(map.contents.containsKey(keys.last)); }); +/// Updates a map with the given [value]. +/// +/// 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 targetted map. If any value along the path is not a map, this +/// creates and inserts a new map at that key. +SassMap _setImpl(SassMap map, List keys, Value value, [int index = 0]) { + var mutableMap = Map.of(map.contents); + var key = keys[index]; + + if (index == keys.length - 1) { + mutableMap[key] = value; + return SassMap(mutableMap); + } + + var nestedMap = mutableMap[key].tryMap() ?? const SassMap.empty(); + mutableMap[key] = _setImpl(nestedMap, keys, value, index + 1); + return SassMap(mutableMap); +} + /// Merges [map1] and [map2], with values in [map2] taking precedence. /// /// If both [map1] and [map2] have a map value associated with the same key, From 45703ab5976b78bec401bb01e61d59cc4e063703 Mon Sep 17 00:00:00 2001 From: Awjin Ahn Date: Fri, 18 Sep 2020 14:18:27 -0500 Subject: [PATCH 5/6] Adds nested map support to map-merge. (#1083) --- CHANGELOG.md | 16 +++++++--- lib/src/functions/map.dart | 63 ++++++++++++++++++++++++++++---------- 2 files changed, 58 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ea31f6..434f2098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,22 @@ ## 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, $args...)`: Adds to or updates a map that is nested within - `$map`. `$args` contains multiple keys, followed by a value. The keys - form a path to the nested map in `$map` where the value is to be added or - updated. + `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. diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index 3362b721..a886d701 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -43,7 +43,7 @@ final _get = _function("get", r"$map, $key, $keys...", (arguments) { final _set = BuiltInCallable.overloadedFunction("set", { r"$map, $key, $value": (arguments) { var map = arguments[0].assertMap("map"); - return _setImpl(map, [arguments[1]], arguments[2]); + return _modify(map, [arguments[1]], (_) => arguments[2]); }, r"$map, $args...": (arguments) { var map = arguments[0].assertMap("map"); @@ -53,14 +53,31 @@ final _set = BuiltInCallable.overloadedFunction("set", { } else if (args.length == 1) { throw SassScriptException("Expected \$args to contain a value."); } - return _setImpl(map, args.sublist(0, args.length - 1), args.last); + return _modify(map, args.sublist(0, args.length - 1), (_) => args.last); }, }); -final _merge = _function("merge", r"$map1, $map2", (arguments) { - var map1 = arguments[0].assertMap("map1"); - var map2 = arguments[1].assertMap("map2"); - return SassMap({...map1.contents, ...map2.contents}); +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.sublist(0, 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) { @@ -114,24 +131,36 @@ final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) { return SassBoolean(map.contents.containsKey(keys.last)); }); -/// Updates a map with the given [value]. +/// 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 targetted map. If any value along the path is not a map, this -/// creates and inserts a new map at that key. -SassMap _setImpl(SassMap map, List keys, Value value, [int index = 0]) { - var mutableMap = Map.of(map.contents); - var key = keys[index]; +/// leads to the targeted map. If any value along the path is not a map, and +/// [overwrite] is true, this inserts a new map at that key and overwrites the +/// current value. Otherwise, this fails and returns [map] with no changes. +SassMap _modify(SassMap map, List keys, Value modify(Value old), + [bool overwrite = true]) { + SassMap _modifyNestedMap(SassMap map, int index) { + var mutableMap = Map.of(map.contents); + var key = keys[index]; - if (index == keys.length - 1) { - mutableMap[key] = value; + if (index == keys.length - 1) { + mutableMap[key] = modify(mutableMap[key]); + return SassMap(mutableMap); + } + + var nestedMap = mutableMap[key]?.tryMap(); + if (nestedMap == null && !overwrite) { + return SassMap(mutableMap); + } + + nestedMap ??= const SassMap.empty(); + mutableMap[key] = _modifyNestedMap(nestedMap, index + 1); return SassMap(mutableMap); } - var nestedMap = mutableMap[key].tryMap() ?? const SassMap.empty(); - mutableMap[key] = _setImpl(nestedMap, keys, value, index + 1); - return SassMap(mutableMap); + return _modifyNestedMap(map, 0); } /// Merges [map1] and [map2], with values in [map2] taking precedence. From 56d979d04d6dcdb0426994cfd2ffddaa045c3aa1 Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Tue, 22 Sep 2020 16:11:48 -0700 Subject: [PATCH 6/6] Add a map.deep-remove() function (#1091) --- CHANGELOG.md | 14 ++++++++++ lib/src/functions/map.dart | 57 ++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 434f2098..af35624e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,20 @@ [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 + ### Dart API * Add a `Value.tryMap()` function which returns the `Value` as a `SassMap` if diff --git a/lib/src/functions/map.dart b/lib/src/functions/map.dart index a886d701..76a1f228 100644 --- a/lib/src/functions/map.dart +++ b/lib/src/functions/map.dart @@ -23,7 +23,15 @@ final global = UnmodifiableListView([ /// The Sass map module. final module = BuiltInModule("map", functions: [ - _get, _set, _merge, _remove, _keys, _values, _hasKey, _deepMerge // + _get, + _set, + _merge, + _remove, + _keys, + _values, + _hasKey, + _deepMerge, + _deepRemove ]); final _get = _function("get", r"$map, $key, $keys...", (arguments) { @@ -72,7 +80,7 @@ final _merge = BuiltInCallable.overloadedFunction("merge", { throw SassScriptException("Expected \$args to contain a map."); } var map2 = args.last.assertMap("map2"); - return _modify(map1, args.sublist(0, args.length - 1), (oldValue) { + return _modify(map1, args.take(args.length - 1), (oldValue) { var nestedMap = oldValue?.tryMap(); if (nestedMap == null) return map2; return SassMap({...nestedMap.contents, ...map2.contents}); @@ -86,6 +94,19 @@ final _deepMerge = _function("deep-merge", r"$map1, $map2", (arguments) { 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", { // Because the signature below has an explicit `$key` argument, it doesn't // allow zero keys to be passed. We want to allow that case, so we add an @@ -131,36 +152,42 @@ final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) { return SassBoolean(map.contents.containsKey(keys.last)); }); -/// Updates the specified value in [map] by applying the [modify()] callback to +/// 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 -/// [overwrite] is true, this inserts a new map at that key and overwrites the -/// current value. Otherwise, this fails and returns [map] with no changes. -SassMap _modify(SassMap map, List keys, Value modify(Value old), - [bool overwrite = true]) { - SassMap _modifyNestedMap(SassMap map, int index) { +/// `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 = keys[index]; + var key = keyIterator.current; - if (index == keys.length - 1) { - mutableMap[key] = modify(mutableMap[key]); + if (!keyIterator.moveNext()) { + mutableMap[key] = newValue ?? modify(mutableMap[key]); return SassMap(mutableMap); } var nestedMap = mutableMap[key]?.tryMap(); - if (nestedMap == null && !overwrite) { - return SassMap(mutableMap); + 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, index + 1); + mutableMap[key] = _modifyNestedMap(nestedMap, newValue); return SassMap(mutableMap); } - return _modifyNestedMap(map, 0); + return keyIterator.moveNext() ? _modifyNestedMap(map) : modify(map); } /// Merges [map1] and [map2], with values in [map2] taking precedence.