@weborigami/async-tree 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/index.ts +16 -6
  2. package/package.json +2 -2
  3. package/shared.js +20 -29
  4. package/src/Tree.js +59 -513
  5. package/src/constants.js +2 -0
  6. package/src/drivers/BrowserFileTree.js +9 -10
  7. package/src/drivers/DeepMapTree.js +3 -3
  8. package/src/drivers/DeepObjectTree.js +4 -5
  9. package/src/drivers/DeferredTree.js +2 -2
  10. package/src/drivers/FileTree.js +11 -33
  11. package/src/drivers/FunctionTree.js +3 -3
  12. package/src/drivers/MapTree.js +6 -6
  13. package/src/drivers/ObjectTree.js +6 -8
  14. package/src/drivers/SetTree.js +1 -1
  15. package/src/drivers/SiteTree.js +1 -1
  16. package/src/drivers/constantTree.js +1 -1
  17. package/src/extension.js +5 -3
  18. package/src/jsonKeys.js +5 -7
  19. package/src/operations/addNextPrevious.js +10 -9
  20. package/src/operations/assign.js +40 -0
  21. package/src/operations/cache.js +18 -12
  22. package/src/operations/cachedKeyFunctions.js +15 -4
  23. package/src/operations/clear.js +20 -0
  24. package/src/operations/deepMap.js +25 -0
  25. package/src/operations/deepMerge.js +11 -25
  26. package/src/operations/deepReverse.js +6 -7
  27. package/src/operations/deepTake.js +6 -7
  28. package/src/operations/deepText.js +4 -4
  29. package/src/operations/deepValuesIterator.js +8 -6
  30. package/src/operations/delete.js +20 -0
  31. package/src/operations/entries.js +16 -0
  32. package/src/operations/extensionKeyFunctions.js +1 -1
  33. package/src/operations/filter.js +7 -8
  34. package/src/operations/first.js +18 -0
  35. package/src/operations/forEach.js +20 -0
  36. package/src/operations/from.js +77 -0
  37. package/src/operations/globKeys.js +8 -8
  38. package/src/operations/group.js +3 -46
  39. package/src/operations/groupBy.js +51 -0
  40. package/src/operations/has.js +16 -0
  41. package/src/operations/indent.js +4 -2
  42. package/src/operations/inners.js +29 -0
  43. package/src/operations/invokeFunctions.js +5 -4
  44. package/src/operations/isAsyncMutableTree.js +15 -0
  45. package/src/operations/isAsyncTree.js +21 -0
  46. package/src/operations/isTraversable.js +15 -0
  47. package/src/operations/isTreelike.js +33 -0
  48. package/src/operations/json.js +4 -3
  49. package/src/operations/keys.js +14 -0
  50. package/src/operations/length.js +15 -0
  51. package/src/operations/map.js +156 -95
  52. package/src/operations/mapExtension.js +78 -0
  53. package/src/operations/mapReduce.js +44 -0
  54. package/src/operations/mask.js +18 -16
  55. package/src/operations/match.js +74 -0
  56. package/src/operations/merge.js +22 -20
  57. package/src/operations/paginate.js +3 -5
  58. package/src/operations/parent.js +13 -0
  59. package/src/operations/paths.js +51 -0
  60. package/src/operations/plain.js +34 -0
  61. package/src/operations/regExpKeys.js +4 -5
  62. package/src/operations/reverse.js +4 -6
  63. package/src/operations/root.js +17 -0
  64. package/src/operations/scope.js +4 -6
  65. package/src/operations/shuffle.js +46 -0
  66. package/src/operations/sort.js +19 -12
  67. package/src/operations/take.js +3 -5
  68. package/src/operations/text.js +3 -3
  69. package/src/operations/toFunction.js +14 -0
  70. package/src/operations/traverse.js +24 -0
  71. package/src/operations/traverseOrThrow.js +59 -0
  72. package/src/operations/traversePath.js +16 -0
  73. package/src/operations/values.js +15 -0
  74. package/src/operations/withKeys.js +33 -0
  75. package/src/utilities/TypedArray.js +2 -0
  76. package/src/utilities/box.js +20 -0
  77. package/src/utilities/castArraylike.js +38 -0
  78. package/src/utilities/getParent.js +33 -0
  79. package/src/utilities/getRealmObjectPrototype.js +19 -0
  80. package/src/utilities/getTreeArgument.js +43 -0
  81. package/src/utilities/isPacked.js +20 -0
  82. package/src/utilities/isPlainObject.js +29 -0
  83. package/src/utilities/isPrimitive.js +13 -0
  84. package/src/utilities/isStringlike.js +25 -0
  85. package/src/utilities/isUnpackable.js +13 -0
  86. package/src/utilities/keysFromPath.js +34 -0
  87. package/src/utilities/naturalOrder.js +9 -0
  88. package/src/utilities/pathFromKeys.js +18 -0
  89. package/src/utilities/setParent.js +38 -0
  90. package/src/utilities/toFunction.js +40 -0
  91. package/src/utilities/toPlainValue.js +95 -0
  92. package/src/utilities/toString.js +37 -0
  93. package/test/drivers/ExplorableSiteTree.test.js +1 -1
  94. package/test/drivers/FileTree.test.js +1 -1
  95. package/test/drivers/calendarTree.test.js +1 -1
  96. package/test/jsonKeys.test.js +1 -1
  97. package/test/operations/assign.test.js +54 -0
  98. package/test/operations/cache.test.js +1 -1
  99. package/test/operations/cachedKeyFunctions.test.js +16 -16
  100. package/test/operations/clear.test.js +34 -0
  101. package/test/operations/deepMerge.test.js +2 -6
  102. package/test/operations/deepReverse.test.js +1 -1
  103. package/test/operations/delete.test.js +20 -0
  104. package/test/operations/entries.test.js +18 -0
  105. package/test/operations/extensionKeyFunctions.test.js +10 -10
  106. package/test/operations/first.test.js +15 -0
  107. package/test/operations/fixtures/README.md +1 -0
  108. package/test/operations/forEach.test.js +22 -0
  109. package/test/operations/from.test.js +67 -0
  110. package/test/operations/globKeys.test.js +3 -3
  111. package/test/operations/{group.test.js → groupBy.test.js} +4 -4
  112. package/test/operations/has.test.js +15 -0
  113. package/test/operations/inners.test.js +30 -0
  114. package/test/operations/invokeFunctions.test.js +1 -1
  115. package/test/operations/isAsyncMutableTree.test.js +17 -0
  116. package/test/operations/isAsyncTree.test.js +26 -0
  117. package/test/operations/isTreelike.test.js +13 -0
  118. package/test/operations/keys.test.js +15 -0
  119. package/test/operations/length.test.js +15 -0
  120. package/test/operations/map.test.js +39 -70
  121. package/test/operations/mapExtension.test.js +53 -0
  122. package/test/operations/mapReduce.test.js +23 -0
  123. package/test/operations/mask.test.js +1 -1
  124. package/test/operations/match.test.js +33 -0
  125. package/test/operations/merge.test.js +23 -9
  126. package/test/operations/paginate.test.js +1 -1
  127. package/test/operations/parent.test.js +15 -0
  128. package/test/operations/paths.test.js +40 -0
  129. package/test/operations/plain.test.js +69 -0
  130. package/test/operations/reverse.test.js +1 -1
  131. package/test/operations/scope.test.js +1 -1
  132. package/test/operations/shuffle.test.js +18 -0
  133. package/test/operations/sort.test.js +3 -3
  134. package/test/operations/toFunction.test.js +16 -0
  135. package/test/operations/traverse.test.js +43 -0
  136. package/test/operations/traversePath.test.js +16 -0
  137. package/test/operations/values.test.js +18 -0
  138. package/test/operations/withKeys.test.js +21 -0
  139. package/test/utilities/box.test.js +26 -0
  140. package/test/utilities/getRealmObjectPrototype.test.js +11 -0
  141. package/test/utilities/isPlainObject.test.js +13 -0
  142. package/test/utilities/keysFromPath.test.js +14 -0
  143. package/test/utilities/naturalOrder.test.js +11 -0
  144. package/test/utilities/pathFromKeys.test.js +12 -0
  145. package/test/utilities/setParent.test.js +34 -0
  146. package/test/utilities/toFunction.test.js +34 -0
  147. package/test/utilities/toPlainValue.test.js +27 -0
  148. package/test/utilities/toString.test.js +22 -0
  149. package/src/Tree.d.ts +0 -24
  150. package/src/utilities.d.ts +0 -21
  151. package/src/utilities.js +0 -443
  152. package/test/Tree.test.js +0 -407
  153. package/test/utilities.test.js +0 -141
@@ -1,8 +1,11 @@
1
- import { Tree } from "../internal.js";
2
1
  import * as trailingSlash from "../trailingSlash.js";
3
- import { assertIsTreelike } from "../utilities.js";
2
+ import getTreeArgument from "../utilities/getTreeArgument.js";
3
+ import isPlainObject from "../utilities/isPlainObject.js";
4
+ import isUnpackable from "../utilities/isUnpackable.js";
5
+ import toFunction from "../utilities/toFunction.js";
4
6
  import cachedKeyFunctions from "./cachedKeyFunctions.js";
5
7
  import extensionKeyFunctions from "./extensionKeyFunctions.js";
8
+ import isAsyncTree from "./isAsyncTree.js";
6
9
  import parseExtensions from "./parseExtensions.js";
7
10
 
8
11
  /**
@@ -11,107 +14,133 @@ import parseExtensions from "./parseExtensions.js";
11
14
  * @typedef {import("../../index.ts").KeyFn} KeyFn
12
15
  * @typedef {import("../../index.ts").TreeMapOptions} MapOptions
13
16
  * @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
17
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
14
18
  *
15
19
  * @param {import("../../index.ts").Treelike} treelike
16
20
  * @param {MapOptions|ValueKeyFn} options
21
+ * @returns {Promise<AsyncTree>}
17
22
  */
18
- export default function map(treelike, options = {}) {
19
- assertIsTreelike(treelike, "map");
20
- const { deep, description, inverseKeyFn, keyFn, needsSourceValue, valueFn } =
21
- validateOptions(options);
23
+ export default async function map(treelike, options = {}) {
24
+ if (isUnpackable(options)) {
25
+ options = await options.unpack();
26
+ }
27
+ const validated = validateOptions(options);
28
+ const mapFn = createMapFn(validated);
29
+
30
+ const tree = await getTreeArgument(treelike, "map", { deep: validated.deep });
31
+ return mapFn(tree);
32
+ }
33
+
34
+ // Create a get() function for the map
35
+ function createGet(tree, options, mapFn) {
36
+ const { inverseKeyFn, deep, valueFn } = options;
37
+ return async (resultKey) => {
38
+ if (resultKey === undefined) {
39
+ throw new ReferenceError(`map: Cannot get an undefined key.`);
40
+ }
41
+
42
+ // Step 1: Map the result key to the source key
43
+ let sourceKey = await inverseKeyFn?.(resultKey, tree);
44
+
45
+ if (sourceKey === undefined) {
46
+ if (deep && trailingSlash.has(resultKey)) {
47
+ // Special case: deep tree and value is expected to be a subtree
48
+ const sourceValue = await tree.get(resultKey);
49
+ // If we did get a subtree, apply the map to it
50
+ const resultValue = isAsyncTree(sourceValue)
51
+ ? mapFn(sourceValue)
52
+ : undefined;
53
+ return resultValue;
54
+ } else {
55
+ // No inverseKeyFn, or it returned undefined; use resultKey
56
+ sourceKey = resultKey;
57
+ }
58
+ }
59
+
60
+ // Step 2: Get the source value
61
+ let sourceValue = await tree.get(sourceKey);
62
+ if (deep && sourceValue === undefined) {
63
+ // Key might be for a subtree, see if original key exists
64
+ sourceValue = await tree.get(resultKey);
65
+ }
66
+
67
+ // Step 3: Map the source value to the result value
68
+ let resultValue;
69
+ if (sourceValue === undefined) {
70
+ // No source value means no result value
71
+ resultValue = undefined;
72
+ } else if (deep && isAsyncTree(sourceValue)) {
73
+ // We weren't expecting a subtree but got one; map it
74
+ resultValue = mapFn(sourceValue);
75
+ } else if (valueFn) {
76
+ // Map a single value
77
+ resultValue = await valueFn(sourceValue, sourceKey, tree);
78
+ } else {
79
+ // Return source value as is
80
+ resultValue = sourceValue;
81
+ }
82
+
83
+ return resultValue;
84
+ };
85
+ }
86
+
87
+ // Create a keys() function for the map
88
+ function createKeys(tree, options) {
89
+ const { deep, keyFn, keyNeedsSourceValue } = options;
90
+ return async () => {
91
+ // Apply the keyFn to source keys for leaf values (not subtrees).
92
+ const sourceKeys = Array.from(await tree.keys());
93
+ const sourceValues = keyNeedsSourceValue
94
+ ? await Promise.all(sourceKeys.map((sourceKey) => tree.get(sourceKey)))
95
+ : sourceKeys.map(() => null);
96
+ const mapped = await Promise.all(
97
+ sourceKeys.map(async (sourceKey, index) =>
98
+ // Deep maps leave source keys for subtrees alone
99
+ deep && trailingSlash.has(sourceKey)
100
+ ? sourceKey
101
+ : await keyFn(sourceValues[index], sourceKey, tree)
102
+ )
103
+ );
104
+ // Filter out any cases where the keyFn returned undefined.
105
+ const resultKeys = mapped.filter((key) => key !== undefined);
106
+ return resultKeys;
107
+ };
108
+ }
22
109
 
110
+ // Create a map function for the given options
111
+ function createMapFn(options) {
112
+ const { description, keyFn, valueFn } = options;
23
113
  /**
24
- * @param {import("@weborigami/types").AsyncTree} tree
114
+ * @param {AsyncTree} tree
115
+ * @return {AsyncTree}
25
116
  */
26
- function mapFn(tree) {
117
+ return function mapFn(tree) {
27
118
  // The transformed tree is actually an extension of the original tree's
28
119
  // prototype chain. This allows the transformed tree to inherit any
29
120
  // properties/methods. For example, the `parent` of the transformed tree is
30
121
  // the original tree's parent.
31
122
  const transformed = Object.create(tree);
32
-
33
123
  transformed.description = description;
34
-
35
124
  if (keyFn || valueFn) {
36
- transformed.get = async (resultKey) => {
37
- if (resultKey === undefined) {
38
- throw new ReferenceError(`map: Cannot get an undefined key.`);
39
- }
40
-
41
- // Step 1: Map the result key to the source key
42
- let sourceKey = await inverseKeyFn?.(resultKey, tree);
43
-
44
- if (sourceKey === undefined) {
45
- if (deep && trailingSlash.has(resultKey)) {
46
- // Special case: deep tree and value is expected to be a subtree
47
- const sourceValue = await tree.get(resultKey);
48
- // If we did get a subtree, apply the map to it
49
- const resultValue = Tree.isAsyncTree(sourceValue)
50
- ? mapFn(sourceValue)
51
- : undefined;
52
- return resultValue;
53
- } else {
54
- // No inverseKeyFn, or it returned undefined; use resultKey
55
- sourceKey = resultKey;
56
- }
57
- }
58
-
59
- // Regular path: map a single value
60
-
61
- // Step 2: Get the source value
62
- let sourceValue;
63
- if (needsSourceValue) {
64
- // Normal case: get the value from the source tree
65
- sourceValue = await tree.get(sourceKey);
66
- if (deep && sourceValue === undefined) {
67
- // Key might be for a subtree, see if original key exists
68
- sourceValue = await tree.get(resultKey);
69
- }
70
- }
71
-
72
- // Step 3: Map the source value to the result value
73
- let resultValue;
74
- if (needsSourceValue && sourceValue === undefined) {
75
- // No source value means no result value
76
- resultValue = undefined;
77
- } else if (deep && Tree.isAsyncTree(sourceValue)) {
78
- // We weren't expecting a subtree but got one; map it
79
- resultValue = mapFn(sourceValue);
80
- } else if (valueFn) {
81
- // Map a single value
82
- resultValue = await valueFn(sourceValue, sourceKey, tree);
83
- } else {
84
- // Return source value as is
85
- resultValue = sourceValue;
86
- }
87
-
88
- return resultValue;
89
- };
125
+ transformed.get = createGet(tree, options, mapFn);
90
126
  }
91
-
92
127
  if (keyFn) {
93
- transformed.keys = async () => {
94
- // Apply the keyFn to source keys for leaf values (not subtrees).
95
- const sourceKeys = Array.from(await tree.keys());
96
- const mapped = await Promise.all(
97
- sourceKeys.map(async (sourceKey) =>
98
- // Deep maps leave source keys for subtrees alone
99
- deep && trailingSlash.has(sourceKey)
100
- ? sourceKey
101
- : await keyFn(sourceKey, tree)
102
- )
103
- );
104
- // Filter out any cases where the keyFn returned undefined.
105
- const resultKeys = mapped.filter((key) => key !== undefined);
106
- return resultKeys;
107
- };
128
+ transformed.keys = createKeys(tree, options);
108
129
  }
109
-
110
130
  return transformed;
111
- }
131
+ };
132
+ }
112
133
 
113
- const tree = Tree.from(treelike, { deep });
114
- return mapFn(tree);
134
+ // Return the indicated option, throwing if it's specified but not defined;
135
+ // that's probably an accident.
136
+ function validateOption(options, key) {
137
+ const value = options[key];
138
+ if (key in options && value === undefined) {
139
+ throw new TypeError(
140
+ `map: The ${key} option is given but its value is undefined.`
141
+ );
142
+ }
143
+ return value;
115
144
  }
116
145
 
117
146
  // Extract and validate options
@@ -121,27 +150,57 @@ function validateOptions(options) {
121
150
  let extension;
122
151
  let inverseKeyFn;
123
152
  let keyFn;
124
- let needsSourceValue;
153
+ let keyNeedsSourceValue;
125
154
  let valueFn;
126
155
 
127
156
  if (typeof options === "function") {
128
157
  // Take the single function argument as the valueFn
129
158
  valueFn = options;
159
+ } else if (isPlainObject(options)) {
160
+ // Extract options from the dictionary
161
+ description = options.description; // fine if it's undefined
162
+
163
+ // Validate individual options
164
+ deep = validateOption(options, "deep");
165
+ extension = validateOption(options, "extension");
166
+ inverseKeyFn = validateOption(options, "inverseKey");
167
+ keyFn = validateOption(options, "key");
168
+ keyNeedsSourceValue = validateOption(options, "keyNeedsSourceValue");
169
+ valueFn = validateOption(options, "value");
170
+
171
+ // Cast function options to functions
172
+ inverseKeyFn &&= toFunction(inverseKeyFn);
173
+ keyFn &&= toFunction(keyFn);
174
+ valueFn &&= toFunction(valueFn);
175
+ } else if (options === undefined) {
176
+ /** @type {any} */
177
+ const error = new TypeError(`map: The second parameter was undefined.`);
178
+ error.position = 1;
179
+ throw error;
130
180
  } else {
131
- deep = options.deep;
132
- description = options.description;
133
- extension = options.extension;
134
- inverseKeyFn = options.inverseKey;
135
- keyFn = options.key;
136
- needsSourceValue = options.needsSourceValue;
137
- valueFn = options.value;
181
+ /** @type {any} */
182
+ const error = new TypeError(
183
+ `map: You must specify a value function or options dictionary as the second parameter.`
184
+ );
185
+ error.position = 1;
186
+ throw error;
138
187
  }
139
188
 
189
+ if (extension && !options._noExtensionWarning) {
190
+ console.warn(
191
+ `map: The 'extension' option for Tree.map() is deprecated and will be removed in a future release. Use Tree.mapExtension() instead.`
192
+ );
193
+ }
140
194
  if (extension && (keyFn || inverseKeyFn)) {
141
195
  throw new TypeError(
142
196
  `map: You can't specify extensions and also a key or inverseKey function`
143
197
  );
144
198
  }
199
+ if (extension && keyNeedsSourceValue === true) {
200
+ throw new TypeError(
201
+ `map: using extensions sets keyNeedsSourceValue to be false`
202
+ );
203
+ }
145
204
 
146
205
  if (extension) {
147
206
  // Use the extension mapping to generate key and inverseKey functions
@@ -152,6 +211,7 @@ function validateOptions(options) {
152
211
  );
153
212
  keyFn = keyFns.key;
154
213
  inverseKeyFn = keyFns.inverseKey;
214
+ keyNeedsSourceValue = false;
155
215
  } else {
156
216
  // If key or inverseKey weren't specified, look for sidecar functions
157
217
  inverseKeyFn ??= valueFn?.inverseKey;
@@ -177,16 +237,17 @@ function validateOptions(options) {
177
237
  );
178
238
  }
179
239
 
180
- deep ??= false;
240
+ // Set defaults for options not specified. We don't set a default value for
241
+ // `deep` because a false value is a stronger signal than undefined.
181
242
  description ??= "key/value map";
182
- needsSourceValue ??= true;
243
+ keyNeedsSourceValue ??= true;
183
244
 
184
245
  return {
185
246
  deep,
186
247
  description,
187
248
  inverseKeyFn,
188
249
  keyFn,
189
- needsSourceValue,
250
+ keyNeedsSourceValue,
190
251
  valueFn,
191
252
  };
192
253
  }
@@ -0,0 +1,78 @@
1
+ import isPlainObject from "../utilities/isPlainObject.js";
2
+ import map from "./map.js";
3
+
4
+ /**
5
+ * @typedef {import("../../index.ts").TreeMapExtensionOptions} TreeMapExtensionOptions
6
+ * @typedef {import("../../index.ts").Treelike} Treelike
7
+ * @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ */
10
+
11
+ /**
12
+ * @overload
13
+ * @param {Treelike} treelike
14
+ * @param {string} extension
15
+ */
16
+
17
+ /**
18
+ * @overload
19
+ * @param {Treelike} treelike
20
+ * @param {TreeMapExtensionOptions} options
21
+ */
22
+
23
+ /**
24
+ * @overload
25
+ * @param {Treelike} treelike
26
+ * @param {string} extension
27
+ * @param {ValueKeyFn} fn
28
+ */
29
+
30
+ /**
31
+ * @overload
32
+ * @param {Treelike} treelike
33
+ * @param {string} extension
34
+ * @param {TreeMapExtensionOptions} options
35
+ */
36
+
37
+ /**
38
+ * Shorthand for calling `map` with the `deep: true` option.
39
+ *
40
+ * @param {Treelike} treelike
41
+ * @param {string|TreeMapExtensionOptions} arg2
42
+ * @param {ValueKeyFn|TreeMapExtensionOptions} [arg3]
43
+ * @returns {Promise<AsyncTree>}
44
+ */
45
+ export default async function mapExtension(treelike, arg2, arg3) {
46
+ /** @type {TreeMapExtensionOptions} */
47
+ // @ts-ignore
48
+ let options = { _noExtensionWarning: true };
49
+ if (arg3 === undefined) {
50
+ if (typeof arg2 === "string") {
51
+ options.extension = arg2;
52
+ } else if (isPlainObject(arg2)) {
53
+ Object.assign(options, arg2);
54
+ } else {
55
+ throw new TypeError(
56
+ "mapExtension: Expected a string or options object for the second argument."
57
+ );
58
+ }
59
+ } else {
60
+ if (typeof arg2 !== "string") {
61
+ throw new TypeError(
62
+ "mapExtension: Expected a string for the second argument."
63
+ );
64
+ }
65
+ options.extension = arg2;
66
+ if (typeof arg3 === "function") {
67
+ options.value = arg3;
68
+ } else if (isPlainObject(arg3)) {
69
+ Object.assign(options, arg3);
70
+ } else {
71
+ throw new TypeError(
72
+ "mapExtension: Expected a function or options object for the third argument."
73
+ );
74
+ }
75
+ }
76
+
77
+ return map(treelike, options);
78
+ }
@@ -0,0 +1,44 @@
1
+ import from from "./from.js";
2
+ import isAsyncTree from "./isAsyncTree.js";
3
+
4
+ /**
5
+ * Map and reduce a tree.
6
+ *
7
+ * This is done in as parallel fashion as possible. Each of the tree's values
8
+ * will be requested in an async call, then those results will be awaited
9
+ * collectively. If a mapFn is provided, it will be invoked to convert each
10
+ * value to a mapped value; otherwise, values will be used as is. When the
11
+ * values have been obtained, all the values and keys will be passed to the
12
+ * reduceFn, which should consolidate those into a single result.
13
+ *
14
+ * @typedef {import("../../index.ts").Treelike} Treelike
15
+ * @typedef {import("../../index.ts").ReduceFn} ReduceFn
16
+ * @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
17
+ *
18
+ * @param {Treelike} treelike
19
+ * @param {ValueKeyFn|null} mapFn
20
+ * @param {ReduceFn} reduceFn
21
+ */
22
+ export default async function mapReduce(treelike, mapFn, reduceFn) {
23
+ const tree = from(treelike);
24
+
25
+ // We're going to fire off all the get requests in parallel, as quickly as
26
+ // the keys come in. We call the tree's `get` method for each key, but
27
+ // *don't* wait for it yet.
28
+ const keys = Array.from(await tree.keys());
29
+ const promises = keys.map(async (key) => {
30
+ const value = await tree.get(key);
31
+ return isAsyncTree(value)
32
+ ? mapReduce(value, mapFn, reduceFn) // subtree; recurse
33
+ : mapFn
34
+ ? mapFn(value, key, tree)
35
+ : value;
36
+ });
37
+
38
+ // Wait for all the promises to resolve. Because the promises were captured
39
+ // in the same order as the keys, the values will also be in the same order.
40
+ const values = await Promise.all(promises);
41
+
42
+ // Reduce the values to a single result.
43
+ return reduceFn(values, keys, tree);
44
+ }
@@ -1,6 +1,7 @@
1
- import { Tree } from "../internal.js";
2
1
  import * as trailingSlash from "../trailingSlash.js";
3
- import { assertIsTreelike } from "../utilities.js";
2
+ import getTreeArgument from "../utilities/getTreeArgument.js";
3
+ import isAsyncTree from "./isAsyncTree.js";
4
+ import isTreelike from "./isTreelike.js";
4
5
 
5
6
  /**
6
7
  * Given trees `a` and `b`, return a masked version of `a` where only the keys
@@ -10,25 +11,26 @@ import { assertIsTreelike } from "../utilities.js";
10
11
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
11
12
  * @typedef {import("../../index.ts").Treelike} Treelike
12
13
  *
13
- * @param {Treelike} a
14
- * @param {Treelike} b
15
- * @returns {AsyncTree}
14
+ * @param {Treelike} aTreelike
15
+ * @param {Treelike} bTreelike
16
+ * @returns {Promise<AsyncTree>}
16
17
  */
17
- export default function mask(a, b) {
18
- assertIsTreelike(a, "filter", 0);
19
- assertIsTreelike(b, "filter", 1);
20
- a = Tree.from(a);
21
- b = Tree.from(b, { deep: true });
18
+ export default async function mask(aTreelike, bTreelike) {
19
+ const aTree = await getTreeArgument(aTreelike, "filter", { position: 0 });
20
+ const bTree = await getTreeArgument(bTreelike, "filter", {
21
+ deep: true,
22
+ position: 1,
23
+ });
22
24
 
23
25
  return {
24
26
  async get(key) {
25
27
  // The key must exist in b and return a truthy value
26
- const bValue = await b.get(key);
28
+ const bValue = await bTree.get(key);
27
29
  if (!bValue) {
28
30
  return undefined;
29
31
  }
30
- let aValue = await a.get(key);
31
- if (Tree.isTreelike(aValue)) {
32
+ let aValue = await aTree.get(key);
33
+ if (isTreelike(aValue)) {
32
34
  // Filter the subtree
33
35
  return mask(aValue, bValue);
34
36
  } else {
@@ -38,13 +40,13 @@ export default function mask(a, b) {
38
40
 
39
41
  async keys() {
40
42
  // Use a's keys as the basis
41
- const aKeys = [...(await a.keys())];
42
- const bValues = await Promise.all(aKeys.map((key) => b.get(key)));
43
+ const aKeys = [...(await aTree.keys())];
44
+ const bValues = await Promise.all(aKeys.map((key) => bTree.get(key)));
43
45
  // An async tree value in b implies that the a key should have a slash
44
46
  const aKeySlashes = aKeys.map((key, index) =>
45
47
  trailingSlash.toggle(
46
48
  key,
47
- trailingSlash.has(key) || Tree.isAsyncTree(bValues[index])
49
+ trailingSlash.has(key) || isAsyncTree(bValues[index])
48
50
  )
49
51
  );
50
52
  // Remove keys that don't have values in b
@@ -0,0 +1,74 @@
1
+ import isAsyncTree from "./isAsyncTree.js";
2
+
3
+ /**
4
+ * Return a tree with the indicated keys (if provided).
5
+ *
6
+ * The pattern can a string with a simplified pattern syntax that tries to match
7
+ * against the entire key and uses brackets to identify named wildcard values.
8
+ * E.g. `[name].html` will match `Alice.html` with wildcard values { name:
9
+ * "Alice" }.
10
+ *
11
+ * The pattern can also be a JavaScript regular expression.
12
+ *
13
+ * If a key is requested, match against the given pattern and, if matches,
14
+ * invokes the given function with an object containing the matched values.
15
+ *
16
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
17
+ * @typedef {import("../../index.ts").Treelike} Treelike
18
+ * @typedef {import("../../index.ts").Invocable} Invocable
19
+ *
20
+ * @param {string|RegExp} pattern
21
+ * @param {Invocable} resultFn
22
+ * @param {Treelike} [keys]
23
+ */
24
+ export default function match(pattern, resultFn, keys = []) {
25
+ let regex;
26
+ if (typeof pattern === "string") {
27
+ // Convert the simple pattern format into a regular expression.
28
+ const regexText = pattern.replace(
29
+ /\[(?<variable>.+)\]/g,
30
+ (match, p1, offset, string, groups) => `(?<${groups.variable}>.+)`
31
+ );
32
+ regex = new RegExp(`^${regexText}$`);
33
+ } else if (pattern instanceof RegExp) {
34
+ regex = pattern;
35
+ } else {
36
+ throw new Error(`match(): Unsupported pattern`);
37
+ }
38
+
39
+ const result = {
40
+ async get(key) {
41
+ const keyMatch = regex.exec(key);
42
+ if (!keyMatch) {
43
+ return undefined;
44
+ }
45
+
46
+ if (
47
+ typeof resultFn !== "function" &&
48
+ !(isAsyncTree(resultFn) && "parent" in resultFn)
49
+ ) {
50
+ // Simple return value; return as is
51
+ return resultFn;
52
+ }
53
+
54
+ // Copy the `groups` property to a real object
55
+ const matches = { ...keyMatch.groups };
56
+
57
+ // Invoke the result function with the extended scope.
58
+ let value;
59
+ if (typeof resultFn === "function") {
60
+ value = await resultFn(matches);
61
+ } else {
62
+ value = Object.create(resultFn);
63
+ }
64
+
65
+ return value;
66
+ },
67
+
68
+ async keys() {
69
+ return typeof keys === "function" ? await keys() : keys;
70
+ },
71
+ };
72
+
73
+ return result;
74
+ }
@@ -1,24 +1,35 @@
1
- import { Tree } from "../internal.js";
2
- import * as symbols from "../symbols.js";
3
1
  import * as trailingSlash from "../trailingSlash.js";
2
+ import isPlainObject from "../utilities/isPlainObject.js";
3
+ import from from "./from.js";
4
+ import isAsyncTree from "./isAsyncTree.js";
4
5
 
5
6
  /**
6
7
  * Return a tree that performs a shallow merge of the given trees.
7
8
  *
8
- * Given a set of trees, the `get` method looks at each tree in turn. The first
9
- * tree is asked for the value with the key. If an tree returns a defined value
10
- * (i.e., not undefined), that value is returned. If the first tree returns
11
- * undefined, the second tree will be asked, and so on. If none of the trees
9
+ * This is similar to an object spread in JavaScript extended to async trees.
10
+ * Given a set of trees, the `get` method looks at each tree in turn, starting
11
+ * from the *last* tree and working backwards to the first. If a tree returns a
12
+ * defined value for the key, that value is returned. If none of the trees
12
13
  * return a defined value, the `get` method returns undefined.
13
14
  *
14
15
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
15
- * @param {import("../../index.ts").Treelike[]} sources
16
- * @returns {AsyncTree & { description?: string, trees?: AsyncTree[]}}
16
+ * @typedef {import("../../index.ts").PlainObject} PlainObject
17
+ * @typedef {import("../../index.ts").Treelike} Treelike
18
+ *
19
+ * @param {Treelike[]} sources
20
+ * @returns {(AsyncTree & { description?: string, trees?: AsyncTree[]}) | PlainObject}
17
21
  */
18
22
  export default function merge(...sources) {
19
- const trees = sources
20
- .filter((source) => source)
21
- .map((treelike) => Tree.from(treelike));
23
+ const filtered = sources.filter((source) => source);
24
+
25
+ // If all arguments are plain objects, return a plain object.
26
+ if (
27
+ filtered.every((source) => !isAsyncTree(source) && isPlainObject(source))
28
+ ) {
29
+ return filtered.reduce((acc, obj) => ({ ...acc, ...obj }), {});
30
+ }
31
+
32
+ const trees = filtered.map((treelike) => from(treelike));
22
33
 
23
34
  if (trees.length === 0) {
24
35
  throw new TypeError("merge: all trees are null or undefined");
@@ -36,15 +47,6 @@ export default function merge(...sources) {
36
47
  const tree = trees[index];
37
48
  const value = await tree.get(key);
38
49
  if (value !== undefined) {
39
- // Merged tree acts as parent instead of the source tree.
40
- if (Tree.isAsyncTree(value) && value.parent === tree) {
41
- value.parent = this;
42
- } else if (
43
- typeof value === "object" &&
44
- value?.[symbols.parent] === tree
45
- ) {
46
- value[symbols.parent] = this;
47
- }
48
50
  return value;
49
51
  }
50
52
  }
@@ -1,6 +1,5 @@
1
- import { Tree } from "../internal.js";
2
1
  import * as trailingSlash from "../trailingSlash.js";
3
- import { assertIsTreelike } from "../utilities.js";
2
+ import getTreeArgument from "../utilities/getTreeArgument.js";
4
3
 
5
4
  /**
6
5
  * Return a new grouping of the treelike's values into chunks of the specified
@@ -9,12 +8,11 @@ import { assertIsTreelike } from "../utilities.js";
9
8
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
9
  * @typedef {import("../../index.ts").Treelike} Treelike
11
10
  *
12
- * @param {Treelike} [treelike]
11
+ * @param {Treelike} treelike
13
12
  * @param {number} [size=10]
14
13
  */
15
14
  export default async function paginate(treelike, size = 10) {
16
- assertIsTreelike(treelike, "paginate");
17
- const tree = Tree.from(treelike);
15
+ const tree = await getTreeArgument(treelike, "paginate");
18
16
 
19
17
  const keys = Array.from(await tree.keys());
20
18
  const pageCount = Math.ceil(keys.length / size);
@@ -0,0 +1,13 @@
1
+ import getTreeArgument from "../utilities/getTreeArgument.js";
2
+
3
+ /**
4
+ * Returns the parent of the current tree.
5
+ *
6
+ * @typedef {import("../../index.ts").Treelike} Treelike
7
+ *
8
+ * @param {Treelike} treelike
9
+ */
10
+ export default async function parent(treelike) {
11
+ const tree = await getTreeArgument(treelike, "parent");
12
+ return tree.parent;
13
+ }