/
map.dart
223 lines (198 loc) · 7.1 KB
/
map.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright 2019 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:collection';
import 'package:collection/collection.dart';
import '../callable.dart';
import '../exception.dart';
import '../module/built_in.dart';
import '../value.dart';
/// The global definitions of Sass map functions.
final global = UnmodifiableListView([
_get.withName("map-get"),
_merge.withName("map-merge"),
_remove.withName("map-remove"),
_keys.withName("map-keys"),
_values.withName("map-values"),
_hasKey.withName("map-has-key")
]);
/// The Sass map module.
final module = BuiltInModule("map", functions: [
_get,
_set,
_merge,
_remove,
_keys,
_values,
_hasKey,
_deepMerge,
_deepRemove
]);
final _get = _function("get", r"$map, $key, $keys...", (arguments) {
var map = arguments[0].assertMap("map");
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 _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 _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 != null && nestedMap.contents.containsKey(keys.last)) {
return SassMap(Map.of(nestedMap.contents)..remove(keys.last));
}
return value;
}, addNesting: false);
});
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
// explicit overload for it.
r"$map": (arguments) => arguments[0].assertMap("map"),
// The first argument has special handling so that the $key parameter can be
// passed by name.
r"$map, $key, $keys...": (arguments) {
var map = arguments[0].assertMap("map");
var keys = [arguments[1], ...arguments[2].asList];
var mutableMap = Map.of(map.contents);
for (var key in keys) {
mutableMap.remove(key);
}
return SassMap(mutableMap);
}
});
final _keys = _function(
"keys",
r"$map",
(arguments) => SassList(
arguments[0].assertMap("map").contents.keys, ListSeparator.comma));
final _values = _function(
"values",
r"$map",
(arguments) => SassList(
arguments[0].assertMap("map").contents.values, ListSeparator.comma));
final _hasKey = _function("has-key", r"$map, $key, $keys...", (arguments) {
var map = arguments[0].assertMap("map");
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 value, which is passed to [modify].
///
/// If any value along the path (other than the last one) is not a map and
/// [addNesting] is `true`, this creates nested maps to match [keys] and passes
/// [sassNull] to [modify]. 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<Value> keys, Value modify(Value old),
{bool addNesting = true}) {
var keyIterator = keys.iterator;
SassMap _modifyNestedMap(SassMap map) {
var mutableMap = Map.of(map.contents);
var key = keyIterator.current;
if (!keyIterator.moveNext()) {
mutableMap[key] = modify(mutableMap[key] ?? sassNull);
return SassMap(mutableMap);
}
var nestedMap = mutableMap[key]?.tryMap();
if (nestedMap == null && !addNesting) return SassMap(mutableMap);
mutableMap[key] = _modifyNestedMap(nestedMap ?? const SassMap.empty());
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 (map1.contents.isEmpty) return map2;
if (map2.contents.isEmpty) return map1;
var result = Map.of(map1.contents);
map2.contents.forEach((key, value) {
var resultMap = result[key]?.tryMap();
if (resultMap == null) {
result[key] = value;
} else {
var valueMap = value.tryMap();
if (valueMap != null) {
var merged = _deepMergeImpl(resultMap, valueMap);
if (identical(merged, resultMap)) return;
result[key] = merged;
} else {
result[key] = value;
}
}
});
return SassMap(result);
}
/// Like [new BuiltInCallable.function], but always sets the URL to `sass:map`.
BuiltInCallable _function(
String name, String arguments, Value callback(List<Value> arguments)) =>
BuiltInCallable.function(name, arguments, callback, url: "sass:map");