@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.
Files changed (158) hide show
  1. package/index.ts +16 -6
  2. package/package.json +2 -2
  3. package/shared.js +20 -30
  4. package/src/Tree.js +62 -513
  5. package/src/constants.js +2 -0
  6. package/src/drivers/BrowserFileTree.js +9 -10
  7. package/src/drivers/DeepMapTree.js +3 -3
  8. package/src/drivers/DeepObjectTree.js +4 -5
  9. package/src/drivers/DeferredTree.js +2 -2
  10. package/src/drivers/FileTree.js +11 -33
  11. package/src/drivers/FunctionTree.js +1 -1
  12. package/src/drivers/MapTree.js +6 -6
  13. package/src/drivers/ObjectTree.js +4 -3
  14. package/src/drivers/SetTree.js +1 -1
  15. package/src/drivers/SiteTree.js +1 -1
  16. package/src/drivers/constantTree.js +1 -1
  17. package/src/extension.js +5 -3
  18. package/src/jsonKeys.js +5 -7
  19. package/src/operations/addNextPrevious.js +10 -9
  20. package/src/operations/assign.js +40 -0
  21. package/src/operations/cache.js +18 -12
  22. package/src/operations/cachedKeyFunctions.js +15 -4
  23. package/src/operations/clear.js +20 -0
  24. package/src/operations/concat.js +17 -0
  25. package/src/operations/deepMap.js +25 -0
  26. package/src/operations/deepMerge.js +11 -25
  27. package/src/operations/deepReverse.js +6 -7
  28. package/src/operations/deepTake.js +6 -7
  29. package/src/operations/deepText.js +4 -4
  30. package/src/operations/deepValuesIterator.js +8 -6
  31. package/src/operations/defineds.js +32 -0
  32. package/src/operations/delete.js +20 -0
  33. package/src/operations/entries.js +16 -0
  34. package/src/operations/extensionKeyFunctions.js +1 -1
  35. package/src/operations/filter.js +7 -8
  36. package/src/operations/first.js +18 -0
  37. package/src/operations/forEach.js +20 -0
  38. package/src/operations/from.js +77 -0
  39. package/src/operations/fromFn.js +26 -0
  40. package/src/operations/globKeys.js +8 -8
  41. package/src/operations/group.js +9 -7
  42. package/src/operations/has.js +16 -0
  43. package/src/operations/indent.js +4 -2
  44. package/src/operations/inners.js +29 -0
  45. package/src/operations/invokeFunctions.js +5 -4
  46. package/src/operations/isAsyncMutableTree.js +15 -0
  47. package/src/operations/isAsyncTree.js +21 -0
  48. package/src/operations/isTraversable.js +15 -0
  49. package/src/operations/isTreelike.js +33 -0
  50. package/src/operations/json.js +4 -3
  51. package/src/operations/keys.js +14 -0
  52. package/src/operations/length.js +15 -0
  53. package/src/operations/map.js +151 -95
  54. package/src/operations/mapExtension.js +27 -0
  55. package/src/operations/mapReduce.js +44 -0
  56. package/src/operations/mask.js +18 -16
  57. package/src/operations/match.js +74 -0
  58. package/src/operations/merge.js +22 -20
  59. package/src/operations/paginate.js +3 -5
  60. package/src/operations/parent.js +13 -0
  61. package/src/operations/paths.js +51 -0
  62. package/src/operations/plain.js +34 -0
  63. package/src/operations/regExpKeys.js +4 -5
  64. package/src/operations/remove.js +14 -0
  65. package/src/operations/reverse.js +4 -6
  66. package/src/operations/root.js +17 -0
  67. package/src/operations/scope.js +4 -6
  68. package/src/operations/setDeep.js +50 -0
  69. package/src/operations/shuffle.js +46 -0
  70. package/src/operations/sort.js +19 -12
  71. package/src/operations/take.js +3 -5
  72. package/src/operations/text.js +3 -3
  73. package/src/operations/toFunction.js +14 -0
  74. package/src/operations/traverse.js +25 -0
  75. package/src/operations/traverseOrThrow.js +64 -0
  76. package/src/operations/traversePath.js +16 -0
  77. package/src/operations/values.js +15 -0
  78. package/src/operations/withKeys.js +33 -0
  79. package/src/utilities/TypedArray.js +2 -0
  80. package/src/utilities/box.js +20 -0
  81. package/src/utilities/castArraylike.js +38 -0
  82. package/src/utilities/getParent.js +33 -0
  83. package/src/utilities/getRealmObjectPrototype.js +19 -0
  84. package/src/utilities/getTreeArgument.js +43 -0
  85. package/src/utilities/isPacked.js +20 -0
  86. package/src/utilities/isPlainObject.js +29 -0
  87. package/src/utilities/isPrimitive.js +13 -0
  88. package/src/utilities/isStringlike.js +25 -0
  89. package/src/utilities/isUnpackable.js +13 -0
  90. package/src/utilities/keysFromPath.js +34 -0
  91. package/src/utilities/naturalOrder.js +9 -0
  92. package/src/utilities/pathFromKeys.js +18 -0
  93. package/src/utilities/setParent.js +38 -0
  94. package/src/utilities/toFunction.js +41 -0
  95. package/src/utilities/toPlainValue.js +95 -0
  96. package/src/utilities/toString.js +37 -0
  97. package/test/drivers/ExplorableSiteTree.test.js +1 -1
  98. package/test/drivers/FileTree.test.js +1 -1
  99. package/test/drivers/calendarTree.test.js +1 -1
  100. package/test/jsonKeys.test.js +1 -1
  101. package/test/operations/assign.test.js +54 -0
  102. package/test/operations/cache.test.js +1 -1
  103. package/test/operations/cachedKeyFunctions.test.js +16 -16
  104. package/test/operations/clear.test.js +34 -0
  105. package/test/operations/deepMap.test.js +29 -0
  106. package/test/operations/deepMerge.test.js +2 -6
  107. package/test/operations/deepReverse.test.js +1 -1
  108. package/test/operations/defineds.test.js +25 -0
  109. package/test/operations/delete.test.js +20 -0
  110. package/test/operations/entries.test.js +18 -0
  111. package/test/operations/extensionKeyFunctions.test.js +10 -10
  112. package/test/operations/first.test.js +15 -0
  113. package/test/operations/fixtures/README.md +1 -0
  114. package/test/operations/forEach.test.js +22 -0
  115. package/test/operations/from.test.js +67 -0
  116. package/test/operations/globKeys.test.js +3 -3
  117. package/test/operations/has.test.js +15 -0
  118. package/test/operations/inners.test.js +30 -0
  119. package/test/operations/invokeFunctions.test.js +1 -1
  120. package/test/operations/isAsyncMutableTree.test.js +17 -0
  121. package/test/operations/isAsyncTree.test.js +26 -0
  122. package/test/operations/isTreelike.test.js +13 -0
  123. package/test/operations/keys.test.js +15 -0
  124. package/test/operations/length.test.js +15 -0
  125. package/test/operations/map.test.js +61 -42
  126. package/test/operations/mapExtension.test.js +0 -0
  127. package/test/operations/mapReduce.test.js +23 -0
  128. package/test/operations/mask.test.js +1 -1
  129. package/test/operations/match.test.js +33 -0
  130. package/test/operations/merge.test.js +23 -9
  131. package/test/operations/parent.test.js +15 -0
  132. package/test/operations/paths.test.js +40 -0
  133. package/test/operations/plain.test.js +69 -0
  134. package/test/operations/reverse.test.js +1 -1
  135. package/test/operations/scope.test.js +1 -1
  136. package/test/operations/setDeep.test.js +53 -0
  137. package/test/operations/shuffle.test.js +18 -0
  138. package/test/operations/sort.test.js +3 -3
  139. package/test/operations/toFunction.test.js +16 -0
  140. package/test/operations/traverse.test.js +43 -0
  141. package/test/operations/traversePath.test.js +16 -0
  142. package/test/operations/values.test.js +18 -0
  143. package/test/operations/withKeys.test.js +21 -0
  144. package/test/utilities/box.test.js +26 -0
  145. package/test/utilities/getRealmObjectPrototype.test.js +11 -0
  146. package/test/utilities/isPlainObject.test.js +13 -0
  147. package/test/utilities/keysFromPath.test.js +14 -0
  148. package/test/utilities/naturalOrder.test.js +11 -0
  149. package/test/utilities/pathFromKeys.test.js +12 -0
  150. package/test/utilities/setParent.test.js +34 -0
  151. package/test/utilities/toFunction.test.js +34 -0
  152. package/test/utilities/toPlainValue.test.js +27 -0
  153. package/test/utilities/toString.test.js +22 -0
  154. package/src/Tree.d.ts +0 -24
  155. package/src/utilities.d.ts +0 -21
  156. package/src/utilities.js +0 -443
  157. package/test/Tree.test.js +0 -407
  158. package/test/utilities.test.js +0 -141
@@ -0,0 +1,22 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import toString from "../../src/utilities/toString.js";
4
+
5
+ describe("toString", () => {
6
+ test("returns the value of an object's `toString` method", () => {
7
+ const object = {
8
+ toString: () => "text",
9
+ };
10
+ assert.equal(toString(object), "text");
11
+ });
12
+
13
+ test("returns null for an object with no useful `toString`", () => {
14
+ const object = {};
15
+ assert.equal(toString(object), null);
16
+ });
17
+
18
+ test("decodes an ArrayBuffer as UTF-8", () => {
19
+ const arrayBuffer = new TextEncoder().encode("text").buffer;
20
+ assert.equal(toString(arrayBuffer), "text");
21
+ });
22
+ });
package/src/Tree.d.ts DELETED
@@ -1,24 +0,0 @@
1
- import type { AsyncMutableTree, AsyncTree } from "@weborigami/types";
2
- import { PlainObject, ReduceFn, Treelike, TreeMapOptions, ValueKeyFn } from "../index.ts";
3
-
4
- export function assign(target: Treelike, source: Treelike): Promise<AsyncTree>;
5
- export function clear(AsyncTree: AsyncMutableTree): Promise<void>;
6
- export function entries(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
7
- export function forEach(AsyncTree: AsyncTree, callbackfn: (value: any, key: any) => Promise<void>): Promise<void>;
8
- export function from(obj: any, options?: { deep?: boolean, parent?: AsyncTree|null }): AsyncTree;
9
- export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
10
- export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
11
- export function isAsyncTree(obj: any): obj is AsyncTree;
12
- export function isTraversable(obj: any): boolean;
13
- export function isTreelike(obj: any): obj is Treelike;
14
- export function map(tree: Treelike, options: TreeMapOptions|ValueKeyFn): AsyncTree;
15
- export function mapReduce(tree: Treelike, mapFn: ValueKeyFn | null, reduceFn: ReduceFn): Promise<any>;
16
- export function paths(tree: Treelike, options?: { assumeSlashes?: boolean, base?: string }): string[];
17
- export function plain(tree: Treelike): Promise<PlainObject>;
18
- export function root(tree: Treelike): AsyncTree;
19
- export function remove(AsyncTree: AsyncMutableTree, key: any): Promise<boolean>;
20
- export function toFunction(tree: Treelike): Function;
21
- export function traverse(tree: Treelike, ...keys: any[]): Promise<any>;
22
- export function traverseOrThrow(tree: Treelike, ...keys: any[]): Promise<any>;
23
- export function traversePath(tree: Treelike, path: string): Promise<any>;
24
- export function values(tree: Treelike): Promise<IterableIterator<any>>;
@@ -1,21 +0,0 @@
1
- import { AsyncTree } from "@weborigami/types";
2
- import { Packed, PlainObject, StringLike } from "../index.ts";
3
-
4
- export function assertIsTreelike(object: any, operation: string, position?: number): void;
5
- export function box(value: any): any;
6
- export function castArrayLike(keys: any[], values: any[]): any;
7
- export function getParent(object: any, options?: any): AsyncTree|null;
8
- export function getRealmObjectPrototype(object: any): any;
9
- export const hiddenFileNames: string[];
10
- export function isPacked(obj: any): obj is Packed;
11
- export function isPlainObject(obj: any): obj is PlainObject;
12
- export function isPrimitive(obj: any): boolean;
13
- export function isStringLike(obj: any): obj is StringLike;
14
- export function isUnpackable(obj): obj is { unpack: () => any };
15
- export function keysFromPath(path: string): string[];
16
- export const naturalOrder: (a: string, b: string) => number;
17
- export function pathFromKeys(keys: string[]): string;
18
- export function pipeline(start: any, ...functions: Function[]): Promise<any>;
19
- export function setParent(child: any, parent: AsyncTree|null): void;
20
- export function toPlainValue(object: any): Promise<any>;
21
- export function toString(object: any): string;
package/src/utilities.js DELETED
@@ -1,443 +0,0 @@
1
- import { Tree } from "./internal.js";
2
- import * as symbols from "./symbols.js";
3
- import * as trailingSlash from "./trailingSlash.js";
4
-
5
- const textDecoder = new TextDecoder();
6
- const TypedArray = Object.getPrototypeOf(Uint8Array);
7
-
8
- /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
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
-
34
- /**
35
- * Return the value as an object. If the value is already an object it will be
36
- * returned as is. If the value is a primitive, it will be wrapped in an object:
37
- * a string will be wrapped in a String object, a number will be wrapped in a
38
- * Number object, and a boolean will be wrapped in a Boolean object.
39
- *
40
- * @param {any} value
41
- */
42
- export function box(value) {
43
- switch (typeof value) {
44
- case "string":
45
- return new String(value);
46
- case "number":
47
- return new Number(value);
48
- case "boolean":
49
- return new Boolean(value);
50
- default:
51
- return value;
52
- }
53
- }
54
-
55
- /**
56
- * Create an array or plain object from the given keys and values.
57
- *
58
- * If the given plain object has only integer keys, and the set of integers is
59
- * complete from 0 to length-1, assume the values are a result of array
60
- * transformations and the values are the desired result; return them as is.
61
- * Otherwise, create a plain object with the keys and values.
62
- *
63
- * @param {any[]} keys
64
- * @param {any[]} values
65
- */
66
- export function castArrayLike(keys, values) {
67
- if (keys.length === 0) {
68
- // Empty keys/values means an empty object, not an empty array
69
- return {};
70
- }
71
-
72
- let onlyNumericKeys = true;
73
- const numberSeen = new Array(keys.length);
74
- for (const key of keys) {
75
- const n = Number(key);
76
- if (isNaN(n) || !Number.isInteger(n) || n < 0 || n >= keys.length) {
77
- onlyNumericKeys = false;
78
- break;
79
- } else {
80
- numberSeen[n] = true;
81
- }
82
- }
83
-
84
- // If any number from 0..length-1 is missing, we can't treat this as an array
85
- const allNumbersSeen = onlyNumericKeys && numberSeen.every((v) => v);
86
- if (allNumbersSeen) {
87
- return values;
88
- } else {
89
- // Return a plain object with the (key, value) pairs
90
- return Object.fromEntries(keys.map((key, i) => [key, values[i]]));
91
- }
92
- }
93
-
94
- /**
95
- * Return a suitable parent for the packed file.
96
- *
97
- * This is intended to be called by unpack functions.
98
- *
99
- * @param {any} packed
100
- * @param {any} [options]
101
- * @returns {AsyncTree|null}
102
- */
103
- export function getParent(packed, options = {}) {
104
- // Prefer parent set on options
105
- if (options?.parent) {
106
- return options.parent;
107
- }
108
-
109
- // If the packed object has a `parent` property, use that. Exception: Node
110
- // Buffer objects have a `parent` property that we ignore.
111
- if (packed.parent && !(packed instanceof Buffer)) {
112
- return packed.parent;
113
- }
114
-
115
- // If the packed object has a parent symbol, use that.
116
- if (packed[symbols.parent]) {
117
- return packed[symbols.parent];
118
- }
119
-
120
- // Otherwise, return null.
121
- return null;
122
- }
123
-
124
- /**
125
- * Return the Object prototype at the root of the object's prototype chain.
126
- *
127
- * This is used by functions like isPlainObject() to handle cases where the
128
- * `Object` at the root prototype chain is in a different realm.
129
- *
130
- * @param {any} object
131
- */
132
- export function getRealmObjectPrototype(object) {
133
- if (Object.getPrototypeOf(object) === null) {
134
- // The object has no prototype.
135
- return null;
136
- }
137
- let proto = object;
138
- while (Object.getPrototypeOf(proto) !== null) {
139
- proto = Object.getPrototypeOf(proto);
140
- }
141
- return proto;
142
- }
143
-
144
- // Names of OS-generated files that should not be enumerated
145
- export const hiddenFileNames = [".DS_Store"];
146
-
147
- /**
148
- * Return true if the object is in a packed form (or can be readily packed into
149
- * a form) that can be given to fs.writeFile or response.write().
150
- *
151
- * @param {any} obj
152
- * @returns {obj is import("../index.ts").Packed}
153
- */
154
- export function isPacked(obj) {
155
- return (
156
- typeof obj === "string" ||
157
- obj instanceof ArrayBuffer ||
158
- obj instanceof ReadableStream ||
159
- obj instanceof String ||
160
- obj instanceof TypedArray
161
- );
162
- }
163
-
164
- /**
165
- * Return true if the object is a plain JavaScript object created by `{}`,
166
- * `new Object()`, or `Object.create(null)`.
167
- *
168
- * This function also considers object-like things with no prototype (like a
169
- * `Module`) as plain objects.
170
- *
171
- * @param {any} obj
172
- * @returns {obj is import("../index.ts").PlainObject}
173
- */
174
- export function isPlainObject(obj) {
175
- // From https://stackoverflow.com/q/51722354/76472
176
- if (typeof obj !== "object" || obj === null) {
177
- return false;
178
- }
179
-
180
- // We treat object-like things with no prototype (like a Module) as plain
181
- // objects.
182
- if (Object.getPrototypeOf(obj) === null) {
183
- return true;
184
- }
185
-
186
- // Do we inherit directly from Object in this realm?
187
- return Object.getPrototypeOf(obj) === getRealmObjectPrototype(obj);
188
- }
189
-
190
- /**
191
- * Return true if the value is a primitive JavaScript value.
192
- *
193
- * @param {any} value
194
- */
195
- export function isPrimitive(value) {
196
- // Check for null first, since typeof null === "object".
197
- if (value === null) {
198
- return true;
199
- }
200
- const type = typeof value;
201
- return type !== "object" && type !== "function";
202
- }
203
-
204
- /**
205
- * Return true if the object is a string or object with a non-trival `toString`
206
- * method.
207
- *
208
- * @param {any} obj
209
- * @returns {obj is import("../index.ts").StringLike}
210
- */
211
- export function isStringLike(obj) {
212
- if (typeof obj === "string") {
213
- return true;
214
- } else if (obj?.toString === undefined) {
215
- return false;
216
- } else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
217
- // The stupid Object.prototype.toString implementation always returns
218
- // "[object Object]", so if that's the only toString method the object has,
219
- // we return false.
220
- return false;
221
- } else {
222
- return true;
223
- }
224
- }
225
-
226
- export function isUnpackable(obj) {
227
- return (
228
- isPacked(obj) && typeof (/** @type {any} */ (obj).unpack) === "function"
229
- );
230
- }
231
-
232
- /**
233
- * Given a path like "/foo/bar/baz", return an array of keys like ["foo/",
234
- * "bar/", "baz"].
235
- *
236
- * Leading slashes are ignored. Consecutive slashes will be ignored. Trailing
237
- * slashes are preserved.
238
- *
239
- * @param {string} pathname
240
- */
241
- export function keysFromPath(pathname) {
242
- // Split the path at each slash
243
- let keys = pathname.split("/");
244
- if (keys[0] === "") {
245
- // The path begins with a slash; drop that part.
246
- keys.shift();
247
- }
248
- if (keys.at(-1) === "") {
249
- // The path ends with a slash; drop that part.
250
- keys.pop();
251
- }
252
- // Drop any empty keys
253
- keys = keys.filter((key) => key !== "");
254
- // Add the trailing slash back to all keys but the last
255
- for (let i = 0; i < keys.length - 1; i++) {
256
- keys[i] += "/";
257
- }
258
- // Add trailing slash to last key if path ended with a slash
259
- if (keys.length > 0 && trailingSlash.has(pathname)) {
260
- keys[keys.length - 1] += "/";
261
- }
262
- return keys;
263
- }
264
-
265
- /**
266
- * Compare two strings using [natural sort
267
- * order](https://en.wikipedia.org/wiki/Natural_sort_order).
268
- */
269
- export const naturalOrder = new Intl.Collator(undefined, {
270
- numeric: true,
271
- }).compare;
272
-
273
- /**
274
- * Return a slash-separated path for the given keys.
275
- *
276
- * This takes care to avoid adding consecutive slashes if they keys themselves
277
- * already have trailing slashes.
278
- *
279
- * @param {string[]} keys
280
- */
281
- export function pathFromKeys(keys) {
282
- // Ensure there's a slash between all keys. If the last key has a trailing
283
- // slash, leave it there.
284
- const normalized = keys.map((key, index) =>
285
- index < keys.length - 1 ? trailingSlash.add(key) : key
286
- );
287
- return normalized.join("");
288
- }
289
-
290
- /**
291
- * Apply a series of functions to a value, passing the result of each function
292
- * to the next one.
293
- *
294
- * @param {any} start
295
- * @param {...Function} fns
296
- */
297
- export async function pipeline(start, ...fns) {
298
- return fns.reduce(async (acc, fn) => fn(await acc), start);
299
- }
300
-
301
- /**
302
- * If the child object doesn't have a parent yet, set it to the indicated
303
- * parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
304
- * set the `symbols.parent` property.
305
- *
306
- * @param {*} child
307
- * @param {AsyncTree|null} parent
308
- */
309
- export function setParent(child, parent) {
310
- if (Tree.isAsyncTree(child)) {
311
- // Value is a subtree; set its parent to this tree.
312
- if (!child.parent) {
313
- child.parent = parent;
314
- }
315
- } else if (Object.isExtensible(child) && !child[symbols.parent]) {
316
- try {
317
- // Add parent reference as a symbol to avoid polluting the object. This
318
- // reference will be used if the object is later used as a tree. We set
319
- // `enumerable` to false even thought this makes no practical difference
320
- // (symbols are never enumerated) because it can provide a hint in the
321
- // debugger that the property is for internal use.
322
- Object.defineProperty(child, symbols.parent, {
323
- configurable: true,
324
- enumerable: false,
325
- value: parent,
326
- writable: true,
327
- });
328
- } catch (error) {
329
- // Ignore exceptions. Some esoteric objects don't allow adding properties.
330
- // We can still treat them as trees, but they won't have a parent.
331
- }
332
- }
333
- }
334
-
335
- function toBase64(object) {
336
- if (typeof Buffer !== "undefined") {
337
- // Node.js environment
338
- return Buffer.from(object).toString("base64");
339
- } else {
340
- // Browser environment
341
- let binary = "";
342
- const bytes = new Uint8Array(object);
343
- const len = bytes.byteLength;
344
- for (let i = 0; i < len; i++) {
345
- binary += String.fromCharCode(bytes[i]);
346
- }
347
- return btoa(binary);
348
- }
349
- }
350
-
351
- /**
352
- * Convert the given input to the plainest possible JavaScript value. This
353
- * helper is intended for functions that want to accept an argument from the ori
354
- * CLI, which could a string, a stream of data, or some other kind of JavaScript
355
- * object.
356
- *
357
- * If the input is a function, it will be invoked and its result will be
358
- * processed.
359
- *
360
- * If the input is a promise, it will be resolved and its result will be
361
- * processed.
362
- *
363
- * If the input is treelike, it will be converted to a plain JavaScript object,
364
- * recursively traversing the tree and converting all values to plain types.
365
- *
366
- * If the input is stringlike, its text will be returned.
367
- *
368
- * If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
369
- * text if it does not contain unprintable characters. If it does, it will be
370
- * returned as a base64-encoded string.
371
- *
372
- * If the input has a custom class instance, its public properties will be
373
- * returned as a plain object.
374
- *
375
- * @param {any} input
376
- * @returns {Promise<any>}
377
- */
378
- export async function toPlainValue(input) {
379
- if (input instanceof Function) {
380
- // Invoke function
381
- input = input();
382
- }
383
- if (input instanceof Promise) {
384
- // Resolve promise
385
- input = await input;
386
- }
387
-
388
- if (isPrimitive(input) || input instanceof Date) {
389
- return input;
390
- } else if (Tree.isTreelike(input)) {
391
- const mapped = await Tree.map(input, (value) => toPlainValue(value));
392
- return Tree.plain(mapped);
393
- } else if (isStringLike(input)) {
394
- return toString(input);
395
- } else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
396
- // Try to interpret the buffer as UTF-8 text, otherwise use base64.
397
- const text = toString(input);
398
- if (text !== null) {
399
- return text;
400
- } else {
401
- return toBase64(input);
402
- }
403
- } else {
404
- // Some other kind of class instance; return its public properties.
405
- const plain = {};
406
- for (const [key, value] of Object.entries(input)) {
407
- plain[key] = await toPlainValue(value);
408
- }
409
- return plain;
410
- }
411
- }
412
-
413
- /**
414
- * Return a string form of the object, handling cases not generally handled by
415
- * the standard JavaScript `toString()` method:
416
- *
417
- * 1. If the object is an ArrayBuffer or TypedArray, decode the array as UTF-8.
418
- * 2. If the object is otherwise a plain JavaScript object with the useless
419
- * default toString() method, return null instead of "[object Object]". In
420
- * practice, it's generally more useful to have this method fail than to
421
- * return a useless string.
422
- * 3. If the object is a defined primitive value, return the result of
423
- * String(object).
424
- *
425
- * Otherwise return null.
426
- *
427
- * @param {any} object
428
- * @returns {string|null}
429
- */
430
- export function toString(object) {
431
- if (object instanceof ArrayBuffer || object instanceof TypedArray) {
432
- // Treat the buffer as UTF-8 text.
433
- const decoded = textDecoder.decode(object);
434
- // If the result appears to contain non-printable characters, it's probably not a string.
435
- // https://stackoverflow.com/a/1677660/76472
436
- const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
437
- return hasNonPrintableCharacters ? null : decoded;
438
- } else if (isStringLike(object) || (object !== null && isPrimitive(object))) {
439
- return String(object);
440
- } else {
441
- return null;
442
- }
443
- }