@weborigami/async-tree 0.2.12 → 0.3.1
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 +1 -0
- package/package.json +6 -4
- package/scripts/headlessTest.js +26 -0
- package/shared.js +3 -1
- package/src/drivers/ExplorableSiteTree.js +8 -4
- package/src/drivers/FileTree.js +2 -0
- package/src/drivers/MapTree.js +3 -0
- package/src/operations/addNextPrevious.js +2 -2
- package/src/operations/{concatTrees.js → deepText.js} +1 -1
- package/src/operations/extensionKeyFunctions.js +3 -3
- package/src/operations/filter.js +27 -41
- package/src/operations/globKeys.js +5 -1
- package/src/operations/map.js +39 -10
- package/src/operations/mask.js +55 -0
- package/src/operations/parseExtensions.js +44 -0
- package/test/browser/index.html +29 -6
- package/test/browser/testRunner.js +2 -1
- package/test/drivers/constantTree.test.js +1 -1
- package/test/jsonKeys.test.js +1 -1
- package/test/operations/addNextPrevious.test.js +4 -4
- package/test/operations/{concatTrees.test.js → deepText.test.js} +3 -3
- package/test/operations/filter.test.js +20 -7
- package/test/operations/globKeys.test.js +1 -1
- package/test/operations/map.test.js +49 -1
- package/test/operations/mask.test.js +32 -0
- package/test/operations/parseExtensions.test.js +61 -0
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
+
"dependencies": {
|
|
10
|
+
"@weborigami/types": "0.3.1"
|
|
11
|
+
},
|
|
9
12
|
"devDependencies": {
|
|
10
13
|
"@types/node": "22.13.13",
|
|
14
|
+
"puppeteer": "24.6.1",
|
|
11
15
|
"typescript": "5.8.2"
|
|
12
16
|
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.2.12"
|
|
15
|
-
},
|
|
16
17
|
"scripts": {
|
|
18
|
+
"headlessTest": "node test/browser/headlessTest.js",
|
|
17
19
|
"test": "node --test --test-reporter=spec",
|
|
18
20
|
"typecheck": "tsc"
|
|
19
21
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import puppeteer from "puppeteer";
|
|
2
|
+
|
|
3
|
+
let failed = false;
|
|
4
|
+
|
|
5
|
+
const browser = await puppeteer.launch({
|
|
6
|
+
args: ["--allow-file-access-from-files", "--disable-web-security"],
|
|
7
|
+
});
|
|
8
|
+
const page = await browser.newPage();
|
|
9
|
+
|
|
10
|
+
page.on("console", (msg) => {
|
|
11
|
+
const text = msg.text();
|
|
12
|
+
if (text.includes("❌")) {
|
|
13
|
+
console.error(text);
|
|
14
|
+
failed = true;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// load your local index.html
|
|
19
|
+
// const url = new URL("index.html", import.meta.url);
|
|
20
|
+
const url = "http://localhost:5000/test/browser/index.html";
|
|
21
|
+
await page.goto(url);
|
|
22
|
+
await browser.close();
|
|
23
|
+
if (!failed) {
|
|
24
|
+
console.log("✅ All tests passed");
|
|
25
|
+
}
|
|
26
|
+
process.exit(failed ? 1 : 0);
|
package/shared.js
CHANGED
|
@@ -15,10 +15,10 @@ export { default as addNextPrevious } from "./src/operations/addNextPrevious.js"
|
|
|
15
15
|
export { default as cache } from "./src/operations/cache.js";
|
|
16
16
|
export { default as cachedKeyFunctions } from "./src/operations/cachedKeyFunctions.js";
|
|
17
17
|
export { default as concat } from "./src/operations/concat.js";
|
|
18
|
-
export { default as concatTrees } from "./src/operations/concatTrees.js";
|
|
19
18
|
export { default as deepMerge } from "./src/operations/deepMerge.js";
|
|
20
19
|
export { default as deepReverse } from "./src/operations/deepReverse.js";
|
|
21
20
|
export { default as deepTake } from "./src/operations/deepTake.js";
|
|
21
|
+
export { default as deepText } from "./src/operations/deepText.js";
|
|
22
22
|
export { default as deepValues } from "./src/operations/deepValues.js";
|
|
23
23
|
export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
|
|
24
24
|
export { default as extensionKeyFunctions } from "./src/operations/extensionKeyFunctions.js";
|
|
@@ -26,8 +26,10 @@ export { default as filter } from "./src/operations/filter.js";
|
|
|
26
26
|
export { default as group } from "./src/operations/group.js";
|
|
27
27
|
export { default as invokeFunctions } from "./src/operations/invokeFunctions.js";
|
|
28
28
|
export { default as map } from "./src/operations/map.js";
|
|
29
|
+
export { default as mask } from "./src/operations/mask.js";
|
|
29
30
|
export { default as merge } from "./src/operations/merge.js";
|
|
30
31
|
export { default as paginate } from "./src/operations/paginate.js";
|
|
32
|
+
export { default as parseExtensions } from "./src/operations/parseExtensions.js";
|
|
31
33
|
export { default as reverse } from "./src/operations/reverse.js";
|
|
32
34
|
export { default as scope } from "./src/operations/scope.js";
|
|
33
35
|
export { default as sort } from "./src/operations/sort.js";
|
|
@@ -6,11 +6,17 @@ import SiteTree from "./SiteTree.js";
|
|
|
6
6
|
* route even though such a mechanism is not built into the HTTP protocol.
|
|
7
7
|
*/
|
|
8
8
|
export default class ExplorableSiteTree extends SiteTree {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} href
|
|
11
|
+
*/
|
|
12
|
+
constructor(href) {
|
|
13
|
+
super(href);
|
|
11
14
|
this.serverKeysPromise = undefined;
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @returns {Promise<string[]>}
|
|
19
|
+
*/
|
|
14
20
|
async getServerKeys() {
|
|
15
21
|
// We use a promise to ensure we only check for keys once.
|
|
16
22
|
const href = new URL(".keys.json", this.href).href;
|
|
@@ -32,8 +38,6 @@ export default class ExplorableSiteTree extends SiteTree {
|
|
|
32
38
|
/**
|
|
33
39
|
* Returns the keys of the site route. For this to work, the route must have a
|
|
34
40
|
* `.keys.json` file that contains a JSON array of string keys.
|
|
35
|
-
*
|
|
36
|
-
* @returns {Promise<Iterable<string>>}
|
|
37
41
|
*/
|
|
38
42
|
async keys() {
|
|
39
43
|
const serverKeys = await this.getServerKeys();
|
package/src/drivers/FileTree.js
CHANGED
package/src/drivers/MapTree.js
CHANGED
|
@@ -16,6 +16,9 @@ import { setParent } from "../utilities.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export default class MapTree {
|
|
18
18
|
/**
|
|
19
|
+
* Constructs a new `MapTree` instance. This `iterable` parameter can be a
|
|
20
|
+
* `Map` instance, or any other iterable of key-value pairs.
|
|
21
|
+
*
|
|
19
22
|
* @param {Iterable} [iterable]
|
|
20
23
|
*/
|
|
21
24
|
constructor(iterable = []) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Tree } from "
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
2
|
import { assertIsTreelike } from "../utilities.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Add nextKey/previousKey properties to values.
|
|
6
6
|
*
|
|
7
7
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
-
* @typedef {import("
|
|
8
|
+
* @typedef {import("../../index.ts").PlainObject} PlainObject
|
|
9
9
|
*
|
|
10
10
|
* @param {import("../../index.ts").Treelike} treelike
|
|
11
11
|
* @returns {Promise<PlainObject|Array>}
|
|
@@ -9,7 +9,7 @@ import concat from "./concat.js";
|
|
|
9
9
|
* @param {TemplateStringsArray} strings
|
|
10
10
|
* @param {...any} values
|
|
11
11
|
*/
|
|
12
|
-
export default async function
|
|
12
|
+
export default async function deepText(strings, ...values) {
|
|
13
13
|
// Convert all the values to strings
|
|
14
14
|
const valueTexts = await Promise.all(
|
|
15
15
|
values.map((value) =>
|
|
@@ -19,8 +19,8 @@ export default function extensionKeyFunctions(
|
|
|
19
19
|
resultExtension = sourceExtension;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
checkExtension(resultExtension);
|
|
23
|
+
checkExtension(sourceExtension);
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
async inverseKey(resultKey, tree) {
|
|
@@ -38,7 +38,7 @@ export default function extensionKeyFunctions(
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function
|
|
41
|
+
function checkExtension(extension) {
|
|
42
42
|
if (extension && extension !== "/" && !extension.startsWith(".")) {
|
|
43
43
|
throw new RangeError(
|
|
44
44
|
`The extension "${extension}" must start with a period.`
|
package/src/operations/filter.js
CHANGED
|
@@ -1,53 +1,39 @@
|
|
|
1
|
-
import { assertIsTreelike
|
|
1
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
|
+
import map from "./map.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Given
|
|
5
|
-
*
|
|
6
|
-
* deep: if a value from `a` is a subtree, it will be filtered recursively.
|
|
5
|
+
* Given a tree an a test function, return a new tree whose keys correspond to
|
|
6
|
+
* the values that pass the test function.
|
|
7
7
|
*
|
|
8
8
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
9
9
|
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
10
10
|
*
|
|
11
|
-
* @param {Treelike}
|
|
12
|
-
* @param {
|
|
11
|
+
* @param {Treelike} treelike
|
|
12
|
+
* @param {function|any} options
|
|
13
13
|
* @returns {AsyncTree}
|
|
14
14
|
*/
|
|
15
|
-
export default function filter(
|
|
16
|
-
assertIsTreelike(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export default function filter(treelike, options) {
|
|
16
|
+
assertIsTreelike(treelike, "map");
|
|
17
|
+
let testFn;
|
|
18
|
+
let deep;
|
|
19
|
+
if (typeof options === "function") {
|
|
20
|
+
testFn = options;
|
|
21
|
+
deep = false;
|
|
22
|
+
} else {
|
|
23
|
+
testFn = options.test;
|
|
24
|
+
deep = options.deep ?? false;
|
|
25
|
+
}
|
|
20
26
|
|
|
21
|
-
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
let aValue = await a.get(key);
|
|
29
|
-
if (Tree.isTreelike(aValue)) {
|
|
30
|
-
// Filter the subtree
|
|
31
|
-
return filter(aValue, bValue);
|
|
32
|
-
} else {
|
|
33
|
-
return aValue;
|
|
34
|
-
}
|
|
35
|
-
},
|
|
27
|
+
return map(treelike, {
|
|
28
|
+
deep,
|
|
29
|
+
|
|
30
|
+
// Assume source key is the same as result key
|
|
31
|
+
inverseKey: async (resultKey) => resultKey,
|
|
36
32
|
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
// An async tree value in b implies that the a key should have a slash
|
|
42
|
-
const aKeySlashes = aKeys.map((key, index) =>
|
|
43
|
-
trailingSlash.toggle(
|
|
44
|
-
key,
|
|
45
|
-
trailingSlash.has(key) || Tree.isAsyncTree(bValues[index])
|
|
46
|
-
)
|
|
47
|
-
);
|
|
48
|
-
// Remove keys that don't have values in b
|
|
49
|
-
const keys = aKeySlashes.filter((key, index) => bValues[index] ?? false);
|
|
50
|
-
return keys;
|
|
33
|
+
key: async (sourceKey, tree) => {
|
|
34
|
+
const value = await tree.get(sourceKey);
|
|
35
|
+
const passes = await testFn(value, sourceKey, tree);
|
|
36
|
+
return passes ? sourceKey : undefined;
|
|
51
37
|
},
|
|
52
|
-
};
|
|
38
|
+
});
|
|
53
39
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { ObjectTree, Tree
|
|
1
|
+
import { ObjectTree, Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
4
|
+
import merge from "./merge.js";
|
|
2
5
|
|
|
3
6
|
const globstar = "**";
|
|
4
7
|
const globstarSlash = `${globstar}/`;
|
|
5
8
|
|
|
6
9
|
export default function globKeys(treelike) {
|
|
10
|
+
assertIsTreelike(treelike, "globKeys");
|
|
7
11
|
const globs = Tree.from(treelike, { deep: true });
|
|
8
12
|
return {
|
|
9
13
|
async get(key) {
|
package/src/operations/map.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
3
|
import { assertIsTreelike } from "../utilities.js";
|
|
4
|
+
import cachedKeyFunctions from "./cachedKeyFunctions.js";
|
|
5
|
+
import extensionKeyFunctions from "./extensionKeyFunctions.js";
|
|
6
|
+
import parseExtensions from "./parseExtensions.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Transform the keys and/or values of a tree.
|
|
@@ -115,6 +118,7 @@ export default function map(treelike, options = {}) {
|
|
|
115
118
|
function validateOptions(options) {
|
|
116
119
|
let deep;
|
|
117
120
|
let description;
|
|
121
|
+
let extension;
|
|
118
122
|
let inverseKeyFn;
|
|
119
123
|
let keyFn;
|
|
120
124
|
let needsSourceValue;
|
|
@@ -126,26 +130,51 @@ function validateOptions(options) {
|
|
|
126
130
|
} else {
|
|
127
131
|
deep = options.deep;
|
|
128
132
|
description = options.description;
|
|
133
|
+
extension = options.extension;
|
|
129
134
|
inverseKeyFn = options.inverseKey;
|
|
130
135
|
keyFn = options.key;
|
|
131
136
|
needsSourceValue = options.needsSourceValue;
|
|
132
137
|
valueFn = options.value;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
|
-
|
|
136
|
-
description ??= "key/value map";
|
|
137
|
-
// @ts-ignore
|
|
138
|
-
inverseKeyFn ??= valueFn?.inverseKey;
|
|
139
|
-
// @ts-ignore
|
|
140
|
-
keyFn ??= valueFn?.key;
|
|
141
|
-
needsSourceValue ??= true;
|
|
142
|
-
|
|
143
|
-
if ((keyFn && !inverseKeyFn) || (!keyFn && inverseKeyFn)) {
|
|
140
|
+
if (extension && (keyFn || inverseKeyFn)) {
|
|
144
141
|
throw new TypeError(
|
|
145
|
-
`map: You
|
|
142
|
+
`map: You can't specify extensions and also a key or inverseKey function`
|
|
146
143
|
);
|
|
147
144
|
}
|
|
148
145
|
|
|
146
|
+
if (extension) {
|
|
147
|
+
// Use the extension mapping to generate key and inverseKey functions
|
|
148
|
+
const parsed = parseExtensions(extension);
|
|
149
|
+
const keyFns = extensionKeyFunctions(
|
|
150
|
+
parsed.sourceExtension,
|
|
151
|
+
parsed.resultExtension
|
|
152
|
+
);
|
|
153
|
+
keyFn = keyFns.key;
|
|
154
|
+
inverseKeyFn = keyFns.inverseKey;
|
|
155
|
+
} else {
|
|
156
|
+
// If key or inverseKey weren't specified, look for sidecar functions
|
|
157
|
+
inverseKeyFn ??= valueFn?.inverseKey;
|
|
158
|
+
keyFn ??= valueFn?.key;
|
|
159
|
+
|
|
160
|
+
if (!keyFn && inverseKeyFn) {
|
|
161
|
+
throw new TypeError(
|
|
162
|
+
`map: You can't specify an inverseKey function without a key function`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (keyFn && !inverseKeyFn) {
|
|
167
|
+
// Only keyFn was provided, so we need to generate the inverseKeyFn
|
|
168
|
+
const keyFns = cachedKeyFunctions(keyFn, deep);
|
|
169
|
+
keyFn = keyFns.key;
|
|
170
|
+
inverseKeyFn = keyFns.inverseKey;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
deep ??= false;
|
|
175
|
+
description ??= "key/value map";
|
|
176
|
+
needsSourceValue ??= true;
|
|
177
|
+
|
|
149
178
|
return {
|
|
150
179
|
deep,
|
|
151
180
|
description,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Given trees `a` and `b`, return a masked version of `a` where only the keys
|
|
7
|
+
* that exist in `b` and have truthy values are kept. The filter operation is
|
|
8
|
+
* deep: if a value from `a` is a subtree, it will be filtered recursively.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
11
|
+
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
12
|
+
*
|
|
13
|
+
* @param {Treelike} a
|
|
14
|
+
* @param {Treelike} b
|
|
15
|
+
* @returns {AsyncTree}
|
|
16
|
+
*/
|
|
17
|
+
export default function mask(a, b) {
|
|
18
|
+
assertIsTreelike(a, "filter", 0);
|
|
19
|
+
assertIsTreelike(b, "filter", 1);
|
|
20
|
+
a = Tree.from(a);
|
|
21
|
+
b = Tree.from(b, { deep: true });
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
async get(key) {
|
|
25
|
+
// The key must exist in b and return a truthy value
|
|
26
|
+
const bValue = await b.get(key);
|
|
27
|
+
if (!bValue) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
let aValue = await a.get(key);
|
|
31
|
+
if (Tree.isTreelike(aValue)) {
|
|
32
|
+
// Filter the subtree
|
|
33
|
+
return mask(aValue, bValue);
|
|
34
|
+
} else {
|
|
35
|
+
return aValue;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async keys() {
|
|
40
|
+
// Use a's keys as the basis
|
|
41
|
+
const aKeys = [...(await a.keys())];
|
|
42
|
+
const bValues = await Promise.all(aKeys.map((key) => b.get(key)));
|
|
43
|
+
// An async tree value in b implies that the a key should have a slash
|
|
44
|
+
const aKeySlashes = aKeys.map((key, index) =>
|
|
45
|
+
trailingSlash.toggle(
|
|
46
|
+
key,
|
|
47
|
+
trailingSlash.has(key) || Tree.isAsyncTree(bValues[index])
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
// Remove keys that don't have values in b
|
|
51
|
+
const keys = aKeySlashes.filter((key, index) => bValues[index] ?? false);
|
|
52
|
+
return keys;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a string specifying an extension or a mapping of one extension to another,
|
|
3
|
+
* return the source and result extensions.
|
|
4
|
+
*
|
|
5
|
+
* Syntax:
|
|
6
|
+
* .foo source and result extension are the same
|
|
7
|
+
* .foo→.bar Unicode Rightwards Arrow
|
|
8
|
+
* .foo→ Unicode Rightwards Arrow, no result extension
|
|
9
|
+
* →.bar Unicode Rightwards Arrow, no source extension
|
|
10
|
+
* .foo->.bar hyphen and greater-than sign
|
|
11
|
+
*
|
|
12
|
+
* @param {string} specifier
|
|
13
|
+
*/
|
|
14
|
+
export default function parseExtensions(specifier) {
|
|
15
|
+
const lowercase = specifier?.toLowerCase() ?? "";
|
|
16
|
+
const extensionRegex =
|
|
17
|
+
/^((?<sourceExtension>\/|\.\S*)?\s*(→|->)\s*(?<resultExtension>\/|\.\S*)?)|(?<extension>\/|\.\S*)$/;
|
|
18
|
+
const match = lowercase.match(extensionRegex);
|
|
19
|
+
if (!match) {
|
|
20
|
+
throw new Error(`Invalid file extension specifier "${specifier}".`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
let { extension, resultExtension, sourceExtension } = match.groups;
|
|
25
|
+
if (extension) {
|
|
26
|
+
// foo
|
|
27
|
+
return {
|
|
28
|
+
resultExtension: extension,
|
|
29
|
+
sourceExtension: extension,
|
|
30
|
+
};
|
|
31
|
+
} else {
|
|
32
|
+
// foo→bar
|
|
33
|
+
|
|
34
|
+
if (resultExtension === undefined && sourceExtension === undefined) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`A file extension mapping must indicate a source or result extension: "${specifier}".`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
resultExtension ??= "";
|
|
41
|
+
sourceExtension ??= "";
|
|
42
|
+
return { resultExtension, sourceExtension };
|
|
43
|
+
}
|
|
44
|
+
}
|
package/test/browser/index.html
CHANGED
|
@@ -13,22 +13,45 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
<!-- Omit FileTree.test.js, which is Node.js only -->
|
|
15
15
|
<!-- Omit SiteTree.test.js, which requires mocks -->
|
|
16
|
+
|
|
17
|
+
<!-- Unclear why drivers/constantTree.test.js won't load -->
|
|
18
|
+
|
|
16
19
|
<script type="module" src="../Tree.test.js"></script>
|
|
17
20
|
<script type="module" src="../drivers/BrowserFileTree.test.js"></script>
|
|
21
|
+
<script type="module" src="../drivers/DeepMapTree.test.js"></script>
|
|
22
|
+
<script type="module" src="../drivers/DeepObjectTree.test.js"></script>
|
|
18
23
|
<script type="module" src="../drivers/DeferredTree.test.js"></script>
|
|
19
24
|
<script type="module" src="../drivers/FunctionTree.test.js"></script>
|
|
20
25
|
<script type="module" src="../drivers/MapTree.test.js"></script>
|
|
21
26
|
<script type="module" src="../drivers/ObjectTree.test.js"></script>
|
|
22
27
|
<script type="module" src="../drivers/SetTree.test.js"></script>
|
|
28
|
+
<script type="module" src="../drivers/calendarTree.test.js"></script>
|
|
29
|
+
<script type="module" src="../operations/addNextPrevious.test.js"></script>
|
|
30
|
+
<script type="module" src="../operations/cache.test.js"></script>
|
|
23
31
|
<script type="module" src="../operations/cache.test.js"></script>
|
|
24
|
-
<script type="module" src="../operations/merge.test.js"></script>
|
|
25
|
-
<script type="module" src="../operations/deepMerge.test.js"></script>
|
|
26
32
|
<script type="module" src="../operations/cachedKeyFunctions.test.js"></script>
|
|
27
|
-
<script
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
></script>
|
|
33
|
+
<script type="module" src="../operations/concat.test.js"></script>
|
|
34
|
+
<script type="module" src="../operations/deepMerge.test.js"></script>
|
|
35
|
+
<script type="module" src="../operations/deepReverse.test.js"></script>
|
|
36
|
+
<script type="module" src="../operations/deepTake.test.js"></script>
|
|
37
|
+
<script type="module" src="../operations/deepText.test.js"></script>
|
|
38
|
+
<script type="module" src="../operations/deepValues.test.js"></script>
|
|
39
|
+
<script type="module" src="../operations/deepValuesIterator.test.js"></script>
|
|
40
|
+
<script type="module" src="../operations/extensionKeyFunctions.test.js"></script>
|
|
41
|
+
<script type="module" src="../operations/filter.test.js"></script>
|
|
42
|
+
<script type="module" src="../operations/globKeys.test.js"></script>
|
|
43
|
+
<script type="module" src="../operations/group.test.js"></script>
|
|
44
|
+
<script type="module" src="../operations/invokeFunctions.test.js"></script>
|
|
31
45
|
<script type="module" src="../operations/map.test.js"></script>
|
|
46
|
+
<script type="module" src="../operations/mask.test.js"></script>
|
|
47
|
+
<script type="module" src="../operations/merge.test.js"></script>
|
|
48
|
+
<script type="module" src="../operations/paginate.test.js"></script>
|
|
49
|
+
<script type="module" src="../operations/parseExtensions.test.js"></script>
|
|
50
|
+
<script type="module" src="../operations/regExpKeys.test.js"></script>
|
|
51
|
+
<script type="module" src="../operations/reverse.test.js"></script>
|
|
52
|
+
<script type="module" src="../operations/scope.test.js"></script>
|
|
53
|
+
<script type="module" src="../operations/sort.test.js"></script>
|
|
54
|
+
<script type="module" src="../operations/take.test.js"></script>
|
|
32
55
|
<script type="module" src="../utilities.test.js"></script>
|
|
33
56
|
</head>
|
|
34
57
|
<body></body>
|
|
@@ -25,7 +25,8 @@ export async function describe(name, fn) {
|
|
|
25
25
|
const name = result.name;
|
|
26
26
|
const message = result.result === "fail" ? `: ${result.message}` : "";
|
|
27
27
|
const skipped = result.result === "skipped" ? " [skipped]" : "";
|
|
28
|
-
|
|
28
|
+
const fn = result.result === "fail" ? "error" : "log";
|
|
29
|
+
console[fn](`${marker} ${name}${message}${skipped}`);
|
|
29
30
|
}
|
|
30
31
|
console.groupEnd();
|
|
31
32
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Tree } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
4
3
|
import constantTree from "../../src/drivers/constantTree.js";
|
|
4
|
+
import { Tree } from "../../src/internal.js";
|
|
5
5
|
|
|
6
6
|
describe("constantTree", () => {
|
|
7
7
|
test("returns a deep tree that returns constant for all keys", async () => {
|
package/test/jsonKeys.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DeepObjectTree } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { DeepObjectTree } from "../src/internal.js";
|
|
4
4
|
import * as jsonKeys from "../src/jsonKeys.js";
|
|
5
5
|
|
|
6
6
|
describe("jsonKeys", () => {
|
|
@@ -39,16 +39,16 @@ describe("addNextPrevious", () => {
|
|
|
39
39
|
assert.deepEqual(result, [
|
|
40
40
|
{
|
|
41
41
|
value: "Alice",
|
|
42
|
-
nextKey: 1,
|
|
42
|
+
nextKey: "1",
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
value: "Bob",
|
|
46
|
-
nextKey: 2,
|
|
47
|
-
previousKey: 0,
|
|
46
|
+
nextKey: "2",
|
|
47
|
+
previousKey: "0",
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
value: "Carol",
|
|
51
|
-
previousKey: 1,
|
|
51
|
+
previousKey: "1",
|
|
52
52
|
},
|
|
53
53
|
]);
|
|
54
54
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import
|
|
3
|
+
import deepText from "../../src/operations/deepText.js";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
5
|
+
describe("deepText", () => {
|
|
6
6
|
test("joins strings and values together", async () => {
|
|
7
7
|
const array = [1, 2, 3];
|
|
8
8
|
const object = { person1: "Alice", person2: "Bob" };
|
|
9
|
-
const result = await
|
|
9
|
+
const result = await deepText`a ${array} b ${object} c`;
|
|
10
10
|
assert.equal(result, "a 123 b AliceBob c");
|
|
11
11
|
});
|
|
12
12
|
});
|
|
@@ -4,8 +4,24 @@ import { Tree } from "../../src/internal.js";
|
|
|
4
4
|
import filter from "../../src/operations/filter.js";
|
|
5
5
|
|
|
6
6
|
describe("filter", () => {
|
|
7
|
-
test("
|
|
8
|
-
const result = filter(
|
|
7
|
+
test("returns values that pass a filter function", async () => {
|
|
8
|
+
const result = await filter(
|
|
9
|
+
{
|
|
10
|
+
a: 1,
|
|
11
|
+
b: 2,
|
|
12
|
+
c: 3,
|
|
13
|
+
d: 4,
|
|
14
|
+
},
|
|
15
|
+
(value) => value % 2 === 1 // odd
|
|
16
|
+
);
|
|
17
|
+
assert.deepEqual(await Tree.plain(result), {
|
|
18
|
+
a: 1,
|
|
19
|
+
c: 3,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns deep values that pass a filter function", async () => {
|
|
24
|
+
const result = await filter(
|
|
9
25
|
{
|
|
10
26
|
a: 1,
|
|
11
27
|
b: 2,
|
|
@@ -15,13 +31,10 @@ describe("filter", () => {
|
|
|
15
31
|
},
|
|
16
32
|
},
|
|
17
33
|
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
d: true,
|
|
21
|
-
},
|
|
34
|
+
deep: true,
|
|
35
|
+
test: (value) => value % 2 === 1, // odd
|
|
22
36
|
}
|
|
23
37
|
);
|
|
24
|
-
assert.deepEqual(await result.keys(), ["a", "c/"]);
|
|
25
38
|
assert.deepEqual(await Tree.plain(result), {
|
|
26
39
|
a: 1,
|
|
27
40
|
c: {
|
|
@@ -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 globKeys from "../../src/operations/globKeys.js";
|
|
5
5
|
|
|
6
6
|
describe("globKeys", () => {
|
|
@@ -68,6 +68,20 @@ describe("map", () => {
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
test("if only given a key, will generate an inverseKey", async () => {
|
|
72
|
+
const tree = {
|
|
73
|
+
a: "letter a",
|
|
74
|
+
b: "letter b",
|
|
75
|
+
};
|
|
76
|
+
const underscoreKeys = map(tree, {
|
|
77
|
+
key: addUnderscore,
|
|
78
|
+
});
|
|
79
|
+
assert.deepEqual(await Tree.plain(underscoreKeys), {
|
|
80
|
+
_a: "letter a",
|
|
81
|
+
_b: "letter b",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
71
85
|
test("maps keys and values", async () => {
|
|
72
86
|
const tree = {
|
|
73
87
|
a: "letter a",
|
|
@@ -102,7 +116,7 @@ describe("map", () => {
|
|
|
102
116
|
});
|
|
103
117
|
});
|
|
104
118
|
|
|
105
|
-
test("value can provide a default key and inverse key functions", async () => {
|
|
119
|
+
test("value can provide a default key and inverse key sidecar functions", async () => {
|
|
106
120
|
const uppercase = (s) => s.toUpperCase();
|
|
107
121
|
uppercase.key = addUnderscore;
|
|
108
122
|
uppercase.inverseKey = removeUnderscore;
|
|
@@ -177,6 +191,40 @@ describe("map", () => {
|
|
|
177
191
|
});
|
|
178
192
|
});
|
|
179
193
|
|
|
194
|
+
test("can change a key's extension", async () => {
|
|
195
|
+
const treelike = {
|
|
196
|
+
"file1.lower": "will be mapped",
|
|
197
|
+
file2: "won't be mapped",
|
|
198
|
+
"file3.foo": "won't be mapped",
|
|
199
|
+
};
|
|
200
|
+
const fixture = await map(treelike, {
|
|
201
|
+
extension: ".lower->.upper",
|
|
202
|
+
value: (sourceValue) => sourceValue.toUpperCase(),
|
|
203
|
+
});
|
|
204
|
+
assert.deepEqual(await Tree.plain(fixture), {
|
|
205
|
+
"file1.upper": "WILL BE MAPPED",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("can manipulate extensions deeply", async () => {
|
|
210
|
+
const treelike = {
|
|
211
|
+
"file1.txt": 1,
|
|
212
|
+
more: {
|
|
213
|
+
"file2.txt": 2,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
const fixture = await map(treelike, {
|
|
217
|
+
deep: true,
|
|
218
|
+
extension: ".txt->",
|
|
219
|
+
});
|
|
220
|
+
assert.deepEqual(await Tree.plain(fixture), {
|
|
221
|
+
file1: 1,
|
|
222
|
+
more: {
|
|
223
|
+
file2: 2,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
180
228
|
test("needsSourceValue can be set to false in cases where the value isn't necessary", async () => {
|
|
181
229
|
let flag = false;
|
|
182
230
|
const tree = new FunctionTree(() => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
|
+
import mask from "../../src/operations/mask.js";
|
|
5
|
+
|
|
6
|
+
describe("mask", () => {
|
|
7
|
+
test("removes keys and values whose mask values are falsy", async () => {
|
|
8
|
+
const result = mask(
|
|
9
|
+
{
|
|
10
|
+
a: 1,
|
|
11
|
+
b: 2,
|
|
12
|
+
c: {
|
|
13
|
+
d: 3,
|
|
14
|
+
e: 4,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
a: true,
|
|
19
|
+
c: {
|
|
20
|
+
d: true,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
assert.deepEqual(await result.keys(), ["a", "c/"]);
|
|
25
|
+
assert.deepEqual(await Tree.plain(result), {
|
|
26
|
+
a: 1,
|
|
27
|
+
c: {
|
|
28
|
+
d: 3,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import parseExtensions from "../../src/operations/parseExtensions.js";
|
|
4
|
+
|
|
5
|
+
describe("keyMapsForExtensions", () => {
|
|
6
|
+
test("source and result extension are the same", async () => {
|
|
7
|
+
assert.deepEqual(parseExtensions(".foo"), {
|
|
8
|
+
sourceExtension: ".foo",
|
|
9
|
+
resultExtension: ".foo",
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("change extension", async () => {
|
|
14
|
+
assert.deepEqual(parseExtensions(".foo->.bar"), {
|
|
15
|
+
sourceExtension: ".foo",
|
|
16
|
+
resultExtension: ".bar",
|
|
17
|
+
});
|
|
18
|
+
// with Unicode Rightwards Arrow
|
|
19
|
+
assert.deepEqual(parseExtensions(".foo→.bar"), {
|
|
20
|
+
sourceExtension: ".foo",
|
|
21
|
+
resultExtension: ".bar",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("add extension", async () => {
|
|
26
|
+
assert.deepEqual(parseExtensions("->.foo"), {
|
|
27
|
+
sourceExtension: "",
|
|
28
|
+
resultExtension: ".foo",
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(parseExtensions("→.foo"), {
|
|
31
|
+
sourceExtension: "",
|
|
32
|
+
resultExtension: ".foo",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("remove extension", async () => {
|
|
37
|
+
assert.deepEqual(parseExtensions(".foo->"), {
|
|
38
|
+
sourceExtension: ".foo",
|
|
39
|
+
resultExtension: "",
|
|
40
|
+
});
|
|
41
|
+
assert.deepEqual(parseExtensions(".foo→"), {
|
|
42
|
+
sourceExtension: ".foo",
|
|
43
|
+
resultExtension: "",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("slash is a valid extension", async () => {
|
|
48
|
+
assert.deepEqual(parseExtensions("/"), {
|
|
49
|
+
sourceExtension: "/",
|
|
50
|
+
resultExtension: "/",
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(parseExtensions(".foo->/"), {
|
|
53
|
+
sourceExtension: ".foo",
|
|
54
|
+
resultExtension: "/",
|
|
55
|
+
});
|
|
56
|
+
assert.deepEqual(parseExtensions("/->.bar"), {
|
|
57
|
+
sourceExtension: "/",
|
|
58
|
+
resultExtension: ".bar",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|