@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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import TypedArray from "./TypedArray.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return true if the object is in a packed form (or can be readily packed into
|
|
5
|
+
* a form) that can be given to fs.writeFile or response.write().
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("../../index.ts").Packed} Packed
|
|
8
|
+
*
|
|
9
|
+
* @param {any} obj
|
|
10
|
+
* @returns {obj is Packed}
|
|
11
|
+
*/
|
|
12
|
+
export default function isPacked(obj) {
|
|
13
|
+
return (
|
|
14
|
+
typeof obj === "string" ||
|
|
15
|
+
obj instanceof ArrayBuffer ||
|
|
16
|
+
obj instanceof ReadableStream ||
|
|
17
|
+
obj instanceof String ||
|
|
18
|
+
obj instanceof TypedArray
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import getRealmObjectPrototype from "./getRealmObjectPrototype.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return true if the object is a plain JavaScript object created by `{}`,
|
|
5
|
+
* `new Object()`, or `Object.create(null)`.
|
|
6
|
+
*
|
|
7
|
+
* This function also considers object-like things with no prototype (like a
|
|
8
|
+
* `Module`) as plain objects.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {import("../../index.ts").PlainObject} PlainObject
|
|
11
|
+
*
|
|
12
|
+
* @param {any} obj
|
|
13
|
+
* @returns {obj is PlainObject}
|
|
14
|
+
*/
|
|
15
|
+
export default function isPlainObject(obj) {
|
|
16
|
+
// From https://stackoverflow.com/q/51722354/76472
|
|
17
|
+
if (typeof obj !== "object" || obj === null) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// We treat object-like things with no prototype (like a Module) as plain
|
|
22
|
+
// objects.
|
|
23
|
+
if (Object.getPrototypeOf(obj) === null) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Do we inherit directly from Object in this realm?
|
|
28
|
+
return Object.getPrototypeOf(obj) === getRealmObjectPrototype(obj);
|
|
29
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return true if the value is a primitive JavaScript value.
|
|
3
|
+
*
|
|
4
|
+
* @param {any} value
|
|
5
|
+
*/
|
|
6
|
+
export default function isPrimitive(value) {
|
|
7
|
+
// Check for null first, since typeof null === "object".
|
|
8
|
+
if (value === null) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
const type = typeof value;
|
|
12
|
+
return type !== "object" && type !== "function";
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import getRealmObjectPrototype from "./getRealmObjectPrototype.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return true if the object is a string or object with a non-trival `toString`
|
|
5
|
+
* method.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("../../index.ts").Stringlike} Stringlike
|
|
8
|
+
*
|
|
9
|
+
* @param {any} obj
|
|
10
|
+
* @returns {obj is Stringlike}
|
|
11
|
+
*/
|
|
12
|
+
export default function isStringlike(obj) {
|
|
13
|
+
if (typeof obj === "string") {
|
|
14
|
+
return true;
|
|
15
|
+
} else if (obj?.toString === undefined) {
|
|
16
|
+
return false;
|
|
17
|
+
} else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
|
|
18
|
+
// The stupid Object.prototype.toString implementation always returns
|
|
19
|
+
// "[object Object]", so if that's the only toString method the object has,
|
|
20
|
+
// we return false.
|
|
21
|
+
return false;
|
|
22
|
+
} else {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import isPacked from "./isPacked.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import("../../index.ts").Unpackable} Unpackable
|
|
5
|
+
*
|
|
6
|
+
* @param {any} obj
|
|
7
|
+
* @returns {obj is Unpackable}
|
|
8
|
+
*/
|
|
9
|
+
export default function isUnpackable(obj) {
|
|
10
|
+
return (
|
|
11
|
+
isPacked(obj) && typeof (/** @type {any} */ (obj).unpack) === "function"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Given a path like "/foo/bar/baz", return an array of keys like ["foo/",
|
|
5
|
+
* "bar/", "baz"].
|
|
6
|
+
*
|
|
7
|
+
* Leading slashes are ignored. Consecutive slashes will be ignored. Trailing
|
|
8
|
+
* slashes are preserved.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} pathname
|
|
11
|
+
*/
|
|
12
|
+
export default function keysFromPath(pathname) {
|
|
13
|
+
// Split the path at each slash
|
|
14
|
+
let keys = pathname.split("/");
|
|
15
|
+
if (keys[0] === "") {
|
|
16
|
+
// The path begins with a slash; drop that part.
|
|
17
|
+
keys.shift();
|
|
18
|
+
}
|
|
19
|
+
if (keys.at(-1) === "") {
|
|
20
|
+
// The path ends with a slash; drop that part.
|
|
21
|
+
keys.pop();
|
|
22
|
+
}
|
|
23
|
+
// Drop any empty keys
|
|
24
|
+
keys = keys.filter((key) => key !== "");
|
|
25
|
+
// Add the trailing slash back to all keys but the last
|
|
26
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
27
|
+
keys[i] += "/";
|
|
28
|
+
}
|
|
29
|
+
// Add trailing slash to last key if path ended with a slash
|
|
30
|
+
if (keys.length > 0 && trailingSlash.has(pathname)) {
|
|
31
|
+
keys[keys.length - 1] += "/";
|
|
32
|
+
}
|
|
33
|
+
return keys;
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Return a slash-separated path for the given keys.
|
|
5
|
+
*
|
|
6
|
+
* This takes care to avoid adding consecutive slashes if they keys themselves
|
|
7
|
+
* already have trailing slashes.
|
|
8
|
+
*
|
|
9
|
+
* @param {string[]} keys
|
|
10
|
+
*/
|
|
11
|
+
export default function pathFromKeys(keys) {
|
|
12
|
+
// Ensure there's a slash between all keys. If the last key has a trailing
|
|
13
|
+
// slash, leave it there.
|
|
14
|
+
const normalized = keys.map((key, index) =>
|
|
15
|
+
index < keys.length - 1 ? trailingSlash.add(key) : key
|
|
16
|
+
);
|
|
17
|
+
return normalized.join("");
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import isAsyncTree from "../operations/isAsyncTree.js";
|
|
2
|
+
import * as symbols from "../symbols.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* If the child object doesn't have a parent yet, set it to the indicated
|
|
6
|
+
* parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
|
|
7
|
+
* set the `symbols.parent` property.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
10
|
+
*
|
|
11
|
+
* @param {*} child
|
|
12
|
+
* @param {AsyncTree|null} parent
|
|
13
|
+
*/
|
|
14
|
+
export default function setParent(child, parent) {
|
|
15
|
+
if (isAsyncTree(child)) {
|
|
16
|
+
// Value is a subtree; set its parent to this tree.
|
|
17
|
+
if (!child.parent) {
|
|
18
|
+
child.parent = parent;
|
|
19
|
+
}
|
|
20
|
+
} else if (Object.isExtensible(child) && !child[symbols.parent]) {
|
|
21
|
+
try {
|
|
22
|
+
// Add parent reference as a symbol to avoid polluting the object. This
|
|
23
|
+
// reference will be used if the object is later used as a tree. We set
|
|
24
|
+
// `enumerable` to false even thought this makes no practical difference
|
|
25
|
+
// (symbols are never enumerated) because it can provide a hint in the
|
|
26
|
+
// debugger that the property is for internal use.
|
|
27
|
+
Object.defineProperty(child, symbols.parent, {
|
|
28
|
+
configurable: true,
|
|
29
|
+
enumerable: false,
|
|
30
|
+
value: parent,
|
|
31
|
+
writable: true,
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Ignore exceptions. Some esoteric objects don't allow adding properties.
|
|
35
|
+
// We can still treat them as trees, but they won't have a parent.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import from from "../operations/from.js";
|
|
2
|
+
import isTreelike from "../operations/isTreelike.js";
|
|
3
|
+
import isUnpackable from "./isUnpackable.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert the given object to a function.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("../../index.ts").Invocable} Invocable
|
|
9
|
+
*
|
|
10
|
+
* @param {Invocable} obj
|
|
11
|
+
* @returns {Function|null}
|
|
12
|
+
*/
|
|
13
|
+
export default function toFunction(obj) {
|
|
14
|
+
if (typeof obj === "function") {
|
|
15
|
+
// Return a function as is.
|
|
16
|
+
return obj;
|
|
17
|
+
} else if (isUnpackable(obj)) {
|
|
18
|
+
// Extract the contents of the object and convert that to a function.
|
|
19
|
+
let fnPromise;
|
|
20
|
+
/** @this {any} */
|
|
21
|
+
return async function (...args) {
|
|
22
|
+
if (!fnPromise) {
|
|
23
|
+
// unpack() may return a function or a promise for a function; normalize
|
|
24
|
+
// to a promise for a function
|
|
25
|
+
const unpackPromise = Promise.resolve(
|
|
26
|
+
/** @type {any} */ (obj).unpack()
|
|
27
|
+
);
|
|
28
|
+
fnPromise = unpackPromise.then((content) => toFunction(content));
|
|
29
|
+
}
|
|
30
|
+
const fn = await fnPromise;
|
|
31
|
+
return fn.call(this, ...args);
|
|
32
|
+
};
|
|
33
|
+
} else if (isTreelike(obj)) {
|
|
34
|
+
// Return a function that invokes the tree's getter.
|
|
35
|
+
const tree = from(obj);
|
|
36
|
+
return tree.get.bind(tree);
|
|
37
|
+
} else {
|
|
38
|
+
// Not a function
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ObjectTree from "../drivers/ObjectTree.js";
|
|
2
|
+
import isTreelike from "../operations/isTreelike.js";
|
|
3
|
+
import mapReduce from "../operations/mapReduce.js";
|
|
4
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
5
|
+
import castArraylike from "./castArraylike.js";
|
|
6
|
+
import isPrimitive from "./isPrimitive.js";
|
|
7
|
+
import isStringlike from "./isStringlike.js";
|
|
8
|
+
import toString from "./toString.js";
|
|
9
|
+
import TypedArray from "./TypedArray.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert the given input to the plainest possible JavaScript value. This
|
|
13
|
+
* helper is intended for functions that want to accept an argument from the ori
|
|
14
|
+
* CLI, which could a string, a stream of data, or some other kind of JavaScript
|
|
15
|
+
* object.
|
|
16
|
+
*
|
|
17
|
+
* If the input is a function, it will be invoked and its result will be
|
|
18
|
+
* processed.
|
|
19
|
+
*
|
|
20
|
+
* If the input is a promise, it will be resolved and its result will be
|
|
21
|
+
* processed.
|
|
22
|
+
*
|
|
23
|
+
* If the input is treelike, it will be converted to a plain JavaScript object,
|
|
24
|
+
* recursively traversing the tree and converting all values to plain types.
|
|
25
|
+
*
|
|
26
|
+
* If the input is stringlike, its text will be returned.
|
|
27
|
+
*
|
|
28
|
+
* If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
|
|
29
|
+
* text if it does not contain unprintable characters. If it does, it will be
|
|
30
|
+
* returned as a base64-encoded string.
|
|
31
|
+
*
|
|
32
|
+
* If the input has a custom class instance, its public properties will be
|
|
33
|
+
* returned as a plain object.
|
|
34
|
+
*
|
|
35
|
+
* @param {any} input
|
|
36
|
+
* @returns {Promise<any>}
|
|
37
|
+
*/
|
|
38
|
+
export default async function toPlainValue(input) {
|
|
39
|
+
if (input instanceof Function) {
|
|
40
|
+
// Invoke function
|
|
41
|
+
input = input();
|
|
42
|
+
}
|
|
43
|
+
if (input instanceof Promise) {
|
|
44
|
+
// Resolve promise
|
|
45
|
+
input = await input;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isPrimitive(input) || input instanceof Date) {
|
|
49
|
+
return input;
|
|
50
|
+
} else if (isTreelike(input)) {
|
|
51
|
+
// Recursively convert tree to plain object.
|
|
52
|
+
return mapReduce(input, toPlainValue, (values, keys, tree) => {
|
|
53
|
+
// Special case for an empty tree: if based on array, return array.
|
|
54
|
+
if (tree instanceof ObjectTree && keys.length === 0) {
|
|
55
|
+
return /** @type {any} */ (tree).object instanceof Array ? [] : {};
|
|
56
|
+
}
|
|
57
|
+
// Normalize slashes in keys.
|
|
58
|
+
keys = keys.map(trailingSlash.remove);
|
|
59
|
+
return castArraylike(keys, values);
|
|
60
|
+
});
|
|
61
|
+
} else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
|
|
62
|
+
// Try to interpret the buffer as UTF-8 text, otherwise use base64.
|
|
63
|
+
const text = toString(input);
|
|
64
|
+
if (text !== null) {
|
|
65
|
+
return text;
|
|
66
|
+
} else {
|
|
67
|
+
return toBase64(input);
|
|
68
|
+
}
|
|
69
|
+
} else if (isStringlike(input)) {
|
|
70
|
+
return toString(input);
|
|
71
|
+
} else {
|
|
72
|
+
// Some other kind of class instance; return its public properties.
|
|
73
|
+
const plain = {};
|
|
74
|
+
for (const [key, value] of Object.entries(input)) {
|
|
75
|
+
plain[key] = await toPlainValue(value);
|
|
76
|
+
}
|
|
77
|
+
return plain;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toBase64(object) {
|
|
82
|
+
if (typeof Buffer !== "undefined") {
|
|
83
|
+
// Node.js environment
|
|
84
|
+
return Buffer.from(object).toString("base64");
|
|
85
|
+
} else {
|
|
86
|
+
// Browser environment
|
|
87
|
+
let binary = "";
|
|
88
|
+
const bytes = new Uint8Array(object);
|
|
89
|
+
const len = bytes.byteLength;
|
|
90
|
+
for (let i = 0; i < len; i++) {
|
|
91
|
+
binary += String.fromCharCode(bytes[i]);
|
|
92
|
+
}
|
|
93
|
+
return btoa(binary);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import isPrimitive from "./isPrimitive.js";
|
|
2
|
+
import isStringlike from "./isStringlike.js";
|
|
3
|
+
import TypedArray from "./TypedArray.js";
|
|
4
|
+
|
|
5
|
+
const textDecoder = new TextDecoder();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return a string form of the object, handling cases not generally handled by
|
|
9
|
+
* the standard JavaScript `toString()` method:
|
|
10
|
+
*
|
|
11
|
+
* 1. If the object is an ArrayBuffer or TypedArray, decode the array as UTF-8.
|
|
12
|
+
* 2. If the object is otherwise a plain JavaScript object with the useless
|
|
13
|
+
* default toString() method, return null instead of "[object Object]". In
|
|
14
|
+
* practice, it's generally more useful to have this method fail than to
|
|
15
|
+
* return a useless string.
|
|
16
|
+
* 3. If the object is a defined primitive value, return the result of
|
|
17
|
+
* String(object).
|
|
18
|
+
*
|
|
19
|
+
* Otherwise return null.
|
|
20
|
+
*
|
|
21
|
+
* @param {any} object
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
export default function toString(object) {
|
|
25
|
+
if (object instanceof ArrayBuffer || object instanceof TypedArray) {
|
|
26
|
+
// Treat the buffer as UTF-8 text.
|
|
27
|
+
const decoded = textDecoder.decode(object);
|
|
28
|
+
// If the result appears to contain non-printable characters, it's probably not a string.
|
|
29
|
+
// https://stackoverflow.com/a/1677660/76472
|
|
30
|
+
const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
|
|
31
|
+
return hasNonPrintableCharacters ? null : decoded;
|
|
32
|
+
} else if (isStringlike(object) || (object !== null && isPrimitive(object))) {
|
|
33
|
+
return String(object);
|
|
34
|
+
} else {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -77,7 +77,7 @@ describe("ExplorableSiteTree", () => {
|
|
|
77
77
|
test("can convert a site to a plain object", async () => {
|
|
78
78
|
const fixture = new ExplorableSiteTree(mockHost);
|
|
79
79
|
// Convert buffers to strings.
|
|
80
|
-
const strings = Tree.map(fixture, {
|
|
80
|
+
const strings = await Tree.map(fixture, {
|
|
81
81
|
deep: true,
|
|
82
82
|
value: (value) => textDecoder.decode(value),
|
|
83
83
|
});
|
|
@@ -132,7 +132,7 @@ describe("FileTree", async () => {
|
|
|
132
132
|
|
|
133
133
|
// Read them back in.
|
|
134
134
|
const actualFiles = await tempFiles.get("folder");
|
|
135
|
-
const strings = Tree.map(actualFiles, {
|
|
135
|
+
const strings = await Tree.map(actualFiles, {
|
|
136
136
|
deep: true,
|
|
137
137
|
value: (buffer) => textDecoder.decode(buffer),
|
|
138
138
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import calendar from "../../src/drivers/calendarTree.js";
|
|
4
|
-
import
|
|
4
|
+
import toPlainValue from "../../src/utilities/toPlainValue.js";
|
|
5
5
|
|
|
6
6
|
describe("calendarTree", () => {
|
|
7
7
|
test("without a start or end, returns a tree for today", async () => {
|
package/test/jsonKeys.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import
|
|
3
|
+
import DeepObjectTree from "../src/drivers/DeepObjectTree.js";
|
|
4
4
|
import * as jsonKeys from "../src/jsonKeys.js";
|
|
5
5
|
|
|
6
6
|
describe("jsonKeys", () => {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import DeepObjectTree from "../../src/drivers/DeepObjectTree.js";
|
|
4
|
+
import ObjectTree from "../../src/drivers/ObjectTree.js";
|
|
5
|
+
import assign from "../../src/operations/assign.js";
|
|
6
|
+
import plain from "../../src/operations/plain.js";
|
|
7
|
+
|
|
8
|
+
describe("assign", () => {
|
|
9
|
+
test("assign applies one tree to another", async () => {
|
|
10
|
+
const target = new DeepObjectTree({
|
|
11
|
+
a: 1,
|
|
12
|
+
b: 2,
|
|
13
|
+
more: {
|
|
14
|
+
d: 3,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const source = new DeepObjectTree({
|
|
19
|
+
a: 4, // Overwrite existing value
|
|
20
|
+
b: undefined, // Delete
|
|
21
|
+
c: 5, // Add
|
|
22
|
+
more: {
|
|
23
|
+
// Should leave existing `more` keys alone.
|
|
24
|
+
e: 6, // Add
|
|
25
|
+
},
|
|
26
|
+
// Add new subtree
|
|
27
|
+
extra: {
|
|
28
|
+
f: 7,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Apply changes.
|
|
33
|
+
const result = await assign(target, source);
|
|
34
|
+
|
|
35
|
+
assert.equal(result, target);
|
|
36
|
+
assert.deepEqual(await plain(target), {
|
|
37
|
+
a: 4,
|
|
38
|
+
c: 5,
|
|
39
|
+
more: {
|
|
40
|
+
d: 3,
|
|
41
|
+
e: 6,
|
|
42
|
+
},
|
|
43
|
+
extra: {
|
|
44
|
+
f: 7,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("assign() can apply updates to an array", async () => {
|
|
50
|
+
const target = new ObjectTree(["a", "b", "c"]);
|
|
51
|
+
await assign(target, ["d", "e"]);
|
|
52
|
+
assert.deepEqual(await plain(target), ["d", "e", "c"]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -6,7 +6,7 @@ import cache from "../../src/operations/cache.js";
|
|
|
6
6
|
describe("cache", () => {
|
|
7
7
|
test("caches reads of values from one tree into another", async () => {
|
|
8
8
|
const objectCache = new ObjectTree({});
|
|
9
|
-
const fixture = cache(
|
|
9
|
+
const fixture = await cache(
|
|
10
10
|
new DeepObjectTree({
|
|
11
11
|
a: 1,
|
|
12
12
|
b: 2,
|
|
@@ -12,7 +12,7 @@ describe("cachedKeyFunctions", () => {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
let callCount = 0;
|
|
15
|
-
const addUnderscore = async (sourceKey, tree) => {
|
|
15
|
+
const addUnderscore = async (sourceValue, sourceKey, tree) => {
|
|
16
16
|
callCount++;
|
|
17
17
|
return `_${sourceKey}`;
|
|
18
18
|
};
|
|
@@ -26,15 +26,15 @@ describe("cachedKeyFunctions", () => {
|
|
|
26
26
|
assert.equal(await inverseKey("_b", tree), "b"); // Cache miss
|
|
27
27
|
assert.equal(callCount, 2);
|
|
28
28
|
|
|
29
|
-
assert.equal(await key("a", tree), "_a");
|
|
30
|
-
assert.equal(await key("a", tree), "_a");
|
|
31
|
-
assert.equal(await key("b", tree), "_b");
|
|
29
|
+
assert.equal(await key(null, "a", tree), "_a");
|
|
30
|
+
assert.equal(await key(null, "a", tree), "_a");
|
|
31
|
+
assert.equal(await key(null, "b", tree), "_b");
|
|
32
32
|
assert.equal(callCount, 2);
|
|
33
33
|
|
|
34
34
|
// `c` isn't in tree, so we should get undefined.
|
|
35
35
|
assert.equal(await inverseKey("_c", tree), undefined);
|
|
36
36
|
// But key mapping is still possible.
|
|
37
|
-
assert.equal(await key("c", tree), "_c");
|
|
37
|
+
assert.equal(await key(null, "c", tree), "_c");
|
|
38
38
|
// And now we have a cache hit.
|
|
39
39
|
assert.equal(await inverseKey("_c", tree), "c");
|
|
40
40
|
assert.equal(callCount, 3);
|
|
@@ -49,7 +49,7 @@ describe("cachedKeyFunctions", () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
let callCount = 0;
|
|
52
|
-
const addUnderscore = async (sourceKey, tree) => {
|
|
52
|
+
const addUnderscore = async (sourceValue, sourceKey, tree) => {
|
|
53
53
|
callCount++;
|
|
54
54
|
return `_${sourceKey}`;
|
|
55
55
|
};
|
|
@@ -66,12 +66,12 @@ describe("cachedKeyFunctions", () => {
|
|
|
66
66
|
assert.equal(await inverseKey("b/", tree), "b/");
|
|
67
67
|
assert.equal(callCount, 1);
|
|
68
68
|
|
|
69
|
-
assert.equal(await key("a", tree), "_a");
|
|
70
|
-
assert.equal(await key("a", tree), "_a");
|
|
69
|
+
assert.equal(await key(null, "a", tree), "_a");
|
|
70
|
+
assert.equal(await key(null, "a", tree), "_a");
|
|
71
71
|
assert.equal(callCount, 1);
|
|
72
72
|
|
|
73
|
-
assert.equal(await key("b/", tree), "b/");
|
|
74
|
-
assert.equal(await key("b", tree), "b");
|
|
73
|
+
assert.equal(await key(null, "b/", tree), "b/");
|
|
74
|
+
assert.equal(await key(null, "b", tree), "b");
|
|
75
75
|
assert.equal(callCount, 1);
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -79,11 +79,11 @@ describe("cachedKeyFunctions", () => {
|
|
|
79
79
|
const tree = new ObjectTree({
|
|
80
80
|
a: "letter a",
|
|
81
81
|
});
|
|
82
|
-
const addUnderscore = async (sourceKey) => `_${sourceKey}`;
|
|
82
|
+
const addUnderscore = async (sourceValue, sourceKey) => `_${sourceKey}`;
|
|
83
83
|
const { inverseKey, key } = cachedKeyFunctions(addUnderscore);
|
|
84
84
|
|
|
85
|
-
assert.equal(await key("a/", tree), "_a/");
|
|
86
|
-
assert.equal(await key("a", tree), "_a");
|
|
85
|
+
assert.equal(await key(null, "a/", tree), "_a/");
|
|
86
|
+
assert.equal(await key(null, "a", tree), "_a");
|
|
87
87
|
|
|
88
88
|
assert.equal(await inverseKey("_a/", tree), "a/");
|
|
89
89
|
assert.equal(await inverseKey("_a", tree), "a");
|
|
@@ -93,14 +93,14 @@ describe("cachedKeyFunctions", () => {
|
|
|
93
93
|
const tree = new ObjectTree({
|
|
94
94
|
a: "letter a",
|
|
95
95
|
});
|
|
96
|
-
const addUnderscoreAndSlash = async (sourceKey) =>
|
|
96
|
+
const addUnderscoreAndSlash = async (sourceValue, sourceKey) =>
|
|
97
97
|
`_${trailingSlash.remove(sourceKey)}/`;
|
|
98
98
|
const { inverseKey, key } = cachedKeyFunctions(addUnderscoreAndSlash);
|
|
99
99
|
|
|
100
100
|
assert.equal(await inverseKey("_a/", tree), "a");
|
|
101
101
|
assert.equal(await inverseKey("_a", tree), "a");
|
|
102
102
|
|
|
103
|
-
assert.equal(await key("a", tree), "_a/");
|
|
104
|
-
assert.equal(await key("a/", tree), "_a/");
|
|
103
|
+
assert.equal(await key(null, "a", tree), "_a/");
|
|
104
|
+
assert.equal(await key(null, "a/", tree), "_a/");
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, test } from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import FileTree from "../../src/drivers/FileTree.js";
|
|
7
|
+
import { ObjectTree, Tree } from "../../src/internal.js";
|
|
8
|
+
|
|
9
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const tempDirectory = path.join(dirname, "fixtures/temp");
|
|
11
|
+
|
|
12
|
+
describe("clear", () => {
|
|
13
|
+
test("unsets all public keys in an object tree", async () => {
|
|
14
|
+
const tree = new ObjectTree({ a: 1, b: 2, c: 3 });
|
|
15
|
+
await Tree.clear(tree);
|
|
16
|
+
assert.deepEqual(await Tree.plain(tree), {});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("unsets all public keys in a file tree", async () => {
|
|
20
|
+
// Create a temp directory with some files
|
|
21
|
+
await fs.mkdir(tempDirectory, { recursive: true });
|
|
22
|
+
await fs.writeFile(path.join(tempDirectory, "a"), "1");
|
|
23
|
+
await fs.writeFile(path.join(tempDirectory, "b"), "2");
|
|
24
|
+
|
|
25
|
+
const tree = new FileTree(tempDirectory);
|
|
26
|
+
await Tree.clear(tree);
|
|
27
|
+
|
|
28
|
+
const files = await fs.readdir(tempDirectory);
|
|
29
|
+
assert.deepEqual(files, []);
|
|
30
|
+
|
|
31
|
+
// Remove temp directory
|
|
32
|
+
await fs.rm(tempDirectory, { recursive: true });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { DeepObjectTree, Tree } from "../../src/internal.js";
|
|
4
|
+
import deepMap from "../../src/operations/deepMap.js";
|
|
5
|
+
|
|
6
|
+
describe("deepMap", () => {
|
|
7
|
+
test("can map extensions deeply", async () => {
|
|
8
|
+
const treelike = new DeepObjectTree({
|
|
9
|
+
"file1.txt": "will be mapped",
|
|
10
|
+
file2: "won't be mapped",
|
|
11
|
+
"file3.foo": "won't be mapped",
|
|
12
|
+
more: {
|
|
13
|
+
"file4.txt": "will be mapped",
|
|
14
|
+
"file5.bar": "won't be mapped",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
const fixture = await deepMap.call(null, treelike, {
|
|
18
|
+
deep: true,
|
|
19
|
+
extension: ".txt->.upper",
|
|
20
|
+
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
|
|
21
|
+
});
|
|
22
|
+
assert.deepEqual(await Tree.plain(fixture), {
|
|
23
|
+
"file1.upper": "WILL BE MAPPED",
|
|
24
|
+
more: {
|
|
25
|
+
"file4.upper": "WILL BE MAPPED",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import { DeepObjectTree, Tree } from "../../src/internal.js";
|
|
4
|
-
import
|
|
4
|
+
import deepMerge from "../../src/operations/deepMerge.js";
|
|
5
5
|
|
|
6
6
|
describe("mergeDeep", () => {
|
|
7
7
|
test("can merge deep", async () => {
|
|
8
|
-
const fixture =
|
|
8
|
+
const fixture = deepMerge(
|
|
9
9
|
new DeepObjectTree({
|
|
10
10
|
a: {
|
|
11
11
|
b: 0, // Will be obscured by `b` below
|
|
@@ -34,9 +34,5 @@ describe("mergeDeep", () => {
|
|
|
34
34
|
f: 4,
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
|
-
|
|
38
|
-
// Parent of a subvalue is the merged tree
|
|
39
|
-
const a = await fixture.get("a");
|
|
40
|
-
assert.equal(a.parent, fixture);
|
|
41
37
|
});
|
|
42
38
|
});
|