@weborigami/async-tree 0.0.65 → 0.0.66-beta.2
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/main.js +2 -0
- package/package.json +4 -4
- package/src/BrowserFileTree.js +28 -6
- package/src/DeepMapTree.js +2 -2
- package/src/DeepObjectTree.js +6 -9
- package/src/FileTree.js +14 -10
- package/src/MapTree.js +27 -4
- package/src/ObjectTree.js +53 -6
- package/src/OpenSiteTree.js +41 -0
- package/src/SetTree.js +0 -6
- package/src/SiteTree.js +24 -85
- package/src/Tree.d.ts +1 -2
- package/src/Tree.js +40 -52
- package/src/jsonKeys.js +4 -37
- package/src/operations/cache.js +0 -4
- package/src/operations/deepMerge.js +7 -10
- package/src/operations/merge.js +7 -10
- package/src/trailingSlash.js +54 -0
- package/src/transforms/cachedKeyFunctions.js +72 -34
- package/src/transforms/keyFunctionsForExtensions.js +24 -10
- package/src/transforms/mapFn.js +11 -17
- package/src/transforms/regExpKeys.js +17 -12
- package/src/utilities.js +34 -6
- package/test/BrowserFileTree.test.js +28 -5
- package/test/DeepMapTree.test.js +17 -0
- package/test/DeepObjectTree.test.js +17 -7
- package/test/FileTree.test.js +14 -7
- package/test/MapTree.test.js +21 -0
- package/test/ObjectTree.test.js +16 -12
- package/test/OpenSiteTree.test.js +113 -0
- package/test/SiteTree.test.js +14 -49
- package/test/Tree.test.js +19 -39
- package/test/browser/assert.js +9 -0
- package/test/browser/index.html +4 -4
- package/test/calendarTree.test.js +1 -1
- package/test/fixtures/markdown/subfolder/README.md +1 -0
- package/test/jsonKeys.test.js +0 -9
- package/test/operations/cache.test.js +1 -1
- package/test/operations/merge.test.js +20 -1
- package/test/trailingSlash.test.js +36 -0
- package/test/transforms/cachedKeyFunctions.test.js +90 -0
- package/test/transforms/keyFunctionsForExtensions.test.js +7 -3
- package/test/transforms/mapFn.test.js +29 -20
- package/test/utilities.test.js +6 -2
- package/test/transforms/cachedKeyMaps.test.js +0 -41
package/src/Tree.js
CHANGED
|
@@ -3,6 +3,7 @@ import FunctionTree from "./FunctionTree.js";
|
|
|
3
3
|
import MapTree from "./MapTree.js";
|
|
4
4
|
import SetTree from "./SetTree.js";
|
|
5
5
|
import { DeepObjectTree, ObjectTree } from "./internal.js";
|
|
6
|
+
import * as trailingSlash from "./trailingSlash.js";
|
|
6
7
|
import mapTransform from "./transforms/mapFn.js";
|
|
7
8
|
import * as utilities from "./utilities.js";
|
|
8
9
|
import {
|
|
@@ -105,35 +106,38 @@ export async function forEach(tree, callbackFn) {
|
|
|
105
106
|
*
|
|
106
107
|
* If the object is a plain object, it will be converted to an ObjectTree. The
|
|
107
108
|
* optional `deep` option can be set to `true` to convert a plain object to a
|
|
108
|
-
* DeepObjectTree.
|
|
109
|
+
* DeepObjectTree. The optional `parent` parameter will be used as the default
|
|
110
|
+
* parent of the new tree.
|
|
109
111
|
*
|
|
110
112
|
* @param {Treelike | Object} object
|
|
111
|
-
* @param {{ deep?: true }} [options]
|
|
113
|
+
* @param {{ deep?: true, parent?: AsyncTree|null }} [options]
|
|
112
114
|
* @returns {AsyncTree}
|
|
113
115
|
*/
|
|
114
116
|
export function from(object, options = {}) {
|
|
117
|
+
let tree;
|
|
115
118
|
if (isAsyncTree(object)) {
|
|
116
119
|
// Argument already supports the tree interface.
|
|
117
120
|
// @ts-ignore
|
|
118
121
|
return object;
|
|
119
122
|
} else if (typeof object === "function") {
|
|
120
|
-
|
|
123
|
+
tree = new FunctionTree(object);
|
|
121
124
|
} else if (object instanceof Map) {
|
|
122
|
-
|
|
125
|
+
tree = new MapTree(object);
|
|
123
126
|
} else if (object instanceof Set) {
|
|
124
|
-
|
|
127
|
+
tree = new SetTree(object);
|
|
125
128
|
} else if (isPlainObject(object) || object instanceof Array) {
|
|
126
|
-
|
|
129
|
+
tree = options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
|
|
127
130
|
} else if (isUnpackable(object)) {
|
|
128
131
|
async function AsyncFunction() {} // Sample async function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
tree =
|
|
133
|
+
object.unpack instanceof AsyncFunction.constructor
|
|
134
|
+
? // Async unpack: return a deferred tree.
|
|
135
|
+
new DeferredTree(object.unpack)
|
|
136
|
+
: // Synchronous unpack: cast the result of unpack() to a tree.
|
|
137
|
+
from(object.unpack());
|
|
134
138
|
} else if (object && typeof object === "object") {
|
|
135
139
|
// An instance of some class.
|
|
136
|
-
|
|
140
|
+
tree = new ObjectTree(object);
|
|
137
141
|
} else if (
|
|
138
142
|
typeof object === "string" ||
|
|
139
143
|
typeof object === "number" ||
|
|
@@ -141,10 +145,15 @@ export function from(object, options = {}) {
|
|
|
141
145
|
) {
|
|
142
146
|
// A primitive value; box it into an object and construct a tree.
|
|
143
147
|
const boxed = utilities.box(object);
|
|
144
|
-
|
|
148
|
+
tree = new ObjectTree(boxed);
|
|
149
|
+
} else {
|
|
150
|
+
throw new TypeError("Couldn't convert argument to an async tree");
|
|
145
151
|
}
|
|
146
152
|
|
|
147
|
-
|
|
153
|
+
if (!tree.parent && options.parent) {
|
|
154
|
+
tree.parent = options.parent;
|
|
155
|
+
}
|
|
156
|
+
return tree;
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
/**
|
|
@@ -167,7 +176,8 @@ export async function has(tree, key) {
|
|
|
167
176
|
*/
|
|
168
177
|
export function isAsyncTree(obj) {
|
|
169
178
|
return (
|
|
170
|
-
obj &&
|
|
179
|
+
obj !== null &&
|
|
180
|
+
typeof obj === "object" &&
|
|
171
181
|
typeof obj.get === "function" &&
|
|
172
182
|
typeof obj.keys === "function" &&
|
|
173
183
|
// JavaScript Map look like trees but can't be extended the same way, so we
|
|
@@ -188,22 +198,6 @@ export function isAsyncMutableTree(obj) {
|
|
|
188
198
|
);
|
|
189
199
|
}
|
|
190
200
|
|
|
191
|
-
/**
|
|
192
|
-
* Return true if the indicated key produces or is expected to produce an
|
|
193
|
-
* async tree.
|
|
194
|
-
*
|
|
195
|
-
* This defers to the tree's own isKeyForSubtree method. If not found, this
|
|
196
|
-
* gets the value of that key and returns true if the value is an async
|
|
197
|
-
* tree.
|
|
198
|
-
*/
|
|
199
|
-
export async function isKeyForSubtree(tree, key) {
|
|
200
|
-
if (tree.isKeyForSubtree) {
|
|
201
|
-
return tree.isKeyForSubtree(key);
|
|
202
|
-
}
|
|
203
|
-
const value = await tree.get(key);
|
|
204
|
-
return isAsyncTree(value);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
201
|
/**
|
|
208
202
|
* Return true if the object can be traversed via the `traverse()` method. The
|
|
209
203
|
* object must be either treelike or a packed object with an `unpack()` method.
|
|
@@ -307,7 +301,8 @@ export async function paths(treelike, base = "") {
|
|
|
307
301
|
const tree = from(treelike);
|
|
308
302
|
const result = [];
|
|
309
303
|
for (const key of await tree.keys()) {
|
|
310
|
-
const
|
|
304
|
+
const separator = trailingSlash.has(base) ? "" : "/";
|
|
305
|
+
const valuePath = base ? `${base}${separator}${key}` : key;
|
|
311
306
|
const value = await tree.get(key);
|
|
312
307
|
if (await isAsyncTree(value)) {
|
|
313
308
|
const subPaths = await paths(value, valuePath);
|
|
@@ -336,7 +331,10 @@ export async function plain(treelike) {
|
|
|
336
331
|
}
|
|
337
332
|
const object = {};
|
|
338
333
|
for (let i = 0; i < keys.length; i++) {
|
|
339
|
-
|
|
334
|
+
// Normalize slashes in keys.
|
|
335
|
+
const key = trailingSlash.remove(keys[i]);
|
|
336
|
+
const value = values[i];
|
|
337
|
+
object[key] = value;
|
|
340
338
|
}
|
|
341
339
|
return castArrayLike(object);
|
|
342
340
|
});
|
|
@@ -428,15 +426,6 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
428
426
|
value = await value.unpack();
|
|
429
427
|
}
|
|
430
428
|
|
|
431
|
-
if (!isTreelike(value)) {
|
|
432
|
-
// Value isn't treelike, so can't be traversed except for a special case:
|
|
433
|
-
// if there's only one key left and it's an empty string, return the
|
|
434
|
-
// non-treelike value.
|
|
435
|
-
if (remainingKeys.length === 1 && remainingKeys[0] === "") {
|
|
436
|
-
return value;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
429
|
if (value instanceof Function) {
|
|
441
430
|
// Value is a function: call it with the remaining keys.
|
|
442
431
|
const fn = value;
|
|
@@ -445,21 +434,20 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
445
434
|
const args = remainingKeys.splice(0, fnKeyCount);
|
|
446
435
|
key = null;
|
|
447
436
|
value = await fn.call(target, ...args);
|
|
448
|
-
} else {
|
|
449
|
-
const originalValue = value;
|
|
450
|
-
|
|
437
|
+
} else if (isTraversable(value) || typeof value === "object") {
|
|
451
438
|
// Value is some other treelike object: cast it to a tree.
|
|
452
439
|
const tree = from(value);
|
|
453
440
|
// Get the next key.
|
|
454
441
|
key = remainingKeys.shift();
|
|
455
442
|
// Get the value for the key.
|
|
456
443
|
value = await tree.get(key);
|
|
457
|
-
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
444
|
+
} else {
|
|
445
|
+
// Value can't be traversed
|
|
446
|
+
throw new TraverseError("Tried to traverse a value that's not treelike", {
|
|
447
|
+
tree: treelike,
|
|
448
|
+
keys,
|
|
449
|
+
position,
|
|
450
|
+
});
|
|
463
451
|
}
|
|
464
452
|
|
|
465
453
|
position++;
|
|
@@ -469,7 +457,7 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
469
457
|
}
|
|
470
458
|
|
|
471
459
|
/**
|
|
472
|
-
* Given a slash-separated path like "foo/bar", traverse the keys "foo" and
|
|
460
|
+
* Given a slash-separated path like "foo/bar", traverse the keys "foo/" and
|
|
473
461
|
* "bar" and return the resulting value.
|
|
474
462
|
*
|
|
475
463
|
* @param {Treelike} tree
|
package/src/jsonKeys.js
CHANGED
|
@@ -9,48 +9,15 @@ import { Tree } from "./internal.js";
|
|
|
9
9
|
* a trailing slash like "about/" for a subtree of that node.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Parse the JSON in a .keys.json file.
|
|
14
|
-
*
|
|
15
|
-
* This returns a flat dictionary of flags which are true for subtrees and
|
|
16
|
-
* false otherwise.
|
|
17
|
-
*
|
|
18
|
-
* Example: the JSON `["index.html","about/"]` parses as:
|
|
19
|
-
*
|
|
20
|
-
* {
|
|
21
|
-
* "index.html": false,
|
|
22
|
-
* about: true,
|
|
23
|
-
* }
|
|
24
|
-
*/
|
|
25
|
-
export function parse(json) {
|
|
26
|
-
const descriptors = JSON.parse(json);
|
|
27
|
-
const result = {};
|
|
28
|
-
for (const descriptor of descriptors) {
|
|
29
|
-
if (descriptor.endsWith("/")) {
|
|
30
|
-
result[descriptor.slice(0, -1)] = true;
|
|
31
|
-
} else {
|
|
32
|
-
result[descriptor] = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return result;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
12
|
/**
|
|
39
13
|
* Given a tree node, return a JSON string that can be written to a .keys.json
|
|
40
14
|
* file.
|
|
41
15
|
*/
|
|
42
16
|
export async function stringify(treelike) {
|
|
43
17
|
const tree = Tree.from(treelike);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const isKeyForSubtree = await Tree.isKeyForSubtree(tree, key);
|
|
51
|
-
const keyDescriptor = isKeyForSubtree ? `${key}/` : key;
|
|
52
|
-
keyDescriptors.push(keyDescriptor);
|
|
53
|
-
}
|
|
54
|
-
const json = JSON.stringify(keyDescriptors);
|
|
18
|
+
let keys = Array.from(await tree.keys());
|
|
19
|
+
// Skip the key `.keys.json` if present.
|
|
20
|
+
keys = keys.filter((key) => key !== ".keys.json");
|
|
21
|
+
const json = JSON.stringify(keys);
|
|
55
22
|
return json;
|
|
56
23
|
}
|
package/src/operations/cache.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return a tree that performs a deep merge of the given trees.
|
|
@@ -42,21 +43,17 @@ export default function deepMerge(...sources) {
|
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
|
|
45
|
-
async isKeyForSubtree(key) {
|
|
46
|
-
// Check trees for the indicated key in reverse order.
|
|
47
|
-
for (let index = trees.length - 1; index >= 0; index--) {
|
|
48
|
-
if (await Tree.isKeyForSubtree(trees[index], key)) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return false;
|
|
53
|
-
},
|
|
54
|
-
|
|
55
46
|
async keys() {
|
|
56
47
|
const keys = new Set();
|
|
57
48
|
// Collect keys in the order the trees were provided.
|
|
58
49
|
for (const tree of trees) {
|
|
59
50
|
for (const key of await tree.keys()) {
|
|
51
|
+
// Remove the alternate form of the key (if it exists)
|
|
52
|
+
const alternateKey = trailingSlash.toggle(key);
|
|
53
|
+
if (alternateKey !== key) {
|
|
54
|
+
keys.delete(alternateKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
60
57
|
keys.add(key);
|
|
61
58
|
}
|
|
62
59
|
}
|
package/src/operations/merge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
2
|
import * as symbols from "../symbols.js";
|
|
3
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Return a tree that performs a shallow merge of the given trees.
|
|
@@ -40,21 +41,17 @@ export default function merge(...sources) {
|
|
|
40
41
|
return undefined;
|
|
41
42
|
},
|
|
42
43
|
|
|
43
|
-
async isKeyForSubtree(key) {
|
|
44
|
-
// Check trees for the indicated key in reverse order.
|
|
45
|
-
for (let index = trees.length - 1; index >= 0; index--) {
|
|
46
|
-
if (await Tree.isKeyForSubtree(trees[index], key)) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
},
|
|
52
|
-
|
|
53
44
|
async keys() {
|
|
54
45
|
const keys = new Set();
|
|
55
46
|
// Collect keys in the order the trees were provided.
|
|
56
47
|
for (const tree of trees) {
|
|
57
48
|
for (const key of await tree.keys()) {
|
|
49
|
+
// Remove the alternate form of the key (if it exists)
|
|
50
|
+
const alternateKey = trailingSlash.toggle(key);
|
|
51
|
+
if (alternateKey !== key) {
|
|
52
|
+
keys.delete(alternateKey);
|
|
53
|
+
}
|
|
54
|
+
|
|
58
55
|
keys.add(key);
|
|
59
56
|
}
|
|
60
57
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add a trailing slash to a string key if the value is truthy. If the key
|
|
3
|
+
* is not a string, it will be returned as is.
|
|
4
|
+
*
|
|
5
|
+
* @param {any} key
|
|
6
|
+
*/
|
|
7
|
+
export function add(key) {
|
|
8
|
+
if (key == null) {
|
|
9
|
+
throw new ReferenceError("trailingSlash: key was undefined");
|
|
10
|
+
}
|
|
11
|
+
return typeof key === "string" && !key.endsWith("/") ? `${key}/` : key;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return true if the indicated key is a string with a trailing slash,
|
|
16
|
+
* false otherwise.
|
|
17
|
+
*
|
|
18
|
+
* @param {any} key
|
|
19
|
+
*/
|
|
20
|
+
export function has(key) {
|
|
21
|
+
if (key == null) {
|
|
22
|
+
throw new ReferenceError("trailingSlash: key was undefined");
|
|
23
|
+
}
|
|
24
|
+
return typeof key === "string" && key.endsWith("/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Remove a trailing slash from a string key.
|
|
29
|
+
*
|
|
30
|
+
* @param {any} key
|
|
31
|
+
*/
|
|
32
|
+
export function remove(key) {
|
|
33
|
+
if (key == null) {
|
|
34
|
+
throw new ReferenceError("trailingSlash: key was undefined");
|
|
35
|
+
}
|
|
36
|
+
return typeof key === "string" ? key.replace(/\/$/, "") : key;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* If the key has a trailing slash, remove it; otherwise add it.
|
|
41
|
+
*
|
|
42
|
+
* @param {any} key
|
|
43
|
+
* @param {boolean} [force]
|
|
44
|
+
*/
|
|
45
|
+
export function toggle(key, force = undefined) {
|
|
46
|
+
if (key == null) {
|
|
47
|
+
throw new ReferenceError("trailingSlash: key was undefined");
|
|
48
|
+
}
|
|
49
|
+
if (typeof key !== "string") {
|
|
50
|
+
return key;
|
|
51
|
+
}
|
|
52
|
+
const addSlash = force ?? !has(key);
|
|
53
|
+
return addSlash ? add(key) : remove(key);
|
|
54
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
2
|
|
|
3
3
|
const treeToCaches = new WeakMap();
|
|
4
4
|
|
|
@@ -6,46 +6,50 @@ const treeToCaches = new WeakMap();
|
|
|
6
6
|
* Given a key function, return a new key function and inverse key function that
|
|
7
7
|
* cache the results of the original.
|
|
8
8
|
*
|
|
9
|
+
* If `skipSubtrees` is true, the inverse key function will skip any source keys
|
|
10
|
+
* that are keys for subtrees, returning the source key unmodified.
|
|
11
|
+
*
|
|
9
12
|
* @typedef {import("../../index.ts").KeyFn} KeyFn
|
|
10
13
|
*
|
|
11
14
|
* @param {KeyFn} keyFn
|
|
12
|
-
* @param {boolean}
|
|
15
|
+
* @param {boolean?} skipSubtrees
|
|
13
16
|
* @returns {{ key: KeyFn, inverseKey: KeyFn }}
|
|
14
17
|
*/
|
|
15
|
-
export default function
|
|
18
|
+
export default function cachedKeyFunctions(keyFn, skipSubtrees = false) {
|
|
16
19
|
return {
|
|
17
20
|
async inverseKey(resultKey, tree) {
|
|
18
|
-
const
|
|
21
|
+
const { resultKeyToSourceKey, sourceKeyToResultKey } =
|
|
22
|
+
getKeyMapsForTree(tree);
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return caches.resultKeyToSourceKey.get(resultKey);
|
|
24
|
+
const cachedSourceKey = searchKeyMap(resultKeyToSourceKey, resultKey);
|
|
25
|
+
if (cachedSourceKey !== undefined) {
|
|
26
|
+
return cachedSourceKey;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
// Iterate through the tree's keys, calculating source keys as we go,
|
|
27
30
|
// until we find a match. Cache all the intermediate results and the
|
|
28
31
|
// final match. This is O(n), but we stop as soon as we find a match,
|
|
29
32
|
// and subsequent calls will benefit from the intermediate results.
|
|
33
|
+
const resultKeyWithoutSlash = trailingSlash.remove(resultKey);
|
|
30
34
|
for (const sourceKey of await tree.keys()) {
|
|
31
35
|
// Skip any source keys we already know about.
|
|
32
|
-
if (
|
|
36
|
+
if (sourceKeyToResultKey.has(sourceKey)) {
|
|
33
37
|
continue;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
caches.sourceKeyToResultKey.set(sourceKey, computedResultKey);
|
|
44
|
-
caches.resultKeyToSourceKey.set(computedResultKey, sourceKey);
|
|
40
|
+
const computedResultKey = await computeAndCacheResultKey(
|
|
41
|
+
tree,
|
|
42
|
+
keyFn,
|
|
43
|
+
skipSubtrees,
|
|
44
|
+
sourceKey
|
|
45
|
+
);
|
|
45
46
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (
|
|
48
|
+
computedResultKey &&
|
|
49
|
+
trailingSlash.remove(computedResultKey) === resultKeyWithoutSlash
|
|
50
|
+
) {
|
|
51
|
+
// Match found, match trailing slash and return
|
|
52
|
+
return trailingSlash.toggle(sourceKey, trailingSlash.has(resultKey));
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
@@ -53,27 +57,42 @@ export default function createCachedKeysTransform(keyFn, deep = false) {
|
|
|
53
57
|
},
|
|
54
58
|
|
|
55
59
|
async key(sourceKey, tree) {
|
|
56
|
-
const
|
|
60
|
+
const { sourceKeyToResultKey } = getKeyMapsForTree(tree);
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (keyMaps.sourceKeyToResultKey.has(sourceKey)) {
|
|
62
|
-
return keyMaps.sourceKeyToResultKey.get(sourceKey);
|
|
62
|
+
const cachedResultKey = searchKeyMap(sourceKeyToResultKey, sourceKey);
|
|
63
|
+
if (cachedResultKey !== undefined) {
|
|
64
|
+
return cachedResultKey;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const resultKey = await
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const resultKey = await computeAndCacheResultKey(
|
|
68
|
+
tree,
|
|
69
|
+
keyFn,
|
|
70
|
+
skipSubtrees,
|
|
71
|
+
sourceKey
|
|
72
|
+
);
|
|
71
73
|
return resultKey;
|
|
72
74
|
},
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function
|
|
78
|
+
async function computeAndCacheResultKey(tree, keyFn, skipSubtrees, sourceKey) {
|
|
79
|
+
const { resultKeyToSourceKey, sourceKeyToResultKey } =
|
|
80
|
+
getKeyMapsForTree(tree);
|
|
81
|
+
|
|
82
|
+
const resultKey =
|
|
83
|
+
skipSubtrees && trailingSlash.has(sourceKey)
|
|
84
|
+
? sourceKey
|
|
85
|
+
: await keyFn(sourceKey, tree);
|
|
86
|
+
|
|
87
|
+
sourceKeyToResultKey.set(sourceKey, resultKey);
|
|
88
|
+
resultKeyToSourceKey.set(resultKey, sourceKey);
|
|
89
|
+
|
|
90
|
+
return resultKey;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Maintain key->inverseKey and inverseKey->key mappings for each tree. These
|
|
94
|
+
// store subtree keys in either direction with a trailing slash.
|
|
95
|
+
function getKeyMapsForTree(tree) {
|
|
77
96
|
let keyMaps = treeToCaches.get(tree);
|
|
78
97
|
if (!keyMaps) {
|
|
79
98
|
keyMaps = {
|
|
@@ -84,3 +103,22 @@ function getCachesForTree(tree) {
|
|
|
84
103
|
}
|
|
85
104
|
return keyMaps;
|
|
86
105
|
}
|
|
106
|
+
|
|
107
|
+
// Search the given key map for the key. Ignore trailing slashes in the search,
|
|
108
|
+
// but preserve them in the result.
|
|
109
|
+
function searchKeyMap(keyMap, key) {
|
|
110
|
+
// Check key as is
|
|
111
|
+
let match;
|
|
112
|
+
if (keyMap.has(key)) {
|
|
113
|
+
match = keyMap.get(key);
|
|
114
|
+
} else if (!trailingSlash.has(key)) {
|
|
115
|
+
// Check key without trailing slash
|
|
116
|
+
const withSlash = trailingSlash.add(key);
|
|
117
|
+
if (keyMap.has(withSlash)) {
|
|
118
|
+
match = keyMap.get(withSlash);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return match
|
|
122
|
+
? trailingSlash.toggle(match, trailingSlash.has(key))
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Given a source resultExtension and a result resultExtension, return a pair of key
|
|
3
5
|
* functions that map between them.
|
|
@@ -19,13 +21,23 @@ export default function keyFunctionsForExtensions({
|
|
|
19
21
|
|
|
20
22
|
return {
|
|
21
23
|
async inverseKey(resultKey, tree) {
|
|
22
|
-
|
|
24
|
+
// Remove trailing slash so that mapFn won't inadvertently unpack files.
|
|
25
|
+
const baseKey = trailingSlash.remove(resultKey);
|
|
26
|
+
const basename = matchExtension(baseKey, resultExtension);
|
|
23
27
|
return basename ? `${basename}${dotPrefix(sourceExtension)}` : undefined;
|
|
24
28
|
},
|
|
25
29
|
|
|
26
30
|
async key(sourceKey, tree) {
|
|
27
|
-
const
|
|
28
|
-
|
|
31
|
+
const hasSlash = trailingSlash.has(sourceKey);
|
|
32
|
+
const baseKey = trailingSlash.remove(sourceKey);
|
|
33
|
+
const basename = matchExtension(baseKey, sourceExtension);
|
|
34
|
+
return basename
|
|
35
|
+
? // Preserve trailing slash
|
|
36
|
+
trailingSlash.toggle(
|
|
37
|
+
`${basename}${dotPrefix(resultExtension)}`,
|
|
38
|
+
hasSlash
|
|
39
|
+
)
|
|
40
|
+
: undefined;
|
|
29
41
|
},
|
|
30
42
|
};
|
|
31
43
|
}
|
|
@@ -35,15 +47,17 @@ function dotPrefix(resultExtension) {
|
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
/**
|
|
38
|
-
* See if the key ends with the given resultExtension. If it does, return the
|
|
39
|
-
* name without the resultExtension; if it doesn't return null.
|
|
50
|
+
* See if the key ends with the given resultExtension. If it does, return the
|
|
51
|
+
* base name without the resultExtension; if it doesn't return null.
|
|
40
52
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
53
|
+
* A trailing slash in the key is ignored for purposes of comparison to comply
|
|
54
|
+
* with the way Origami can unpack files. Example: the keys "data.json" and
|
|
55
|
+
* "data.json/" are treated equally.
|
|
43
56
|
*
|
|
44
|
-
* This uses a different, more general interpretation of "resultExtension" to
|
|
45
|
-
* suffix, rather than Node's interpretation `path.extname`. In
|
|
46
|
-
* will match an "resultExtension" like ".foo.bar" that
|
|
57
|
+
* This uses a different, more general interpretation of "resultExtension" to
|
|
58
|
+
* mean any suffix, rather than Node's interpretation `path.extname`. In
|
|
59
|
+
* particular, this will match an "resultExtension" like ".foo.bar" that
|
|
60
|
+
* contains more than one dot.
|
|
47
61
|
*/
|
|
48
62
|
function matchExtension(key, resultExtension) {
|
|
49
63
|
if (resultExtension) {
|
package/src/transforms/mapFn.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return a transform function that maps the keys and/or values of a tree.
|
|
@@ -65,13 +66,9 @@ export default function createMapTransform(options = {}) {
|
|
|
65
66
|
if (keyFn || valueFn) {
|
|
66
67
|
transformed.get = async (resultKey) => {
|
|
67
68
|
// Step 1: Map the result key to the source key.
|
|
68
|
-
const
|
|
69
|
-
const sourceKey =
|
|
70
|
-
!isSubtree && inverseKeyFn
|
|
71
|
-
? await inverseKeyFn(resultKey, tree)
|
|
72
|
-
: resultKey;
|
|
69
|
+
const sourceKey = (await inverseKeyFn?.(resultKey, tree)) ?? resultKey;
|
|
73
70
|
|
|
74
|
-
if (sourceKey
|
|
71
|
+
if (sourceKey === undefined) {
|
|
75
72
|
// No source key means no value.
|
|
76
73
|
return undefined;
|
|
77
74
|
}
|
|
@@ -81,8 +78,8 @@ export default function createMapTransform(options = {}) {
|
|
|
81
78
|
if (needsSourceValue) {
|
|
82
79
|
// Normal case: get the value from the source tree.
|
|
83
80
|
sourceValue = await tree.get(sourceKey);
|
|
84
|
-
} else if (deep &&
|
|
85
|
-
// Only get the source value if it's a subtree.
|
|
81
|
+
} else if (deep && trailingSlash.has(sourceKey)) {
|
|
82
|
+
// Only get the source value if it's expected to be a subtree.
|
|
86
83
|
sourceValue = tree;
|
|
87
84
|
}
|
|
88
85
|
|
|
@@ -111,15 +108,12 @@ export default function createMapTransform(options = {}) {
|
|
|
111
108
|
// Apply the keyFn to source keys for leaf values (not subtrees).
|
|
112
109
|
const sourceKeys = Array.from(await tree.keys());
|
|
113
110
|
const mapped = await Promise.all(
|
|
114
|
-
sourceKeys.map(async (sourceKey) =>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
return resultKey;
|
|
122
|
-
})
|
|
111
|
+
sourceKeys.map(async (sourceKey) =>
|
|
112
|
+
// Deep maps leave source keys for subtrees alone
|
|
113
|
+
deep && trailingSlash.has(sourceKey)
|
|
114
|
+
? sourceKey
|
|
115
|
+
: await keyFn(sourceKey, tree)
|
|
116
|
+
)
|
|
123
117
|
);
|
|
124
118
|
// Filter out any cases where the keyFn returned undefined.
|
|
125
119
|
const resultKeys = mapped.filter((key) => key !== undefined);
|