@weborigami/async-tree 0.2.11 → 0.2.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "5.8.2"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.2.11"
14
+ "@weborigami/types": "0.2.12"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
package/shared.js CHANGED
@@ -11,20 +11,23 @@ export { default as SetTree } from "./src/drivers/SetTree.js";
11
11
  export { default as SiteTree } from "./src/drivers/SiteTree.js";
12
12
  export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
13
13
  export * as jsonKeys from "./src/jsonKeys.js";
14
+ export { default as addNextPrevious } from "./src/operations/addNextPrevious.js";
14
15
  export { default as cache } from "./src/operations/cache.js";
15
16
  export { default as cachedKeyFunctions } from "./src/operations/cachedKeyFunctions.js";
16
17
  export { default as concat } from "./src/operations/concat.js";
18
+ export { default as concatTrees } from "./src/operations/concatTrees.js";
17
19
  export { default as deepMerge } from "./src/operations/deepMerge.js";
18
20
  export { default as deepReverse } from "./src/operations/deepReverse.js";
19
21
  export { default as deepTake } from "./src/operations/deepTake.js";
20
22
  export { default as deepValues } from "./src/operations/deepValues.js";
21
23
  export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
24
+ export { default as extensionKeyFunctions } from "./src/operations/extensionKeyFunctions.js";
22
25
  export { default as filter } from "./src/operations/filter.js";
23
26
  export { default as group } from "./src/operations/group.js";
24
27
  export { default as invokeFunctions } from "./src/operations/invokeFunctions.js";
25
- export { default as keyFunctionsForExtensions } from "./src/operations/keyFunctionsForExtensions.js";
26
28
  export { default as map } from "./src/operations/map.js";
27
29
  export { default as merge } from "./src/operations/merge.js";
30
+ export { default as paginate } from "./src/operations/paginate.js";
28
31
  export { default as reverse } from "./src/operations/reverse.js";
29
32
  export { default as scope } from "./src/operations/scope.js";
30
33
  export { default as sort } from "./src/operations/sort.js";
@@ -0,0 +1,56 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { assertIsTreelike } from "../utilities.js";
3
+
4
+ /**
5
+ * Add nextKey/previousKey properties to values.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
9
+ *
10
+ * @param {import("../../index.ts").Treelike} treelike
11
+ * @returns {Promise<PlainObject|Array>}
12
+ */
13
+ export default async function addNextPrevious(treelike) {
14
+ assertIsTreelike(treelike, "addNextPrevious");
15
+ const tree = Tree.from(treelike);
16
+
17
+ const entries = [...(await Tree.entries(tree))];
18
+ const keys = entries.map(([key]) => key);
19
+
20
+ // Map to an array of [key, result] pairs, where the result includes
21
+ // nextKey/previousKey properties.
22
+ const mappedEntries = await Promise.all(
23
+ entries.map(async ([key, value], index) => {
24
+ let resultValue;
25
+ if (value === undefined) {
26
+ resultValue = undefined;
27
+ } else if (Tree.isTreelike(value)) {
28
+ resultValue = await Tree.plain(value);
29
+ } else if (typeof value === "object") {
30
+ // Clone value to avoid modifying the original object
31
+ resultValue = { ...value };
32
+ } else {
33
+ // Take the object as the `value` property
34
+ resultValue = { value };
35
+ }
36
+
37
+ if (resultValue) {
38
+ // Extend result with nextKey/previousKey properties.
39
+ const nextKey = keys[index + 1];
40
+ if (nextKey) {
41
+ resultValue.nextKey = nextKey;
42
+ }
43
+ const previousKey = keys[index - 1];
44
+ if (previousKey) {
45
+ resultValue.previousKey = previousKey;
46
+ }
47
+ }
48
+
49
+ return [key, resultValue];
50
+ })
51
+ );
52
+
53
+ return treelike instanceof Array
54
+ ? mappedEntries.map(([_, value]) => value)
55
+ : Object.fromEntries(mappedEntries);
56
+ }
@@ -1,4 +1,5 @@
1
1
  import { ObjectTree, Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Caches values from a source tree in a second cache tree. Cache source tree
@@ -15,12 +16,7 @@ import { ObjectTree, Tree } from "../internal.js";
15
16
  * @returns {AsyncTree & { description: string }}
16
17
  */
17
18
  export default function treeCache(sourceTreelike, cacheTreelike) {
18
- if (!sourceTreelike) {
19
- const error = new TypeError(`cache: The source tree isn't defined.`);
20
- /** @type {any} */ (error).position = 0;
21
- throw error;
22
- }
23
-
19
+ assertIsTreelike(sourceTreelike, "cache");
24
20
  const source = Tree.from(sourceTreelike);
25
21
 
26
22
  /** @type {AsyncMutableTree} */
@@ -1,6 +1,9 @@
1
1
  import * as trailingSlash from "../trailingSlash.js";
2
2
 
3
- const treeToCaches = new Map();
3
+ // For each (tree, keyFn) combination, we maintain a cache mapping a source key to
4
+ // a result key and vice versa. We have to maintain three levels of Map: tree ->
5
+ // keyFn -> sourceKey -> resultKey. This is the top level map.
6
+ const treeMap = new Map();
4
7
 
5
8
  /**
6
9
  * Given a key function, return a new key function and inverse key function that
@@ -19,7 +22,7 @@ export default function cachedKeyFunctions(keyFn, deep = false) {
19
22
  return {
20
23
  async inverseKey(resultKey, tree) {
21
24
  const { resultKeyToSourceKey, sourceKeyToResultKey } =
22
- getKeyMapsForTree(tree);
25
+ getKeyMapsForTreeKeyFn(tree, keyFn);
23
26
 
24
27
  const cachedSourceKey = searchKeyMap(resultKeyToSourceKey, resultKey);
25
28
  if (cachedSourceKey !== undefined) {
@@ -57,7 +60,7 @@ export default function cachedKeyFunctions(keyFn, deep = false) {
57
60
  },
58
61
 
59
62
  async key(sourceKey, tree) {
60
- const { sourceKeyToResultKey } = getKeyMapsForTree(tree);
63
+ const { sourceKeyToResultKey } = getKeyMapsForTreeKeyFn(tree, keyFn);
61
64
 
62
65
  const cachedResultKey = searchKeyMap(sourceKeyToResultKey, sourceKey);
63
66
  if (cachedResultKey !== undefined) {
@@ -76,8 +79,10 @@ export default function cachedKeyFunctions(keyFn, deep = false) {
76
79
  }
77
80
 
78
81
  async function computeAndCacheResultKey(tree, keyFn, deep, sourceKey) {
79
- const { resultKeyToSourceKey, sourceKeyToResultKey } =
80
- getKeyMapsForTree(tree);
82
+ const { resultKeyToSourceKey, sourceKeyToResultKey } = getKeyMapsForTreeKeyFn(
83
+ tree,
84
+ keyFn
85
+ );
81
86
 
82
87
  const resultKey =
83
88
  deep && trailingSlash.has(sourceKey)
@@ -90,17 +95,26 @@ async function computeAndCacheResultKey(tree, keyFn, deep, sourceKey) {
90
95
  return resultKey;
91
96
  }
92
97
 
93
- // Maintain key->inverseKey and inverseKey->key mappings for each tree. These
94
- // store subtree keys in either direction with a trailing slash.
95
- function getKeyMapsForTree(tree) {
96
- let keyMaps = treeToCaches.get(tree);
98
+ // Maintain key->inverseKey and inverseKey->key mappings for each (tree, keyFn)
99
+ // pair. These store subtree keys in either direction with a trailing slash.
100
+ function getKeyMapsForTreeKeyFn(tree, keyFn) {
101
+ // Check if we already have a cache for this tree
102
+ let keyFnMap = treeMap.get(tree);
103
+ if (!keyFnMap) {
104
+ keyFnMap = new Map();
105
+ treeMap.set(tree, keyFnMap);
106
+ }
107
+
108
+ // Check if we have a cache for this keyFn
109
+ let keyMaps = keyFnMap.get(keyFn);
97
110
  if (!keyMaps) {
98
111
  keyMaps = {
99
112
  resultKeyToSourceKey: new Map(),
100
113
  sourceKeyToResultKey: new Map(),
101
114
  };
102
- treeToCaches.set(tree, keyMaps);
115
+ keyFnMap.set(keyFn, keyMaps);
103
116
  }
117
+
104
118
  return keyMaps;
105
119
  }
106
120
 
@@ -1,20 +1,13 @@
1
- import { toString } from "../utilities.js";
1
+ import { assertIsTreelike, toString } from "../utilities.js";
2
2
  import deepValuesIterator from "./deepValuesIterator.js";
3
3
 
4
4
  /**
5
5
  * Concatenate the deep text values in a tree.
6
6
  *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
- *
9
- * @this {AsyncTree|null}
10
7
  * @param {import("../../index.ts").Treelike} treelike
11
8
  */
12
- export default async function concatTreeValues(treelike) {
13
- if (!treelike) {
14
- const error = new TypeError(`concat: The tree isn't defined.`);
15
- /** @type {any} */ (error).position = 0;
16
- throw error;
17
- }
9
+ export default async function concat(treelike) {
10
+ assertIsTreelike(treelike, "concat");
18
11
 
19
12
  const strings = [];
20
13
  for await (const value of deepValuesIterator(treelike, { expand: true })) {
@@ -0,0 +1,25 @@
1
+ import { Tree } from "../internal.js";
2
+ import { toString } from "../utilities.js";
3
+ import concat from "./concat.js";
4
+
5
+ /**
6
+ * A tagged template literal function that concatenate the deep text values in a
7
+ * tree. Any treelike values will be concatenated using `concat`.
8
+ *
9
+ * @param {TemplateStringsArray} strings
10
+ * @param {...any} values
11
+ */
12
+ export default async function concatTrees(strings, ...values) {
13
+ // Convert all the values to strings
14
+ const valueTexts = await Promise.all(
15
+ values.map((value) =>
16
+ Tree.isTreelike(value) ? concat(value) : toString(value)
17
+ )
18
+ );
19
+ // Splice all the strings together
20
+ let result = strings[0];
21
+ for (let i = 0; i < valueTexts.length; i++) {
22
+ result += valueTexts[i] + strings[i + 1];
23
+ }
24
+ return result;
25
+ }
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Reverse the order of keys at all levels of the tree.
@@ -10,13 +11,7 @@ import { Tree } from "../internal.js";
10
11
  * @returns {AsyncTree}
11
12
  */
12
13
  export default function deepReverse(treelike) {
13
- if (!treelike) {
14
- const error = new TypeError(
15
- `deepReverse: The tree to reverse isn't defined.`
16
- );
17
- /** @type {any} */ (error).position = 0;
18
- throw error;
19
- }
14
+ assertIsTreelike(treelike, "deepReverse");
20
15
 
21
16
  const tree = Tree.from(treelike, { deep: true });
22
17
  return {
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Returns a function that traverses a tree deeply and returns the values of the
@@ -11,13 +12,9 @@ import { Tree } from "../internal.js";
11
12
  * @param {number} count
12
13
  */
13
14
  export default async function deepTake(treelike, count) {
14
- if (!treelike) {
15
- const error = new TypeError(`deepTake: The tree isn't defined.`);
16
- /** @type {any} */ (error).position = 0;
17
- throw error;
18
- }
19
-
15
+ assertIsTreelike(treelike, "deepTake");
20
16
  const tree = await Tree.from(treelike, { deep: true });
17
+
21
18
  const { values } = await traverse(tree, count);
22
19
  return Tree.from(values, { deep: true });
23
20
  }
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Return an iterator that yields all values in a tree, including nested trees.
@@ -14,13 +15,9 @@ export default async function* deepValuesIterator(
14
15
  treelike,
15
16
  options = { expand: false }
16
17
  ) {
17
- if (!treelike) {
18
- const error = new TypeError(`deepValues: The tree isn't defined.`);
19
- /** @type {any} */ (error).position = 0;
20
- throw error;
21
- }
22
-
18
+ assertIsTreelike(treelike, "deepValuesIterator");
23
19
  const tree = Tree.from(treelike, { deep: true });
20
+
24
21
  for (const key of await tree.keys()) {
25
22
  let value = await tree.get(key);
26
23
 
@@ -8,13 +8,13 @@ import * as trailingSlash from "../trailingSlash.js";
8
8
  * The resulting `inverseKey` and `key` functions are compatible with those
9
9
  * expected by map and other transforms.
10
10
  *
11
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
12
- * @param {{ resultExtension?: string, sourceExtension: string }} options
11
+ * @param {string} sourceExtension
12
+ * @param {string} [resultExtension]
13
13
  */
14
- export default function keyFunctionsForExtensions({
15
- resultExtension,
14
+ export default function extensionKeyFunctions(
16
15
  sourceExtension,
17
- }) {
16
+ resultExtension
17
+ ) {
18
18
  if (resultExtension === undefined) {
19
19
  resultExtension = sourceExtension;
20
20
  }
@@ -1,4 +1,4 @@
1
- import { trailingSlash, Tree } from "@weborigami/async-tree";
1
+ import { assertIsTreelike, trailingSlash, Tree } from "@weborigami/async-tree";
2
2
 
3
3
  /**
4
4
  * Given trees `a` and `b`, return a filtered version of `a` where only the keys
@@ -13,6 +13,8 @@ import { trailingSlash, Tree } from "@weborigami/async-tree";
13
13
  * @returns {AsyncTree}
14
14
  */
15
15
  export default function filter(a, b) {
16
+ assertIsTreelike(a, "filter", 0);
17
+ assertIsTreelike(b, "filter", 1);
16
18
  a = Tree.from(a);
17
19
  b = Tree.from(b, { deep: true });
18
20
 
@@ -1,4 +1,5 @@
1
1
  import { ObjectTree, Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Given a function that returns a grouping key for a value, returns a transform
@@ -8,12 +9,7 @@ import { ObjectTree, Tree } from "../internal.js";
8
9
  * @param {import("../../index.ts").ValueKeyFn} groupKeyFn
9
10
  */
10
11
  export default async function group(treelike, groupKeyFn) {
11
- if (!treelike) {
12
- const error = new TypeError(`groupBy: The tree to group isn't defined.`);
13
- /** @type {any} */ (error).position = 0;
14
- throw error;
15
- }
16
-
12
+ assertIsTreelike(treelike, "group");
17
13
  const tree = Tree.from(treelike);
18
14
 
19
15
  const keys = Array.from(await tree.keys());
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "../internal.js";
2
2
  import * as trailingSlash from "../trailingSlash.js";
3
+ import { assertIsTreelike } from "../utilities.js";
3
4
 
4
5
  /**
5
6
  * Transform the keys and/or values of a tree.
@@ -12,44 +13,9 @@ import * as trailingSlash from "../trailingSlash.js";
12
13
  * @param {MapOptions|ValueKeyFn} options
13
14
  */
14
15
  export default function map(treelike, options = {}) {
15
- let deep;
16
- let description;
17
- let inverseKeyFn;
18
- let keyFn;
19
- let needsSourceValue;
20
- let valueFn;
21
-
22
- if (!treelike) {
23
- const error = new TypeError(`map: The tree to map isn't defined.`);
24
- /** @type {any} */ (error).position = 0;
25
- throw error;
26
- }
27
-
28
- if (typeof options === "function") {
29
- // Take the single function argument as the valueFn
30
- valueFn = options;
31
- } else {
32
- deep = options.deep;
33
- description = options.description;
34
- inverseKeyFn = options.inverseKey;
35
- keyFn = options.key;
36
- needsSourceValue = options.needsSourceValue;
37
- valueFn = options.value;
38
- }
39
-
40
- deep ??= false;
41
- description ??= "key/value map";
42
- // @ts-ignore
43
- inverseKeyFn ??= valueFn?.inverseKey;
44
- // @ts-ignore
45
- keyFn ??= valueFn?.key;
46
- needsSourceValue ??= true;
47
-
48
- if ((keyFn && !inverseKeyFn) || (!keyFn && inverseKeyFn)) {
49
- throw new TypeError(
50
- `map: You must specify both key and inverseKey functions, or neither.`
51
- );
52
- }
16
+ assertIsTreelike(treelike, "map");
17
+ const { deep, description, inverseKeyFn, keyFn, needsSourceValue, valueFn } =
18
+ validateOptions(options);
53
19
 
54
20
  /**
55
21
  * @param {import("@weborigami/types").AsyncTree} tree
@@ -144,3 +110,48 @@ export default function map(treelike, options = {}) {
144
110
  const tree = Tree.from(treelike, { deep });
145
111
  return mapFn(tree);
146
112
  }
113
+
114
+ // Extract and validate options
115
+ function validateOptions(options) {
116
+ let deep;
117
+ let description;
118
+ let inverseKeyFn;
119
+ let keyFn;
120
+ let needsSourceValue;
121
+ let valueFn;
122
+
123
+ if (typeof options === "function") {
124
+ // Take the single function argument as the valueFn
125
+ valueFn = options;
126
+ } else {
127
+ deep = options.deep;
128
+ description = options.description;
129
+ inverseKeyFn = options.inverseKey;
130
+ keyFn = options.key;
131
+ needsSourceValue = options.needsSourceValue;
132
+ valueFn = options.value;
133
+ }
134
+
135
+ deep ??= false;
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)) {
144
+ throw new TypeError(
145
+ `map: You must specify both key and inverseKey functions, or neither.`
146
+ );
147
+ }
148
+
149
+ return {
150
+ deep,
151
+ description,
152
+ inverseKeyFn,
153
+ keyFn,
154
+ needsSourceValue,
155
+ valueFn,
156
+ };
157
+ }
@@ -0,0 +1,56 @@
1
+ import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
3
+
4
+ /**
5
+ * Return a new grouping of the treelike's values into chunks of the specified
6
+ * size.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {import("../../index.ts").Treelike} Treelike
10
+ *
11
+ * @param {Treelike} [treelike]
12
+ * @param {number} [size=10]
13
+ */
14
+ export default async function paginate(treelike, size = 10) {
15
+ assertIsTreelike(treelike, "paginate");
16
+ const tree = Tree.from(treelike);
17
+
18
+ const keys = Array.from(await tree.keys());
19
+ const pageCount = Math.ceil(keys.length / size);
20
+
21
+ const paginated = {
22
+ async get(pageKey) {
23
+ // Note: page numbers are 1-based.
24
+ const pageNumber = Number(pageKey);
25
+ if (Number.isNaN(pageNumber)) {
26
+ return undefined;
27
+ }
28
+ const nextPage = pageNumber + 1 <= pageCount ? pageNumber + 1 : null;
29
+ const previousPage = pageNumber - 1 >= 1 ? pageNumber - 1 : null;
30
+ const items = {};
31
+ for (
32
+ let index = (pageNumber - 1) * size;
33
+ index < Math.min(keys.length, pageNumber * size);
34
+ index++
35
+ ) {
36
+ const key = keys[index];
37
+ items[key] = await tree.get(keys[index]);
38
+ }
39
+
40
+ return {
41
+ items,
42
+ nextPage,
43
+ pageCount,
44
+ pageNumber,
45
+ previousPage,
46
+ };
47
+ },
48
+
49
+ async keys() {
50
+ // Return an array from 1..totalPages
51
+ return Array.from({ length: pageCount }, (_, index) => index + 1);
52
+ },
53
+ };
54
+
55
+ return paginated;
56
+ }
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "../internal.js";
2
2
  import * as trailingSlash from "../trailingSlash.js";
3
+ import { assertIsTreelike } from "../utilities.js";
3
4
 
4
5
  /**
5
6
  * A tree whose keys are strings interpreted as regular expressions.
@@ -12,15 +13,9 @@ import * as trailingSlash from "../trailingSlash.js";
12
13
  * @type {import("../../index.ts").TreeTransform}
13
14
  */
14
15
  export default async function regExpKeys(treelike) {
15
- if (!treelike) {
16
- const error = new TypeError(
17
- `regExpKeys: The tree of regular expressions isn't defined.`
18
- );
19
- /** @type {any} */ (error).position = 0;
20
- throw error;
21
- }
22
-
16
+ assertIsTreelike(treelike, "regExpKeys");
23
17
  const tree = Tree.from(treelike);
18
+
24
19
  const map = new Map();
25
20
 
26
21
  // We build the output tree first so that we can refer to it when setting
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Reverse the order of the top-level keys in the tree.
@@ -10,13 +11,9 @@ import { Tree } from "../internal.js";
10
11
  * @returns {AsyncTree}
11
12
  */
12
13
  export default function reverse(treelike) {
13
- if (!treelike) {
14
- const error = new TypeError(`reverse: The tree to reverse isn't defined.`);
15
- /** @type {any} */ (error).position = 0;
16
- throw error;
17
- }
18
-
14
+ assertIsTreelike(treelike, "reverse");
19
15
  const tree = Tree.from(treelike);
16
+
20
17
  return {
21
18
  async get(key) {
22
19
  return tree.get(key);
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * A tree's "scope" is the collection of everything in that tree and all of its
@@ -11,12 +12,7 @@ import { Tree } from "../internal.js";
11
12
  * @returns {AsyncTree & {trees: AsyncTree[]}}
12
13
  */
13
14
  export default function scope(treelike) {
14
- if (!treelike) {
15
- const error = new TypeError(`scope: The tree isn't defined.`);
16
- /** @type {any} */ (error).position = 0;
17
- throw error;
18
- }
19
-
15
+ assertIsTreelike(treelike, "scope");
20
16
  const tree = Tree.from(treelike);
21
17
 
22
18
  return {
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Return a new tree with the original's keys sorted. A comparison function can
@@ -14,16 +15,12 @@ import { Tree } from "../internal.js";
14
15
  * @param {SortOptions} [options]
15
16
  */
16
17
  export default function sort(treelike, options) {
17
- if (!treelike) {
18
- const error = new TypeError(`sort: The tree to sort isn't defined.`);
19
- /** @type {any} */ (error).position = 0;
20
- throw error;
21
- }
18
+ assertIsTreelike(treelike, "sort");
19
+ const tree = Tree.from(treelike);
22
20
 
23
21
  const sortKey = options?.sortKey;
24
22
  let compare = options?.compare;
25
23
 
26
- const tree = Tree.from(treelike);
27
24
  const transformed = Object.create(tree);
28
25
  transformed.keys = async () => {
29
26
  const keys = Array.from(await tree.keys());
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import { assertIsTreelike } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Returns a new tree with the number of keys limited to the indicated count.
@@ -7,12 +8,7 @@ import { Tree } from "../internal.js";
7
8
  * @param {number} count
8
9
  */
9
10
  export default function take(treelike, count) {
10
- if (!treelike) {
11
- const error = new TypeError(`take: The tree to take from isn't defined.`);
12
- /** @type {any} */ (error).position = 0;
13
- throw error;
14
- }
15
-
11
+ assertIsTreelike(treelike, "take");
16
12
  const tree = Tree.from(treelike);
17
13
 
18
14
  return {
@@ -1,6 +1,7 @@
1
1
  import { AsyncTree } from "@weborigami/types";
2
2
  import { Packed, PlainObject, StringLike } from "../index.ts";
3
3
 
4
+ export function assertIsTreelike(object: any, operation: string, position?: number): void;
4
5
  export function box(value: any): any;
5
6
  export function castArrayLike(keys: any[], values: any[]): any;
6
7
  export function getRealmObjectPrototype(object: any): any;
package/src/utilities.js CHANGED
@@ -7,6 +7,30 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
7
7
 
8
8
  /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
9
9
 
10
+ /**
11
+ * If the given object isn't treelike, throw an exception.
12
+ *
13
+ * @param {any} object
14
+ * @param {string} operation
15
+ * @param {number} [position]
16
+ */
17
+ export function assertIsTreelike(object, operation, position = 0) {
18
+ let message;
19
+ if (!object) {
20
+ message = `${operation}: The tree argument wasn't defined.`;
21
+ } else if (object instanceof Promise) {
22
+ // A common mistake
23
+ message = `${operation}: The tree argument was a Promise. Did you mean to use await?`;
24
+ } else if (!Tree.isTreelike) {
25
+ message = `${operation}: The tree argument wasn't a treelike object.`;
26
+ }
27
+ if (message) {
28
+ const error = new TypeError(message);
29
+ /** @type {any} */ (error).position = position;
30
+ throw error;
31
+ }
32
+ }
33
+
10
34
  /**
11
35
  * Return the value as an object. If the value is already an object it will be
12
36
  * returned as is. If the value is a primitive, it will be wrapped in an object:
@@ -0,0 +1,55 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import addNextPrevious from "../../src/operations/addNextPrevious.js";
4
+
5
+ describe("addNextPrevious", () => {
6
+ test("adds next/previous properties to values", async () => {
7
+ const tree = {
8
+ alice: {
9
+ name: "Alice",
10
+ },
11
+ bob: {
12
+ name: "Bob",
13
+ },
14
+ carol: {
15
+ name: "Carol",
16
+ },
17
+ };
18
+ const result = await addNextPrevious(tree);
19
+ assert.deepEqual(result, {
20
+ alice: {
21
+ name: "Alice",
22
+ nextKey: "bob",
23
+ },
24
+ bob: {
25
+ name: "Bob",
26
+ nextKey: "carol",
27
+ previousKey: "alice",
28
+ },
29
+ carol: {
30
+ name: "Carol",
31
+ previousKey: "bob",
32
+ },
33
+ });
34
+ });
35
+
36
+ test("returns a non-object value as a 'value' property", async () => {
37
+ const array = ["Alice", "Bob", "Carol"];
38
+ const result = await addNextPrevious(array);
39
+ assert.deepEqual(result, [
40
+ {
41
+ value: "Alice",
42
+ nextKey: 1,
43
+ },
44
+ {
45
+ value: "Bob",
46
+ nextKey: 2,
47
+ previousKey: 0,
48
+ },
49
+ {
50
+ value: "Carol",
51
+ previousKey: 1,
52
+ },
53
+ ]);
54
+ });
55
+ });
@@ -0,0 +1,12 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import concatTrees from "../../src/operations/concatTrees.js";
4
+
5
+ describe("concatTrees", () => {
6
+ test("joins strings and values together", async () => {
7
+ const array = [1, 2, 3];
8
+ const object = { person1: "Alice", person2: "Bob" };
9
+ const result = await concatTrees`a ${array} b ${object} c`;
10
+ assert.equal(result, "a 123 b AliceBob c");
11
+ });
12
+ });
@@ -1,14 +1,12 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { ObjectTree, Tree } from "../../src/internal.js";
4
- import keyFunctionsForExtensions from "../../src/operations/keyFunctionsForExtensions.js";
4
+ import extensionKeyFunctions from "../../src/operations/extensionKeyFunctions.js";
5
5
  import map from "../../src/operations/map.js";
6
6
 
7
7
  describe("keyMapsForExtensions", () => {
8
8
  test("returns key functions that pass a matching key through", async () => {
9
- const { inverseKey, key } = keyFunctionsForExtensions({
10
- sourceExtension: ".txt",
11
- });
9
+ const { inverseKey, key } = extensionKeyFunctions(".txt");
12
10
  assert.equal(await inverseKey("file.txt"), "file.txt");
13
11
  assert.equal(await inverseKey("file.txt/"), "file.txt");
14
12
  assert.equal(await key("file.txt"), "file.txt");
@@ -18,10 +16,7 @@ describe("keyMapsForExtensions", () => {
18
16
  });
19
17
 
20
18
  test("returns key functions that can map extensions", async () => {
21
- const { inverseKey, key } = keyFunctionsForExtensions({
22
- resultExtension: ".json",
23
- sourceExtension: ".md",
24
- });
19
+ const { inverseKey, key } = extensionKeyFunctions(".md", ".json");
25
20
  assert.equal(await inverseKey("file.json"), "file.md");
26
21
  assert.equal(await inverseKey("file.json/"), "file.md");
27
22
  assert.equal(await key("file.md"), "file.json");
@@ -31,10 +26,7 @@ describe("keyMapsForExtensions", () => {
31
26
  });
32
27
 
33
28
  test("key functions can handle a slash as an explicit extension", async () => {
34
- const { inverseKey, key } = keyFunctionsForExtensions({
35
- resultExtension: ".html",
36
- sourceExtension: "/",
37
- });
29
+ const { inverseKey, key } = extensionKeyFunctions("/", ".html");
38
30
  assert.equal(await inverseKey("file.html"), "file/");
39
31
  assert.equal(await inverseKey("file.html/"), "file/");
40
32
  assert.equal(await key("file"), undefined);
@@ -47,9 +39,7 @@ describe("keyMapsForExtensions", () => {
47
39
  file2: "won't be mapped",
48
40
  "file3.foo": "won't be mapped",
49
41
  });
50
- const { inverseKey, key } = keyFunctionsForExtensions({
51
- sourceExtension: ".txt",
52
- });
42
+ const { inverseKey, key } = extensionKeyFunctions(".txt");
53
43
  const fixture = map(files, {
54
44
  inverseKey,
55
45
  key,
@@ -66,10 +56,7 @@ describe("keyMapsForExtensions", () => {
66
56
  file2: "won't be mapped",
67
57
  "file3.foo": "won't be mapped",
68
58
  });
69
- const { inverseKey, key } = keyFunctionsForExtensions({
70
- resultExtension: ".upper",
71
- sourceExtension: ".txt",
72
- });
59
+ const { inverseKey, key } = extensionKeyFunctions(".txt", ".upper");
73
60
  const fixture = map(files, {
74
61
  inverseKey,
75
62
  key,
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
4
+ import paginate from "../../src/operations/paginate.js";
5
+
6
+ describe("paginate", () => {
7
+ test("divides tree keys into fixed-length chunks", async () => {
8
+ const treelike = {
9
+ a: 1,
10
+ b: 2,
11
+ c: 3,
12
+ d: 4,
13
+ e: 5,
14
+ };
15
+ const paginated = await paginate.call(null, treelike, 2);
16
+ assert.deepEqual(await Tree.plain(paginated), {
17
+ 1: {
18
+ items: { a: 1, b: 2 },
19
+ nextPage: 2,
20
+ pageCount: 3,
21
+ pageNumber: 1,
22
+ previousPage: null,
23
+ },
24
+ 2: {
25
+ items: { c: 3, d: 4 },
26
+ nextPage: 3,
27
+ pageCount: 3,
28
+ pageNumber: 2,
29
+ previousPage: 1,
30
+ },
31
+ 3: {
32
+ items: { e: 5 },
33
+ nextPage: null,
34
+ pageCount: 3,
35
+ pageNumber: 3,
36
+ previousPage: 2,
37
+ },
38
+ });
39
+ });
40
+ });