@weborigami/async-tree 0.5.4 → 0.5.5

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 (158) hide show
  1. package/index.ts +16 -6
  2. package/package.json +2 -2
  3. package/shared.js +20 -30
  4. package/src/Tree.js +62 -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 +1 -1
  12. package/src/drivers/MapTree.js +6 -6
  13. package/src/drivers/ObjectTree.js +4 -3
  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/concat.js +17 -0
  25. package/src/operations/deepMap.js +25 -0
  26. package/src/operations/deepMerge.js +11 -25
  27. package/src/operations/deepReverse.js +6 -7
  28. package/src/operations/deepTake.js +6 -7
  29. package/src/operations/deepText.js +4 -4
  30. package/src/operations/deepValuesIterator.js +8 -6
  31. package/src/operations/defineds.js +32 -0
  32. package/src/operations/delete.js +20 -0
  33. package/src/operations/entries.js +16 -0
  34. package/src/operations/extensionKeyFunctions.js +1 -1
  35. package/src/operations/filter.js +7 -8
  36. package/src/operations/first.js +18 -0
  37. package/src/operations/forEach.js +20 -0
  38. package/src/operations/from.js +77 -0
  39. package/src/operations/fromFn.js +26 -0
  40. package/src/operations/globKeys.js +8 -8
  41. package/src/operations/group.js +9 -7
  42. package/src/operations/has.js +16 -0
  43. package/src/operations/indent.js +4 -2
  44. package/src/operations/inners.js +29 -0
  45. package/src/operations/invokeFunctions.js +5 -4
  46. package/src/operations/isAsyncMutableTree.js +15 -0
  47. package/src/operations/isAsyncTree.js +21 -0
  48. package/src/operations/isTraversable.js +15 -0
  49. package/src/operations/isTreelike.js +33 -0
  50. package/src/operations/json.js +4 -3
  51. package/src/operations/keys.js +14 -0
  52. package/src/operations/length.js +15 -0
  53. package/src/operations/map.js +151 -95
  54. package/src/operations/mapExtension.js +27 -0
  55. package/src/operations/mapReduce.js +44 -0
  56. package/src/operations/mask.js +18 -16
  57. package/src/operations/match.js +74 -0
  58. package/src/operations/merge.js +22 -20
  59. package/src/operations/paginate.js +3 -5
  60. package/src/operations/parent.js +13 -0
  61. package/src/operations/paths.js +51 -0
  62. package/src/operations/plain.js +34 -0
  63. package/src/operations/regExpKeys.js +4 -5
  64. package/src/operations/remove.js +14 -0
  65. package/src/operations/reverse.js +4 -6
  66. package/src/operations/root.js +17 -0
  67. package/src/operations/scope.js +4 -6
  68. package/src/operations/setDeep.js +50 -0
  69. package/src/operations/shuffle.js +46 -0
  70. package/src/operations/sort.js +19 -12
  71. package/src/operations/take.js +3 -5
  72. package/src/operations/text.js +3 -3
  73. package/src/operations/toFunction.js +14 -0
  74. package/src/operations/traverse.js +25 -0
  75. package/src/operations/traverseOrThrow.js +64 -0
  76. package/src/operations/traversePath.js +16 -0
  77. package/src/operations/values.js +15 -0
  78. package/src/operations/withKeys.js +33 -0
  79. package/src/utilities/TypedArray.js +2 -0
  80. package/src/utilities/box.js +20 -0
  81. package/src/utilities/castArraylike.js +38 -0
  82. package/src/utilities/getParent.js +33 -0
  83. package/src/utilities/getRealmObjectPrototype.js +19 -0
  84. package/src/utilities/getTreeArgument.js +43 -0
  85. package/src/utilities/isPacked.js +20 -0
  86. package/src/utilities/isPlainObject.js +29 -0
  87. package/src/utilities/isPrimitive.js +13 -0
  88. package/src/utilities/isStringlike.js +25 -0
  89. package/src/utilities/isUnpackable.js +13 -0
  90. package/src/utilities/keysFromPath.js +34 -0
  91. package/src/utilities/naturalOrder.js +9 -0
  92. package/src/utilities/pathFromKeys.js +18 -0
  93. package/src/utilities/setParent.js +38 -0
  94. package/src/utilities/toFunction.js +41 -0
  95. package/src/utilities/toPlainValue.js +95 -0
  96. package/src/utilities/toString.js +37 -0
  97. package/test/drivers/ExplorableSiteTree.test.js +1 -1
  98. package/test/drivers/FileTree.test.js +1 -1
  99. package/test/drivers/calendarTree.test.js +1 -1
  100. package/test/jsonKeys.test.js +1 -1
  101. package/test/operations/assign.test.js +54 -0
  102. package/test/operations/cache.test.js +1 -1
  103. package/test/operations/cachedKeyFunctions.test.js +16 -16
  104. package/test/operations/clear.test.js +34 -0
  105. package/test/operations/deepMap.test.js +29 -0
  106. package/test/operations/deepMerge.test.js +2 -6
  107. package/test/operations/deepReverse.test.js +1 -1
  108. package/test/operations/defineds.test.js +25 -0
  109. package/test/operations/delete.test.js +20 -0
  110. package/test/operations/entries.test.js +18 -0
  111. package/test/operations/extensionKeyFunctions.test.js +10 -10
  112. package/test/operations/first.test.js +15 -0
  113. package/test/operations/fixtures/README.md +1 -0
  114. package/test/operations/forEach.test.js +22 -0
  115. package/test/operations/from.test.js +67 -0
  116. package/test/operations/globKeys.test.js +3 -3
  117. package/test/operations/has.test.js +15 -0
  118. package/test/operations/inners.test.js +30 -0
  119. package/test/operations/invokeFunctions.test.js +1 -1
  120. package/test/operations/isAsyncMutableTree.test.js +17 -0
  121. package/test/operations/isAsyncTree.test.js +26 -0
  122. package/test/operations/isTreelike.test.js +13 -0
  123. package/test/operations/keys.test.js +15 -0
  124. package/test/operations/length.test.js +15 -0
  125. package/test/operations/map.test.js +61 -42
  126. package/test/operations/mapExtension.test.js +0 -0
  127. package/test/operations/mapReduce.test.js +23 -0
  128. package/test/operations/mask.test.js +1 -1
  129. package/test/operations/match.test.js +33 -0
  130. package/test/operations/merge.test.js +23 -9
  131. package/test/operations/parent.test.js +15 -0
  132. package/test/operations/paths.test.js +40 -0
  133. package/test/operations/plain.test.js +69 -0
  134. package/test/operations/reverse.test.js +1 -1
  135. package/test/operations/scope.test.js +1 -1
  136. package/test/operations/setDeep.test.js +53 -0
  137. package/test/operations/shuffle.test.js +18 -0
  138. package/test/operations/sort.test.js +3 -3
  139. package/test/operations/toFunction.test.js +16 -0
  140. package/test/operations/traverse.test.js +43 -0
  141. package/test/operations/traversePath.test.js +16 -0
  142. package/test/operations/values.test.js +18 -0
  143. package/test/operations/withKeys.test.js +21 -0
  144. package/test/utilities/box.test.js +26 -0
  145. package/test/utilities/getRealmObjectPrototype.test.js +11 -0
  146. package/test/utilities/isPlainObject.test.js +13 -0
  147. package/test/utilities/keysFromPath.test.js +14 -0
  148. package/test/utilities/naturalOrder.test.js +11 -0
  149. package/test/utilities/pathFromKeys.test.js +12 -0
  150. package/test/utilities/setParent.test.js +34 -0
  151. package/test/utilities/toFunction.test.js +34 -0
  152. package/test/utilities/toPlainValue.test.js +27 -0
  153. package/test/utilities/toString.test.js +22 -0
  154. package/src/Tree.d.ts +0 -24
  155. package/src/utilities.d.ts +0 -21
  156. package/src/utilities.js +0 -443
  157. package/test/Tree.test.js +0 -407
  158. 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
+ }
22
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
+ }
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,20 +150,40 @@ 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
 
140
189
  if (extension && (keyFn || inverseKeyFn)) {
@@ -142,6 +191,11 @@ function validateOptions(options) {
142
191
  `map: You can't specify extensions and also a key or inverseKey function`
143
192
  );
144
193
  }
194
+ if (extension && keyNeedsSourceValue === true) {
195
+ throw new TypeError(
196
+ `map: using extensions sets keyNeedsSourceValue to be false`
197
+ );
198
+ }
145
199
 
146
200
  if (extension) {
147
201
  // Use the extension mapping to generate key and inverseKey functions
@@ -152,6 +206,7 @@ function validateOptions(options) {
152
206
  );
153
207
  keyFn = keyFns.key;
154
208
  inverseKeyFn = keyFns.inverseKey;
209
+ keyNeedsSourceValue = false;
155
210
  } else {
156
211
  // If key or inverseKey weren't specified, look for sidecar functions
157
212
  inverseKeyFn ??= valueFn?.inverseKey;
@@ -177,16 +232,17 @@ function validateOptions(options) {
177
232
  );
178
233
  }
179
234
 
180
- deep ??= false;
235
+ // Set defaults for options not specified. We don't set a default value for
236
+ // `deep` because a false value is a stronger signal than undefined.
181
237
  description ??= "key/value map";
182
- needsSourceValue ??= true;
238
+ keyNeedsSourceValue ??= true;
183
239
 
184
240
  return {
185
241
  deep,
186
242
  description,
187
243
  inverseKeyFn,
188
244
  keyFn,
189
- needsSourceValue,
245
+ keyNeedsSourceValue,
190
246
  valueFn,
191
247
  };
192
248
  }
@@ -0,0 +1,27 @@
1
+ import getTreeArgument from "../utilities/getTreeArgument.js";
2
+ import isPlainObject from "../utilities/isPlainObject.js";
3
+ import map from "./map.js";
4
+
5
+ /**
6
+ * Shorthand for calling `map` with the `deep: true` option.
7
+ *
8
+ * @typedef {import("../../index.ts").TreeMapExtensionOptions} TreeMapExtensionOptions
9
+ * @typedef {import("../../index.ts").Treelike} Treelike
10
+ * @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
11
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
12
+ *
13
+ * @param {Treelike} treelike
14
+ * @param {string} extension
15
+ * @param {ValueKeyFn|TreeMapExtensionOptions} options
16
+ * @returns {Promise<AsyncTree>}
17
+ */
18
+ export default async function mapExtension(treelike, extension, options) {
19
+ const tree = await getTreeArgument(treelike, "mapExtension");
20
+ const withExtension = isPlainObject(options)
21
+ ? // Dictionary
22
+ { ...options, extension }
23
+ : // Function
24
+ { extension, value: options };
25
+
26
+ return map(tree, withExtension);
27
+ }
@@ -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("@weborigami/async-tree").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
+ }
@@ -0,0 +1,51 @@
1
+ import * as trailingSlash from "../trailingSlash.js";
2
+ import from from "./from.js";
3
+ import isAsyncTree from "./isAsyncTree.js";
4
+
5
+ /**
6
+ * Returns slash-separated paths for all values in the tree.
7
+ *
8
+ * The `base` argument is prepended to all paths.
9
+ *
10
+ * If `assumeSlashes` is true, then keys are assumed to have trailing slashes to
11
+ * indicate subtrees. The default value of this option is false.
12
+ *
13
+ * @typedef {import("../../index.ts").Treelike} Treelike
14
+ *
15
+ * @param {Treelike} treelike
16
+ * @param {{ assumeSlashes?: boolean, base?: string }} options
17
+ */
18
+ export default async function paths(treelike, options = {}) {
19
+ const tree = from(treelike);
20
+ const base = options.base ?? "";
21
+ const assumeSlashes = options.assumeSlashes ?? false;
22
+ const result = [];
23
+ for (const key of await tree.keys()) {
24
+ const separator = trailingSlash.has(base) ? "" : "/";
25
+ const valuePath = base ? `${base}${separator}${key}` : key;
26
+ let isSubtree;
27
+ let value;
28
+ if (assumeSlashes) {
29
+ // Subtree needs to have a trailing slash
30
+ isSubtree = trailingSlash.has(key);
31
+ if (isSubtree) {
32
+ // We'll need the value to recurse
33
+ value = await tree.get(key);
34
+ }
35
+ } else {
36
+ // Get value and check
37
+ value = await tree.get(key);
38
+ }
39
+ if (value) {
40
+ // If we got the value we can check if it's a subtree
41
+ isSubtree = isAsyncTree(value);
42
+ }
43
+ if (isSubtree) {
44
+ const subPaths = await paths(value, { assumeSlashes, base: valuePath });
45
+ result.push(...subPaths);
46
+ } else {
47
+ result.push(valuePath);
48
+ }
49
+ }
50
+ return result;
51
+ }