@weborigami/async-tree 0.0.66-beta.1 → 0.0.66
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/DeferredTree.js +4 -2
- package/src/ExplorableSiteTree.js +52 -0
- package/src/FileTree.js +14 -10
- package/src/MapTree.js +27 -4
- package/src/ObjectTree.js +53 -6
- package/src/SetTree.js +0 -6
- package/src/SiteTree.js +32 -85
- package/src/Tree.d.ts +0 -1
- package/src/Tree.js +24 -41
- 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/ExplorableSiteTree.test.js +113 -0
- package/test/FileTree.test.js +14 -7
- package/test/MapTree.test.js +21 -0
- package/test/ObjectTree.test.js +16 -12
- 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 {
|
|
@@ -109,10 +110,11 @@ export async function forEach(tree, callbackFn) {
|
|
|
109
110
|
* parent of the new tree.
|
|
110
111
|
*
|
|
111
112
|
* @param {Treelike | Object} object
|
|
112
|
-
* @param {{ deep?:
|
|
113
|
+
* @param {{ deep?: boolean, parent?: AsyncTree|null }} [options]
|
|
113
114
|
* @returns {AsyncTree}
|
|
114
115
|
*/
|
|
115
116
|
export function from(object, options = {}) {
|
|
117
|
+
const deep = options.deep ?? false;
|
|
116
118
|
let tree;
|
|
117
119
|
if (isAsyncTree(object)) {
|
|
118
120
|
// Argument already supports the tree interface.
|
|
@@ -125,13 +127,13 @@ export function from(object, options = {}) {
|
|
|
125
127
|
} else if (object instanceof Set) {
|
|
126
128
|
tree = new SetTree(object);
|
|
127
129
|
} else if (isPlainObject(object) || object instanceof Array) {
|
|
128
|
-
tree =
|
|
130
|
+
tree = deep ? new DeepObjectTree(object) : new ObjectTree(object);
|
|
129
131
|
} else if (isUnpackable(object)) {
|
|
130
132
|
async function AsyncFunction() {} // Sample async function
|
|
131
133
|
tree =
|
|
132
134
|
object.unpack instanceof AsyncFunction.constructor
|
|
133
135
|
? // Async unpack: return a deferred tree.
|
|
134
|
-
new DeferredTree(object.unpack)
|
|
136
|
+
new DeferredTree(object.unpack, { deep })
|
|
135
137
|
: // Synchronous unpack: cast the result of unpack() to a tree.
|
|
136
138
|
from(object.unpack());
|
|
137
139
|
} else if (object && typeof object === "object") {
|
|
@@ -175,7 +177,8 @@ export async function has(tree, key) {
|
|
|
175
177
|
*/
|
|
176
178
|
export function isAsyncTree(obj) {
|
|
177
179
|
return (
|
|
178
|
-
obj &&
|
|
180
|
+
obj !== null &&
|
|
181
|
+
typeof obj === "object" &&
|
|
179
182
|
typeof obj.get === "function" &&
|
|
180
183
|
typeof obj.keys === "function" &&
|
|
181
184
|
// JavaScript Map look like trees but can't be extended the same way, so we
|
|
@@ -196,22 +199,6 @@ export function isAsyncMutableTree(obj) {
|
|
|
196
199
|
);
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
/**
|
|
200
|
-
* Return true if the indicated key produces or is expected to produce an
|
|
201
|
-
* async tree.
|
|
202
|
-
*
|
|
203
|
-
* This defers to the tree's own isKeyForSubtree method. If not found, this
|
|
204
|
-
* gets the value of that key and returns true if the value is an async
|
|
205
|
-
* tree.
|
|
206
|
-
*/
|
|
207
|
-
export async function isKeyForSubtree(tree, key) {
|
|
208
|
-
if (tree.isKeyForSubtree) {
|
|
209
|
-
return tree.isKeyForSubtree(key);
|
|
210
|
-
}
|
|
211
|
-
const value = await tree.get(key);
|
|
212
|
-
return isAsyncTree(value);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
202
|
/**
|
|
216
203
|
* Return true if the object can be traversed via the `traverse()` method. The
|
|
217
204
|
* object must be either treelike or a packed object with an `unpack()` method.
|
|
@@ -315,7 +302,8 @@ export async function paths(treelike, base = "") {
|
|
|
315
302
|
const tree = from(treelike);
|
|
316
303
|
const result = [];
|
|
317
304
|
for (const key of await tree.keys()) {
|
|
318
|
-
const
|
|
305
|
+
const separator = trailingSlash.has(base) ? "" : "/";
|
|
306
|
+
const valuePath = base ? `${base}${separator}${key}` : key;
|
|
319
307
|
const value = await tree.get(key);
|
|
320
308
|
if (await isAsyncTree(value)) {
|
|
321
309
|
const subPaths = await paths(value, valuePath);
|
|
@@ -333,6 +321,8 @@ export async function paths(treelike, base = "") {
|
|
|
333
321
|
* The result's keys will be the tree's keys cast to strings. Any tree value
|
|
334
322
|
* that is itself a tree will be similarly converted to a plain object.
|
|
335
323
|
*
|
|
324
|
+
* Any trailing slashes in keys will be removed.
|
|
325
|
+
*
|
|
336
326
|
* @param {Treelike} treelike
|
|
337
327
|
* @returns {Promise<PlainObject|Array>}
|
|
338
328
|
*/
|
|
@@ -344,7 +334,10 @@ export async function plain(treelike) {
|
|
|
344
334
|
}
|
|
345
335
|
const object = {};
|
|
346
336
|
for (let i = 0; i < keys.length; i++) {
|
|
347
|
-
|
|
337
|
+
// Normalize slashes in keys.
|
|
338
|
+
const key = trailingSlash.remove(keys[i]);
|
|
339
|
+
const value = values[i];
|
|
340
|
+
object[key] = value;
|
|
348
341
|
}
|
|
349
342
|
return castArrayLike(object);
|
|
350
343
|
});
|
|
@@ -436,15 +429,6 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
436
429
|
value = await value.unpack();
|
|
437
430
|
}
|
|
438
431
|
|
|
439
|
-
if (!isTreelike(value)) {
|
|
440
|
-
// Value isn't treelike, so can't be traversed except for a special case:
|
|
441
|
-
// if there's only one key left and it's an empty string, return the
|
|
442
|
-
// non-treelike value.
|
|
443
|
-
if (remainingKeys.length === 1 && remainingKeys[0] === "") {
|
|
444
|
-
return value;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
432
|
if (value instanceof Function) {
|
|
449
433
|
// Value is a function: call it with the remaining keys.
|
|
450
434
|
const fn = value;
|
|
@@ -453,21 +437,20 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
453
437
|
const args = remainingKeys.splice(0, fnKeyCount);
|
|
454
438
|
key = null;
|
|
455
439
|
value = await fn.call(target, ...args);
|
|
456
|
-
} else {
|
|
457
|
-
const originalValue = value;
|
|
458
|
-
|
|
440
|
+
} else if (isTraversable(value) || typeof value === "object") {
|
|
459
441
|
// Value is some other treelike object: cast it to a tree.
|
|
460
442
|
const tree = from(value);
|
|
461
443
|
// Get the next key.
|
|
462
444
|
key = remainingKeys.shift();
|
|
463
445
|
// Get the value for the key.
|
|
464
446
|
value = await tree.get(key);
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
447
|
+
} else {
|
|
448
|
+
// Value can't be traversed
|
|
449
|
+
throw new TraverseError("Tried to traverse a value that's not treelike", {
|
|
450
|
+
tree: treelike,
|
|
451
|
+
keys,
|
|
452
|
+
position,
|
|
453
|
+
});
|
|
471
454
|
}
|
|
472
455
|
|
|
473
456
|
position++;
|
|
@@ -477,7 +460,7 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
477
460
|
}
|
|
478
461
|
|
|
479
462
|
/**
|
|
480
|
-
* Given a slash-separated path like "foo/bar", traverse the keys "foo" and
|
|
463
|
+
* Given a slash-separated path like "foo/bar", traverse the keys "foo/" and
|
|
481
464
|
* "bar" and return the resulting value.
|
|
482
465
|
*
|
|
483
466
|
* @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);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A tree whose keys are strings interpreted as regular expressions.
|
|
@@ -57,25 +58,29 @@ export default async function regExpKeys(treelike) {
|
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
// Construct regular expression.
|
|
61
|
-
let text = key;
|
|
62
|
-
if (!text.startsWith("^")) {
|
|
63
|
-
text = "^" + text;
|
|
64
|
-
}
|
|
65
|
-
if (!text.endsWith("$")) {
|
|
66
|
-
text = text + "$";
|
|
67
|
-
}
|
|
68
|
-
const regExp = new RegExp(text);
|
|
69
|
-
|
|
70
61
|
// Get value.
|
|
71
62
|
let value = await tree.get(key);
|
|
72
|
-
|
|
63
|
+
|
|
64
|
+
let regExp;
|
|
65
|
+
if (trailingSlash.has(key) || Tree.isAsyncTree(value)) {
|
|
66
|
+
const baseKey = trailingSlash.remove(key);
|
|
67
|
+
regExp = new RegExp("^" + baseKey + "/?$");
|
|
68
|
+
// Subtree
|
|
73
69
|
value = regExpKeys(value);
|
|
74
70
|
if (!value.parent) {
|
|
75
71
|
value.parent = result;
|
|
76
72
|
}
|
|
73
|
+
} else {
|
|
74
|
+
// Construct regular expression.
|
|
75
|
+
let text = key;
|
|
76
|
+
if (!text.startsWith("^")) {
|
|
77
|
+
text = "^" + text;
|
|
78
|
+
}
|
|
79
|
+
if (!text.endsWith("$")) {
|
|
80
|
+
text = text + "$";
|
|
81
|
+
}
|
|
82
|
+
regExp = new RegExp(text);
|
|
77
83
|
}
|
|
78
|
-
|
|
79
84
|
map.set(regExp, value);
|
|
80
85
|
}
|
|
81
86
|
|