@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.
- package/index.ts +16 -6
- package/package.json +2 -2
- package/shared.js +20 -30
- package/src/Tree.js +62 -513
- package/src/constants.js +2 -0
- package/src/drivers/BrowserFileTree.js +9 -10
- package/src/drivers/DeepMapTree.js +3 -3
- package/src/drivers/DeepObjectTree.js +4 -5
- package/src/drivers/DeferredTree.js +2 -2
- package/src/drivers/FileTree.js +11 -33
- package/src/drivers/FunctionTree.js +1 -1
- package/src/drivers/MapTree.js +6 -6
- package/src/drivers/ObjectTree.js +4 -3
- package/src/drivers/SetTree.js +1 -1
- package/src/drivers/SiteTree.js +1 -1
- package/src/drivers/constantTree.js +1 -1
- package/src/extension.js +5 -3
- package/src/jsonKeys.js +5 -7
- package/src/operations/addNextPrevious.js +10 -9
- package/src/operations/assign.js +40 -0
- package/src/operations/cache.js +18 -12
- package/src/operations/cachedKeyFunctions.js +15 -4
- package/src/operations/clear.js +20 -0
- package/src/operations/concat.js +17 -0
- package/src/operations/deepMap.js +25 -0
- package/src/operations/deepMerge.js +11 -25
- package/src/operations/deepReverse.js +6 -7
- package/src/operations/deepTake.js +6 -7
- package/src/operations/deepText.js +4 -4
- package/src/operations/deepValuesIterator.js +8 -6
- package/src/operations/defineds.js +32 -0
- package/src/operations/delete.js +20 -0
- package/src/operations/entries.js +16 -0
- package/src/operations/extensionKeyFunctions.js +1 -1
- package/src/operations/filter.js +7 -8
- package/src/operations/first.js +18 -0
- package/src/operations/forEach.js +20 -0
- package/src/operations/from.js +77 -0
- package/src/operations/fromFn.js +26 -0
- package/src/operations/globKeys.js +8 -8
- package/src/operations/group.js +9 -7
- package/src/operations/has.js +16 -0
- package/src/operations/indent.js +4 -2
- package/src/operations/inners.js +29 -0
- package/src/operations/invokeFunctions.js +5 -4
- package/src/operations/isAsyncMutableTree.js +15 -0
- package/src/operations/isAsyncTree.js +21 -0
- package/src/operations/isTraversable.js +15 -0
- package/src/operations/isTreelike.js +33 -0
- package/src/operations/json.js +4 -3
- package/src/operations/keys.js +14 -0
- package/src/operations/length.js +15 -0
- package/src/operations/map.js +151 -95
- package/src/operations/mapExtension.js +27 -0
- package/src/operations/mapReduce.js +44 -0
- package/src/operations/mask.js +18 -16
- package/src/operations/match.js +74 -0
- package/src/operations/merge.js +22 -20
- package/src/operations/paginate.js +3 -5
- package/src/operations/parent.js +13 -0
- package/src/operations/paths.js +51 -0
- package/src/operations/plain.js +34 -0
- package/src/operations/regExpKeys.js +4 -5
- package/src/operations/remove.js +14 -0
- package/src/operations/reverse.js +4 -6
- package/src/operations/root.js +17 -0
- package/src/operations/scope.js +4 -6
- package/src/operations/setDeep.js +50 -0
- package/src/operations/shuffle.js +46 -0
- package/src/operations/sort.js +19 -12
- package/src/operations/take.js +3 -5
- package/src/operations/text.js +3 -3
- package/src/operations/toFunction.js +14 -0
- package/src/operations/traverse.js +25 -0
- package/src/operations/traverseOrThrow.js +64 -0
- package/src/operations/traversePath.js +16 -0
- package/src/operations/values.js +15 -0
- package/src/operations/withKeys.js +33 -0
- package/src/utilities/TypedArray.js +2 -0
- package/src/utilities/box.js +20 -0
- package/src/utilities/castArraylike.js +38 -0
- package/src/utilities/getParent.js +33 -0
- package/src/utilities/getRealmObjectPrototype.js +19 -0
- package/src/utilities/getTreeArgument.js +43 -0
- package/src/utilities/isPacked.js +20 -0
- package/src/utilities/isPlainObject.js +29 -0
- package/src/utilities/isPrimitive.js +13 -0
- package/src/utilities/isStringlike.js +25 -0
- package/src/utilities/isUnpackable.js +13 -0
- package/src/utilities/keysFromPath.js +34 -0
- package/src/utilities/naturalOrder.js +9 -0
- package/src/utilities/pathFromKeys.js +18 -0
- package/src/utilities/setParent.js +38 -0
- package/src/utilities/toFunction.js +41 -0
- package/src/utilities/toPlainValue.js +95 -0
- package/src/utilities/toString.js +37 -0
- package/test/drivers/ExplorableSiteTree.test.js +1 -1
- package/test/drivers/FileTree.test.js +1 -1
- package/test/drivers/calendarTree.test.js +1 -1
- package/test/jsonKeys.test.js +1 -1
- package/test/operations/assign.test.js +54 -0
- package/test/operations/cache.test.js +1 -1
- package/test/operations/cachedKeyFunctions.test.js +16 -16
- package/test/operations/clear.test.js +34 -0
- package/test/operations/deepMap.test.js +29 -0
- package/test/operations/deepMerge.test.js +2 -6
- package/test/operations/deepReverse.test.js +1 -1
- package/test/operations/defineds.test.js +25 -0
- package/test/operations/delete.test.js +20 -0
- package/test/operations/entries.test.js +18 -0
- package/test/operations/extensionKeyFunctions.test.js +10 -10
- package/test/operations/first.test.js +15 -0
- package/test/operations/fixtures/README.md +1 -0
- package/test/operations/forEach.test.js +22 -0
- package/test/operations/from.test.js +67 -0
- package/test/operations/globKeys.test.js +3 -3
- package/test/operations/has.test.js +15 -0
- package/test/operations/inners.test.js +30 -0
- package/test/operations/invokeFunctions.test.js +1 -1
- package/test/operations/isAsyncMutableTree.test.js +17 -0
- package/test/operations/isAsyncTree.test.js +26 -0
- package/test/operations/isTreelike.test.js +13 -0
- package/test/operations/keys.test.js +15 -0
- package/test/operations/length.test.js +15 -0
- package/test/operations/map.test.js +61 -42
- package/test/operations/mapExtension.test.js +0 -0
- package/test/operations/mapReduce.test.js +23 -0
- package/test/operations/mask.test.js +1 -1
- package/test/operations/match.test.js +33 -0
- package/test/operations/merge.test.js +23 -9
- package/test/operations/parent.test.js +15 -0
- package/test/operations/paths.test.js +40 -0
- package/test/operations/plain.test.js +69 -0
- package/test/operations/reverse.test.js +1 -1
- package/test/operations/scope.test.js +1 -1
- package/test/operations/setDeep.test.js +53 -0
- package/test/operations/shuffle.test.js +18 -0
- package/test/operations/sort.test.js +3 -3
- package/test/operations/toFunction.test.js +16 -0
- package/test/operations/traverse.test.js +43 -0
- package/test/operations/traversePath.test.js +16 -0
- package/test/operations/values.test.js +18 -0
- package/test/operations/withKeys.test.js +21 -0
- package/test/utilities/box.test.js +26 -0
- package/test/utilities/getRealmObjectPrototype.test.js +11 -0
- package/test/utilities/isPlainObject.test.js +13 -0
- package/test/utilities/keysFromPath.test.js +14 -0
- package/test/utilities/naturalOrder.test.js +11 -0
- package/test/utilities/pathFromKeys.test.js +12 -0
- package/test/utilities/setParent.test.js +34 -0
- package/test/utilities/toFunction.test.js +34 -0
- package/test/utilities/toPlainValue.test.js +27 -0
- package/test/utilities/toString.test.js +22 -0
- package/src/Tree.d.ts +0 -24
- package/src/utilities.d.ts +0 -21
- package/src/utilities.js +0 -443
- package/test/Tree.test.js +0 -407
- package/test/utilities.test.js +0 -141
package/src/operations/map.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { Tree } from "../internal.js";
|
|
2
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
-
import
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
+
keyNeedsSourceValue ??= true;
|
|
183
239
|
|
|
184
240
|
return {
|
|
185
241
|
deep,
|
|
186
242
|
description,
|
|
187
243
|
inverseKeyFn,
|
|
188
244
|
keyFn,
|
|
189
|
-
|
|
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
|
+
}
|
package/src/operations/mask.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Tree } from "../internal.js";
|
|
2
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
-
import
|
|
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}
|
|
14
|
-
* @param {Treelike}
|
|
15
|
-
* @returns {AsyncTree}
|
|
14
|
+
* @param {Treelike} aTreelike
|
|
15
|
+
* @param {Treelike} bTreelike
|
|
16
|
+
* @returns {Promise<AsyncTree>}
|
|
16
17
|
*/
|
|
17
|
-
export default function mask(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
28
|
+
const bValue = await bTree.get(key);
|
|
27
29
|
if (!bValue) {
|
|
28
30
|
return undefined;
|
|
29
31
|
}
|
|
30
|
-
let aValue = await
|
|
31
|
-
if (
|
|
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
|
|
42
|
-
const bValues = await Promise.all(aKeys.map((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) ||
|
|
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
|
+
}
|
package/src/operations/merge.js
CHANGED
|
@@ -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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
* @
|
|
16
|
-
* @
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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}
|
|
11
|
+
* @param {Treelike} treelike
|
|
13
12
|
* @param {number} [size=10]
|
|
14
13
|
*/
|
|
15
14
|
export default async function paginate(treelike, size = 10) {
|
|
16
|
-
|
|
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
|
+
}
|