@weborigami/async-tree 0.0.58 → 0.0.60
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 +3 -0
- package/package.json +4 -4
- package/src/DeferredTree.js +2 -27
- package/src/FileTree.js +9 -2
- package/src/operations/cache.js +1 -1
- package/src/operations/concat.js +20 -0
- package/src/operations/deepMerge.js +15 -2
- package/src/operations/deepTakeFn.js +1 -1
- package/src/operations/deepValues.js +12 -3
- package/src/operations/deepValuesIterator.js +31 -0
- package/src/operations/merge.js +12 -21
- package/src/operations/scope.js +65 -0
- package/src/transforms/regExpKeys.js +3 -1
- package/src/utilities.d.ts +2 -1
- package/src/utilities.js +87 -2
- package/test/DeepObjectTree.test.js +2 -0
- package/test/FileTree.test.js +8 -3
- package/test/SiteTree.test.js +8 -5
- package/test/operations/concat.test.js +34 -0
- package/test/operations/deepMerge.test.js +4 -0
- package/test/operations/deepTakeFn.test.js +2 -1
- package/test/operations/deepValuesIterator.test.js +23 -0
- package/test/operations/merge.test.js +5 -0
- package/test/operations/scope.test.js +25 -0
- package/test/transforms/deepReverse.test.js +1 -1
- package/test/transforms/reverse.test.js +1 -1
- package/test/utilities.test.js +27 -2
package/main.js
CHANGED
|
@@ -11,14 +11,17 @@ export { default as SiteTree } from "./src/SiteTree.js";
|
|
|
11
11
|
export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
|
|
12
12
|
export * as keysJson from "./src/keysJson.js";
|
|
13
13
|
export { default as cache } from "./src/operations/cache.js";
|
|
14
|
+
export { default as concat } from "./src/operations/concat.js";
|
|
14
15
|
export { default as deepMerge } from "./src/operations/deepMerge.js";
|
|
15
16
|
export { default as deepTake } from "./src/operations/deepTake.js";
|
|
16
17
|
export { default as deepTakeFn } from "./src/operations/deepTakeFn.js";
|
|
17
18
|
export { default as deepValues } from "./src/operations/deepValues.js";
|
|
19
|
+
export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
|
|
18
20
|
export { default as group } from "./src/operations/group.js";
|
|
19
21
|
export { default as groupFn } from "./src/operations/groupFn.js";
|
|
20
22
|
export { default as map } from "./src/operations/map.js";
|
|
21
23
|
export { default as merge } from "./src/operations/merge.js";
|
|
24
|
+
export { default as scope } from "./src/operations/scope.js";
|
|
22
25
|
export { default as sort } from "./src/operations/sort.js";
|
|
23
26
|
export { default as take } from "./src/operations/take.js";
|
|
24
27
|
export * as symbols from "./src/symbols.js";
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.60",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
7
7
|
"browser": "./browser.js",
|
|
8
8
|
"types": "./index.ts",
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/node": "20.
|
|
11
|
-
"typescript": "5.
|
|
10
|
+
"@types/node": "20.14.9",
|
|
11
|
+
"typescript": "5.5.3"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.0.
|
|
14
|
+
"@weborigami/types": "0.0.60"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/src/DeferredTree.js
CHANGED
|
@@ -20,7 +20,6 @@ export default class DeferredTree {
|
|
|
20
20
|
this.treePromise = null;
|
|
21
21
|
this._tree = null;
|
|
22
22
|
this._parentUntilLoaded = null;
|
|
23
|
-
this._scopeUntilLoaded = null;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
async get(key) {
|
|
@@ -54,25 +53,6 @@ export default class DeferredTree {
|
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// HACK: The concept of scope is defined in Origami, not at the AsyncTree
|
|
58
|
-
// level. If a DeferredTree is used to wrap an OrigamiTree, the inner
|
|
59
|
-
// OrigamiTree will have a `scope` but not a `parent`. If someone asks the
|
|
60
|
-
// outer deferrred tree for a scope, they'd otherwise get `undefined`, which
|
|
61
|
-
// is incorrect. As a workaround, we introduce a `scope` getter here that
|
|
62
|
-
// defers to the inner tree, but we need to find a way to avoid having to
|
|
63
|
-
// introduce the concept of scope here.
|
|
64
|
-
get scope() {
|
|
65
|
-
return /** @type {any} */ (this._tree)?.scope;
|
|
66
|
-
}
|
|
67
|
-
set scope(scope) {
|
|
68
|
-
// As with `parent`, we can defer setting of scope.
|
|
69
|
-
if (this._tree && !(/** @type {any} */ (this._tree).scope)) {
|
|
70
|
-
/** @type {any} */ (this._tree).scope = scope;
|
|
71
|
-
} else {
|
|
72
|
-
this._scopeUntilLoaded = scope;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
56
|
async tree() {
|
|
77
57
|
if (this._tree) {
|
|
78
58
|
return this._tree;
|
|
@@ -82,18 +62,13 @@ export default class DeferredTree {
|
|
|
82
62
|
this.treePromise ??= this.loadResult().then((treelike) => {
|
|
83
63
|
this._tree = Tree.from(treelike);
|
|
84
64
|
if (this._parentUntilLoaded) {
|
|
85
|
-
// Now that the tree has been loaded, we can set its parent
|
|
65
|
+
// Now that the tree has been loaded, we can set its parent if it hasn't
|
|
66
|
+
// already been set.
|
|
86
67
|
if (!this._tree.parent) {
|
|
87
68
|
this._tree.parent = this._parentUntilLoaded;
|
|
88
69
|
}
|
|
89
70
|
this._parentUntilLoaded = null;
|
|
90
71
|
}
|
|
91
|
-
if (this._scopeUntilLoaded) {
|
|
92
|
-
if (!(/** @type {any} */ (this._tree).scope)) {
|
|
93
|
-
/** @type {any} */ (this._tree).scope = this._scopeUntilLoaded;
|
|
94
|
-
}
|
|
95
|
-
this._scopeUntilLoaded = null;
|
|
96
|
-
}
|
|
97
72
|
return this._tree;
|
|
98
73
|
});
|
|
99
74
|
|
package/src/FileTree.js
CHANGED
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
/**
|
|
14
14
|
* A file system tree via the Node file system API.
|
|
15
15
|
*
|
|
16
|
+
* File values are returned as Uint8Array instances. The underlying Node fs API
|
|
17
|
+
* returns file contents as instances of the node-specific Buffer class, but
|
|
18
|
+
* that class has some incompatible method implementations; see
|
|
19
|
+
* https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
|
|
20
|
+
* compatibility, files are returned as standard Uint8Array instances instead.
|
|
21
|
+
*
|
|
16
22
|
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
17
23
|
* @implements {AsyncMutableTree}
|
|
18
24
|
*/
|
|
@@ -65,8 +71,9 @@ export default class FileTree {
|
|
|
65
71
|
subtree.parent = this;
|
|
66
72
|
return subtree;
|
|
67
73
|
} else {
|
|
68
|
-
// Return file contents
|
|
69
|
-
|
|
74
|
+
// Return file contents as a standard Uint8Array.
|
|
75
|
+
const buffer = await fs.readFile(filePath);
|
|
76
|
+
return Uint8Array.from(buffer);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
79
|
|
package/src/operations/cache.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { toString } from "../utilities.js";
|
|
2
|
+
import deepValuesIterator from "./deepValuesIterator.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Concatenate the deep text values in a tree.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
*
|
|
9
|
+
* @this {AsyncTree|null}
|
|
10
|
+
* @param {import("@weborigami/async-tree").Treelike} treelike
|
|
11
|
+
*/
|
|
12
|
+
export default async function concatTreeValues(treelike) {
|
|
13
|
+
const strings = [];
|
|
14
|
+
for await (const value of deepValuesIterator(treelike, { expand: true })) {
|
|
15
|
+
if (value) {
|
|
16
|
+
strings.push(toString(value));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return strings.join("");
|
|
20
|
+
}
|
|
@@ -18,15 +18,28 @@ export default function deepMerge(...sources) {
|
|
|
18
18
|
|
|
19
19
|
// Check trees for the indicated key in reverse order.
|
|
20
20
|
for (let index = trees.length - 1; index >= 0; index--) {
|
|
21
|
-
const
|
|
21
|
+
const tree = trees[index];
|
|
22
|
+
const value = await tree.get(key);
|
|
22
23
|
if (Tree.isAsyncTree(value)) {
|
|
24
|
+
if (value.parent === tree) {
|
|
25
|
+
// Merged tree acts as parent instead of the source tree.
|
|
26
|
+
value.parent = this;
|
|
27
|
+
}
|
|
23
28
|
subtrees.unshift(value);
|
|
24
29
|
} else if (value !== undefined) {
|
|
25
30
|
return value;
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
if (subtrees.length > 1) {
|
|
35
|
+
const merged = deepMerge(...subtrees);
|
|
36
|
+
merged.parent = this;
|
|
37
|
+
return merged;
|
|
38
|
+
} else if (subtrees.length === 1) {
|
|
39
|
+
return subtrees[0];
|
|
40
|
+
} else {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
30
43
|
},
|
|
31
44
|
|
|
32
45
|
async isKeyForSubtree(key) {
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import deepValuesIterator from "./deepValuesIterator.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Return the in-order exterior values of a tree as a flat array.
|
|
5
5
|
*
|
|
6
6
|
* @param {import("../../index.ts").Treelike} treelike
|
|
7
|
+
* @param {{ expand?: boolean }} [options]
|
|
7
8
|
*/
|
|
8
|
-
export default async function deepValues(
|
|
9
|
-
|
|
9
|
+
export default async function deepValues(
|
|
10
|
+
treelike,
|
|
11
|
+
options = { expand: false }
|
|
12
|
+
) {
|
|
13
|
+
const iterator = deepValuesIterator(treelike, options);
|
|
14
|
+
const values = [];
|
|
15
|
+
for await (const value of iterator) {
|
|
16
|
+
values.push(value);
|
|
17
|
+
}
|
|
18
|
+
return values;
|
|
10
19
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return an iterator that yields all values in a tree, including nested trees.
|
|
5
|
+
*
|
|
6
|
+
* If the `expand` option is true, treelike values (but not functions) will be
|
|
7
|
+
* expanded into nested trees and their values will be yielded.
|
|
8
|
+
*
|
|
9
|
+
* @param {import("../../index.ts").Treelike} treelike
|
|
10
|
+
* @param {{ expand?: boolean }} [options]
|
|
11
|
+
* @returns {AsyncGenerator<any, void, undefined>}
|
|
12
|
+
*/
|
|
13
|
+
export default async function* deepValuesIterator(
|
|
14
|
+
treelike,
|
|
15
|
+
options = { expand: false }
|
|
16
|
+
) {
|
|
17
|
+
const tree = Tree.from(treelike);
|
|
18
|
+
for (const key of await tree.keys()) {
|
|
19
|
+
let value = await tree.get(key);
|
|
20
|
+
|
|
21
|
+
// Recurse into child trees, but don't expand functions.
|
|
22
|
+
const recurse =
|
|
23
|
+
Tree.isAsyncTree(value) ||
|
|
24
|
+
(options.expand && typeof value !== "function" && Tree.isTreelike(value));
|
|
25
|
+
if (recurse) {
|
|
26
|
+
yield* deepValuesIterator(value, options);
|
|
27
|
+
} else {
|
|
28
|
+
yield value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/operations/merge.js
CHANGED
|
@@ -11,19 +11,22 @@ import { Tree } from "../internal.js";
|
|
|
11
11
|
*
|
|
12
12
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
13
13
|
* @param {import("../../index.ts").Treelike[]} sources
|
|
14
|
-
* @returns {AsyncTree & { description: string }}
|
|
14
|
+
* @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
|
|
15
15
|
*/
|
|
16
16
|
export default function merge(...sources) {
|
|
17
|
-
let trees = sources.map((treelike) => Tree.from(treelike));
|
|
18
|
-
let mergeParent;
|
|
19
17
|
return {
|
|
20
18
|
description: "merge",
|
|
21
19
|
|
|
22
20
|
async get(key) {
|
|
23
21
|
// Check trees for the indicated key in reverse order.
|
|
24
|
-
for (let index = trees.length - 1; index >= 0; index--) {
|
|
25
|
-
const
|
|
22
|
+
for (let index = this.trees.length - 1; index >= 0; index--) {
|
|
23
|
+
const tree = this.trees[index];
|
|
24
|
+
const value = await tree.get(key);
|
|
26
25
|
if (value !== undefined) {
|
|
26
|
+
if (Tree.isAsyncTree(value) && value.parent === tree) {
|
|
27
|
+
// Merged tree acts as parent instead of the source tree.
|
|
28
|
+
value.parent = this;
|
|
29
|
+
}
|
|
27
30
|
return value;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -32,8 +35,8 @@ export default function merge(...sources) {
|
|
|
32
35
|
|
|
33
36
|
async isKeyForSubtree(key) {
|
|
34
37
|
// Check trees for the indicated key in reverse order.
|
|
35
|
-
for (let index = trees.length - 1; index >= 0; index--) {
|
|
36
|
-
if (await Tree.isKeyForSubtree(trees[index], key)) {
|
|
38
|
+
for (let index = this.trees.length - 1; index >= 0; index--) {
|
|
39
|
+
if (await Tree.isKeyForSubtree(this.trees[index], key)) {
|
|
37
40
|
return true;
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -43,7 +46,7 @@ export default function merge(...sources) {
|
|
|
43
46
|
async keys() {
|
|
44
47
|
const keys = new Set();
|
|
45
48
|
// Collect keys in the order the trees were provided.
|
|
46
|
-
for (const tree of trees) {
|
|
49
|
+
for (const tree of this.trees) {
|
|
47
50
|
for (const key of await tree.keys()) {
|
|
48
51
|
keys.add(key);
|
|
49
52
|
}
|
|
@@ -51,18 +54,6 @@ export default function merge(...sources) {
|
|
|
51
54
|
return keys;
|
|
52
55
|
},
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
return mergeParent;
|
|
56
|
-
},
|
|
57
|
-
set parent(parent) {
|
|
58
|
-
mergeParent = parent;
|
|
59
|
-
trees = sources.map((treelike) => {
|
|
60
|
-
const tree = Tree.isAsyncTree(treelike)
|
|
61
|
-
? Object.create(treelike)
|
|
62
|
-
: Tree.from(treelike);
|
|
63
|
-
tree.parent = parent;
|
|
64
|
-
return tree;
|
|
65
|
-
});
|
|
66
|
-
},
|
|
57
|
+
trees: sources.map((treelike) => Tree.from(treelike)),
|
|
67
58
|
};
|
|
68
59
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tree's "scope" is the collection of everything in that tree and all of its
|
|
5
|
+
* ancestors.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
9
|
+
*
|
|
10
|
+
* @param {Treelike} treelike
|
|
11
|
+
* @returns {AsyncTree & {trees: AsyncTree[]}}
|
|
12
|
+
*/
|
|
13
|
+
export default function scope(treelike) {
|
|
14
|
+
const tree = Tree.from(treelike);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
// Starting with this tree, search up the parent hierarchy.
|
|
18
|
+
async get(key) {
|
|
19
|
+
/** @type {AsyncTree|null|undefined} */
|
|
20
|
+
let current = tree;
|
|
21
|
+
let value;
|
|
22
|
+
while (current) {
|
|
23
|
+
value = await current.get(key);
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
current = current.parent;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Collect all keys for this tree and all parents
|
|
33
|
+
async keys() {
|
|
34
|
+
const keys = new Set();
|
|
35
|
+
|
|
36
|
+
/** @type {AsyncTree|null|undefined} */
|
|
37
|
+
let current = tree;
|
|
38
|
+
while (current) {
|
|
39
|
+
for (const key of await current.keys()) {
|
|
40
|
+
keys.add(key);
|
|
41
|
+
}
|
|
42
|
+
current = current.parent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return keys;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Collect all keys for this tree and all parents.
|
|
49
|
+
//
|
|
50
|
+
// This method exists for debugging purposes, as it's helpful to be able to
|
|
51
|
+
// quickly flatten and view the entire scope chain.
|
|
52
|
+
get trees() {
|
|
53
|
+
const result = [];
|
|
54
|
+
|
|
55
|
+
/** @type {AsyncTree|null|undefined} */
|
|
56
|
+
let current = tree;
|
|
57
|
+
while (current) {
|
|
58
|
+
result.push(current);
|
|
59
|
+
current = current.parent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -56,7 +56,9 @@ export default async function regExpKeys(treelike) {
|
|
|
56
56
|
let value = await tree.get(key);
|
|
57
57
|
if (Tree.isAsyncTree(value)) {
|
|
58
58
|
value = regExpKeys(value);
|
|
59
|
-
value.parent
|
|
59
|
+
if (!value.parent) {
|
|
60
|
+
value.parent = result;
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
map.set(regExp, value);
|
package/src/utilities.d.ts
CHANGED
|
@@ -10,4 +10,5 @@ export function isStringLike(object: any): object is StringLike;
|
|
|
10
10
|
export function keysFromPath(path: string): string[];
|
|
11
11
|
export const naturalOrder: (a: string, b: string) => number;
|
|
12
12
|
export function pipeline(start: any, ...functions: Function[]): Promise<any>;
|
|
13
|
-
export function
|
|
13
|
+
export function toPlainValue(object: any): Promise<any>;
|
|
14
|
+
export function toString(object: any): string;
|
package/src/utilities.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Tree } from "./internal.js";
|
|
2
|
+
|
|
1
3
|
const textDecoder = new TextDecoder();
|
|
2
4
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
3
5
|
|
|
@@ -56,7 +58,6 @@ export function isPacked(object) {
|
|
|
56
58
|
return (
|
|
57
59
|
typeof object === "string" ||
|
|
58
60
|
object instanceof ArrayBuffer ||
|
|
59
|
-
object instanceof Buffer ||
|
|
60
61
|
object instanceof ReadableStream ||
|
|
61
62
|
object instanceof String ||
|
|
62
63
|
object instanceof TypedArray
|
|
@@ -89,6 +90,20 @@ export function isPlainObject(object) {
|
|
|
89
90
|
return Object.getPrototypeOf(object) === getRealmObjectPrototype(object);
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Return true if the value is a primitive JavaScript value.
|
|
95
|
+
*
|
|
96
|
+
* @param {any} value
|
|
97
|
+
*/
|
|
98
|
+
export function isPrimitive(value) {
|
|
99
|
+
// Check for null first, since typeof null === "object".
|
|
100
|
+
if (value === null) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const type = typeof value;
|
|
104
|
+
return type !== "object" && type !== "function";
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
/**
|
|
93
108
|
* Return true if the object is a string or object with a non-trival `toString`
|
|
94
109
|
* method.
|
|
@@ -155,6 +170,72 @@ export async function pipeline(start, ...fns) {
|
|
|
155
170
|
return fns.reduce(async (acc, fn) => fn(await acc), start);
|
|
156
171
|
}
|
|
157
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Convert the given input to the plainest possible JavaScript value. This
|
|
175
|
+
* helper is intended for functions that want to accept an argument from the ori
|
|
176
|
+
* CLI, which could a string, a stream of data, or some other kind of JavaScript
|
|
177
|
+
* object.
|
|
178
|
+
*
|
|
179
|
+
* If the input is a function, it will be invoked and its result will be
|
|
180
|
+
* processed.
|
|
181
|
+
*
|
|
182
|
+
* If the input is a promise, it will be resolved and its result will be
|
|
183
|
+
* processed.
|
|
184
|
+
*
|
|
185
|
+
* If the input is treelike, it will be converted to a plain JavaScript object,
|
|
186
|
+
* recursively traversing the tree and converting all values to plain types.
|
|
187
|
+
*
|
|
188
|
+
* If the input is stringlike, its text will be returned.
|
|
189
|
+
*
|
|
190
|
+
* If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
|
|
191
|
+
* text if it does not contain unprintable characters. If it does, it will be
|
|
192
|
+
* returned as a base64-encoded string.
|
|
193
|
+
*
|
|
194
|
+
* If the input has a custom class instance, its public properties will be
|
|
195
|
+
* returned as a plain object.
|
|
196
|
+
*
|
|
197
|
+
* @param {any} input
|
|
198
|
+
* @returns {Promise<any>}
|
|
199
|
+
*/
|
|
200
|
+
export async function toPlainValue(input) {
|
|
201
|
+
if (input instanceof Function) {
|
|
202
|
+
// Invoke function
|
|
203
|
+
input = input();
|
|
204
|
+
}
|
|
205
|
+
if (input instanceof Promise) {
|
|
206
|
+
// Resolve promise
|
|
207
|
+
input = await input;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (isPrimitive(input) || input instanceof Date) {
|
|
211
|
+
return input;
|
|
212
|
+
} else if (Tree.isTreelike(input)) {
|
|
213
|
+
const mapped = await Tree.map(input, (value) => toPlainValue(value));
|
|
214
|
+
return Tree.plain(mapped);
|
|
215
|
+
} else if (isStringLike(input)) {
|
|
216
|
+
return toString(input);
|
|
217
|
+
} else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
|
|
218
|
+
// Try to interpret the buffer as UTF-8 text, otherwise use base64.
|
|
219
|
+
const text = toString(input);
|
|
220
|
+
if (text !== null) {
|
|
221
|
+
return text;
|
|
222
|
+
} else {
|
|
223
|
+
return toBase64(input);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
// Some other kind of class instance; return its public properties.
|
|
227
|
+
const plain = {};
|
|
228
|
+
for (const [key, value] of Object.entries(input)) {
|
|
229
|
+
plain[key] = await toPlainValue(value);
|
|
230
|
+
}
|
|
231
|
+
return plain;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function toBase64(object) {
|
|
236
|
+
return Buffer.from(object).toString("base64");
|
|
237
|
+
}
|
|
238
|
+
|
|
158
239
|
/**
|
|
159
240
|
* Return a string form of the object, handling cases not generally handled by
|
|
160
241
|
* the standard JavaScript `toString()` method:
|
|
@@ -164,6 +245,10 @@ export async function pipeline(start, ...fns) {
|
|
|
164
245
|
* default toString() method, return null instead of "[object Object]". In
|
|
165
246
|
* practice, it's generally more useful to have this method fail than to
|
|
166
247
|
* return a useless string.
|
|
248
|
+
* 3. If the object is a defined primitive value, return the result of
|
|
249
|
+
* String(object).
|
|
250
|
+
*
|
|
251
|
+
* Otherwise return null.
|
|
167
252
|
*
|
|
168
253
|
* @param {any} object
|
|
169
254
|
* @returns {string|null}
|
|
@@ -176,7 +261,7 @@ export function toString(object) {
|
|
|
176
261
|
// https://stackoverflow.com/a/1677660/76472
|
|
177
262
|
const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
|
|
178
263
|
return hasNonPrintableCharacters ? null : decoded;
|
|
179
|
-
} else if (isStringLike(object)) {
|
|
264
|
+
} else if (isStringLike(object) || (object !== null && isPrimitive(object))) {
|
|
180
265
|
return String(object);
|
|
181
266
|
} else {
|
|
182
267
|
return null;
|
|
@@ -15,9 +15,11 @@ describe("DeepObjectTree", () => {
|
|
|
15
15
|
const object = await tree.get("object");
|
|
16
16
|
assert.equal(object instanceof DeepObjectTree, true);
|
|
17
17
|
assert.deepEqual(await Tree.plain(object), { b: 2 });
|
|
18
|
+
assert.equal(object.parent, tree);
|
|
18
19
|
|
|
19
20
|
const array = await tree.get("array");
|
|
20
21
|
assert.equal(array instanceof DeepObjectTree, true);
|
|
21
22
|
assert.deepEqual(await Tree.plain(array), [3]);
|
|
23
|
+
assert.equal(array.parent, tree);
|
|
22
24
|
});
|
|
23
25
|
});
|
package/test/FileTree.test.js
CHANGED
|
@@ -9,6 +9,8 @@ import { Tree } from "../src/internal.js";
|
|
|
9
9
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const tempDirectory = path.join(dirname, "fixtures/temp");
|
|
11
11
|
|
|
12
|
+
const textDecoder = new TextDecoder();
|
|
13
|
+
|
|
12
14
|
describe.only("FileTree", async () => {
|
|
13
15
|
test("can get the keys of the tree", async () => {
|
|
14
16
|
const fixture = createFixture("fixtures/markdown");
|
|
@@ -21,8 +23,9 @@ describe.only("FileTree", async () => {
|
|
|
21
23
|
|
|
22
24
|
test("can get the value for a key", async () => {
|
|
23
25
|
const fixture = createFixture("fixtures/markdown");
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
+
const buffer = await fixture.get("Alice.md");
|
|
27
|
+
const text = textDecoder.decode(buffer);
|
|
28
|
+
assert.equal(text, "Hello, **Alice**.");
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
test("getting an unsupported key returns undefined", async () => {
|
|
@@ -95,7 +98,9 @@ describe.only("FileTree", async () => {
|
|
|
95
98
|
|
|
96
99
|
// Read them back in.
|
|
97
100
|
const actualFiles = await tempFiles.get("folder");
|
|
98
|
-
const strings = Tree.map(actualFiles, (buffer) =>
|
|
101
|
+
const strings = Tree.map(actualFiles, (buffer) =>
|
|
102
|
+
textDecoder.decode(buffer)
|
|
103
|
+
);
|
|
99
104
|
const plain = await Tree.plain(strings);
|
|
100
105
|
assert.deepEqual(plain, obj);
|
|
101
106
|
|
package/test/SiteTree.test.js
CHANGED
|
@@ -3,6 +3,9 @@ import { beforeEach, describe, mock, test } from "node:test";
|
|
|
3
3
|
import SiteTree from "../src/SiteTree.js";
|
|
4
4
|
import { Tree } from "../src/internal.js";
|
|
5
5
|
|
|
6
|
+
const textDecoder = new TextDecoder();
|
|
7
|
+
const textEncoder = new TextEncoder();
|
|
8
|
+
|
|
6
9
|
const mockHost = "https://mock";
|
|
7
10
|
|
|
8
11
|
const mockResponses = {
|
|
@@ -53,8 +56,9 @@ describe("SiteTree", () => {
|
|
|
53
56
|
test("can get the value for a key", async () => {
|
|
54
57
|
const fixture = new SiteTree(mockHost);
|
|
55
58
|
const about = fixture.resolve("about");
|
|
56
|
-
const
|
|
57
|
-
|
|
59
|
+
const arrayBuffer = await about.get("Alice.html");
|
|
60
|
+
const text = textDecoder.decode(arrayBuffer);
|
|
61
|
+
assert.equal(text, "Hello, Alice!");
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
test("getting an unsupported key returns undefined", async () => {
|
|
@@ -78,7 +82,7 @@ describe("SiteTree", () => {
|
|
|
78
82
|
test("can convert a SiteGraph to a plain object", async () => {
|
|
79
83
|
const fixture = new SiteTree(mockHost);
|
|
80
84
|
// Convert buffers to strings.
|
|
81
|
-
const strings = Tree.map(fixture, (value) =>
|
|
85
|
+
const strings = Tree.map(fixture, (value) => textDecoder.decode(value));
|
|
82
86
|
assert.deepEqual(await Tree.plain(strings), {
|
|
83
87
|
about: {
|
|
84
88
|
"Alice.html": "Hello, Alice!",
|
|
@@ -98,8 +102,7 @@ async function mockFetch(href) {
|
|
|
98
102
|
if (mockedResponse) {
|
|
99
103
|
return Object.assign(
|
|
100
104
|
{
|
|
101
|
-
|
|
102
|
-
arrayBuffer: () => Buffer.from(mockedResponse.data),
|
|
105
|
+
arrayBuffer: () => textEncoder.encode(mockedResponse.data).buffer,
|
|
103
106
|
ok: true,
|
|
104
107
|
status: 200,
|
|
105
108
|
text: () => mockedResponse.data,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import FunctionTree from "../../src/FunctionTree.js";
|
|
4
|
+
import { Tree } from "../../src/internal.js";
|
|
5
|
+
import concat from "../../src/operations/concat.js";
|
|
6
|
+
|
|
7
|
+
describe("concat", () => {
|
|
8
|
+
test("concatenates deep tree values", async () => {
|
|
9
|
+
const tree = Tree.from({
|
|
10
|
+
a: "A",
|
|
11
|
+
b: "B",
|
|
12
|
+
c: "C",
|
|
13
|
+
more: {
|
|
14
|
+
d: "D",
|
|
15
|
+
e: "E",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const result = await concat.call(null, tree);
|
|
19
|
+
assert.equal(result, "ABCDE");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("concatenates deep tree-like values", async () => {
|
|
23
|
+
const letters = ["a", "b", "c"];
|
|
24
|
+
const specimens = new FunctionTree(
|
|
25
|
+
(letter) => ({
|
|
26
|
+
lowercase: letter,
|
|
27
|
+
uppercase: letter.toUpperCase(),
|
|
28
|
+
}),
|
|
29
|
+
letters
|
|
30
|
+
);
|
|
31
|
+
const result = await concat.call(null, specimens);
|
|
32
|
+
assert.equal(result, "aAbBcC");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
3
4
|
import deepTakeFn from "../../src/operations/deepTakeFn.js";
|
|
4
5
|
|
|
5
6
|
describe("deepTakeFn", () => {
|
|
@@ -16,6 +17,6 @@ describe("deepTakeFn", () => {
|
|
|
16
17
|
g: 5,
|
|
17
18
|
};
|
|
18
19
|
const result = await deepTakeFn(4)(tree);
|
|
19
|
-
assert.deepEqual(result, [1, 2, 3, 4]);
|
|
20
|
+
assert.deepEqual(await Tree.plain(result), [1, 2, 3, 4]);
|
|
20
21
|
});
|
|
21
22
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { ObjectTree } from "../../src/internal.js";
|
|
4
|
+
import deepValuesIterator from "../../src/operations/deepValuesIterator.js";
|
|
5
|
+
|
|
6
|
+
describe("deepValuesIterator", () => {
|
|
7
|
+
test("returns an iterator of a tree's deep values", async () => {
|
|
8
|
+
const tree = new ObjectTree({
|
|
9
|
+
a: 1,
|
|
10
|
+
b: 2,
|
|
11
|
+
more: {
|
|
12
|
+
c: 3,
|
|
13
|
+
d: 4,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const values = [];
|
|
17
|
+
// The tree will be shallow, but we'll ask to expand the values.
|
|
18
|
+
for await (const value of deepValuesIterator(tree, { expand: true })) {
|
|
19
|
+
values.push(value);
|
|
20
|
+
}
|
|
21
|
+
assert.deepEqual(values, [1, 2, 3, 4]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -33,7 +33,12 @@ describe("merge", () => {
|
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
// Merge is shallow, and last tree wins, so `b/c` doesn't exist
|
|
36
37
|
const c = await Tree.traverse(fixture, "b", "c");
|
|
37
38
|
assert.equal(c, undefined);
|
|
39
|
+
|
|
40
|
+
// Parent of a subvalue is the merged tree
|
|
41
|
+
const b = await Tree.traverse(fixture, "b");
|
|
42
|
+
assert.equal(b.parent, fixture);
|
|
38
43
|
});
|
|
39
44
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { ObjectTree } from "../../src/internal.js";
|
|
4
|
+
import scope from "../../src/operations/scope.js";
|
|
5
|
+
|
|
6
|
+
describe("scope", () => {
|
|
7
|
+
test("gets the first defined value from the scope trees", async () => {
|
|
8
|
+
const outer = new ObjectTree({
|
|
9
|
+
a: 1,
|
|
10
|
+
b: 2,
|
|
11
|
+
});
|
|
12
|
+
const inner = new ObjectTree({
|
|
13
|
+
a: 3,
|
|
14
|
+
});
|
|
15
|
+
inner.parent = outer;
|
|
16
|
+
const innerScope = scope(inner);
|
|
17
|
+
assert.deepEqual([...(await innerScope.keys())], ["a", "b"]);
|
|
18
|
+
// Inner tree has precedence
|
|
19
|
+
assert.equal(await innerScope.get("a"), 3);
|
|
20
|
+
// If tree doesn't have value, finds value from parent
|
|
21
|
+
assert.equal(await innerScope.get("b"), 2);
|
|
22
|
+
assert.equal(await innerScope.get("c"), undefined);
|
|
23
|
+
assert.deepEqual(innerScope.trees, [inner, outer]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Tree } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
4
|
import deepReverse from "../../src/transforms/deepReverse.js";
|
|
5
5
|
|
|
6
6
|
describe("deepReverse", () => {
|
package/test/utilities.test.js
CHANGED
|
@@ -36,6 +36,31 @@ describe("utilities", () => {
|
|
|
36
36
|
assert.equal(result, 16);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
test("toPlainValue returns the plainest representation of an object", async () => {
|
|
40
|
+
class User {
|
|
41
|
+
constructor(name) {
|
|
42
|
+
this.name = name;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert.equal(await utilities.toPlainValue(1), 1);
|
|
47
|
+
assert.equal(await utilities.toPlainValue("string"), "string");
|
|
48
|
+
assert.deepEqual(await utilities.toPlainValue({ a: 1 }), { a: 1 });
|
|
49
|
+
assert.equal(
|
|
50
|
+
await utilities.toPlainValue(new TextEncoder().encode("bytes")),
|
|
51
|
+
"bytes"
|
|
52
|
+
);
|
|
53
|
+
// ArrayBuffer with non-printable characters should be returned as base64
|
|
54
|
+
assert.equal(
|
|
55
|
+
await utilities.toPlainValue(new Uint8Array([1, 2, 3]).buffer),
|
|
56
|
+
"AQID"
|
|
57
|
+
);
|
|
58
|
+
assert.equal(await utilities.toPlainValue(async () => "result"), "result");
|
|
59
|
+
assert.deepEqual(await utilities.toPlainValue(new User("Alice")), {
|
|
60
|
+
name: "Alice",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
39
64
|
test("toString returns the value of an object's `toString` method", () => {
|
|
40
65
|
const object = {
|
|
41
66
|
toString: () => "text",
|
|
@@ -49,7 +74,7 @@ describe("utilities", () => {
|
|
|
49
74
|
});
|
|
50
75
|
|
|
51
76
|
test("toString decodes an ArrayBuffer as UTF-8", () => {
|
|
52
|
-
const
|
|
53
|
-
assert.equal(utilities.toString(
|
|
77
|
+
const arrayBuffer = new TextEncoder().encode("text").buffer;
|
|
78
|
+
assert.equal(utilities.toString(arrayBuffer), "text");
|
|
54
79
|
});
|
|
55
80
|
});
|