@weborigami/async-tree 0.0.66-beta.1 → 0.0.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +2 -0
- package/package.json +4 -4
- package/src/BrowserFileTree.js +28 -6
- package/src/DeepMapTree.js +2 -2
- package/src/DeepObjectTree.js +6 -9
- package/src/DeferredTree.js +4 -2
- package/src/ExplorableSiteTree.js +52 -0
- package/src/FileTree.js +14 -10
- package/src/MapTree.js +27 -4
- package/src/ObjectTree.js +53 -6
- package/src/SetTree.js +0 -6
- package/src/SiteTree.js +32 -85
- package/src/Tree.d.ts +0 -1
- package/src/Tree.js +24 -41
- package/src/jsonKeys.js +4 -37
- package/src/operations/cache.js +0 -4
- package/src/operations/deepMerge.js +7 -10
- package/src/operations/merge.js +7 -10
- package/src/trailingSlash.js +54 -0
- package/src/transforms/cachedKeyFunctions.js +72 -34
- package/src/transforms/keyFunctionsForExtensions.js +24 -10
- package/src/transforms/mapFn.js +11 -17
- package/src/transforms/regExpKeys.js +17 -12
- package/src/utilities.js +34 -6
- package/test/BrowserFileTree.test.js +28 -5
- package/test/DeepMapTree.test.js +17 -0
- package/test/DeepObjectTree.test.js +17 -7
- package/test/ExplorableSiteTree.test.js +113 -0
- package/test/FileTree.test.js +14 -7
- package/test/MapTree.test.js +21 -0
- package/test/ObjectTree.test.js +16 -12
- package/test/SiteTree.test.js +14 -49
- package/test/Tree.test.js +19 -39
- package/test/browser/assert.js +9 -0
- package/test/browser/index.html +4 -4
- package/test/calendarTree.test.js +1 -1
- package/test/fixtures/markdown/subfolder/README.md +1 -0
- package/test/jsonKeys.test.js +0 -9
- package/test/operations/cache.test.js +1 -1
- package/test/operations/merge.test.js +20 -1
- package/test/trailingSlash.test.js +36 -0
- package/test/transforms/cachedKeyFunctions.test.js +90 -0
- package/test/transforms/keyFunctionsForExtensions.test.js +7 -3
- package/test/transforms/mapFn.test.js +29 -20
- package/test/utilities.test.js +6 -2
- package/test/transforms/cachedKeyMaps.test.js +0 -41
package/main.js
CHANGED
|
@@ -6,6 +6,7 @@ export { default as FunctionTree } from "./src/FunctionTree.js";
|
|
|
6
6
|
export { default as MapTree } from "./src/MapTree.js";
|
|
7
7
|
// Skip BrowserFileTree.js, which is browser-only.
|
|
8
8
|
export { default as DeepMapTree } from "./src/DeepMapTree.js";
|
|
9
|
+
export { default as ExplorableSiteTree } from "./src/ExplorableSiteTree.js";
|
|
9
10
|
export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
|
|
10
11
|
export * as jsonKeys from "./src/jsonKeys.js";
|
|
11
12
|
export { default as cache } from "./src/operations/cache.js";
|
|
@@ -25,6 +26,7 @@ export { default as take } from "./src/operations/take.js";
|
|
|
25
26
|
export { default as SetTree } from "./src/SetTree.js";
|
|
26
27
|
export { default as SiteTree } from "./src/SiteTree.js";
|
|
27
28
|
export * as symbols from "./src/symbols.js";
|
|
29
|
+
export * as trailingSlash from "./src/trailingSlash.js";
|
|
28
30
|
export { default as cachedKeyFunctions } from "./src/transforms/cachedKeyFunctions.js";
|
|
29
31
|
export { default as deepReverse } from "./src/transforms/deepReverse.js";
|
|
30
32
|
export { default as invokeFunctions } from "./src/transforms/invokeFunctions.js";
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.0.66
|
|
3
|
+
"version": "0.0.66",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
7
7
|
"browser": "./browser.js",
|
|
8
8
|
"types": "./index.ts",
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/node": "22.
|
|
11
|
-
"typescript": "5.
|
|
10
|
+
"@types/node": "22.7.4",
|
|
11
|
+
"typescript": "5.6.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.0.66
|
|
14
|
+
"@weborigami/types": "0.0.66"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/src/BrowserFileTree.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as trailingSlash from "../src/trailingSlash.js";
|
|
1
2
|
import { Tree } from "./internal.js";
|
|
2
3
|
import {
|
|
3
4
|
hiddenFileNames,
|
|
@@ -40,10 +41,17 @@ export default class BrowserFileTree {
|
|
|
40
41
|
`${this.constructor.name}: Cannot get a null or undefined key.`
|
|
41
42
|
);
|
|
42
43
|
}
|
|
44
|
+
if (key === "") {
|
|
45
|
+
// Can't have a file with no name
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remove trailing slash if present
|
|
50
|
+
key = trailingSlash.remove(key);
|
|
43
51
|
|
|
44
52
|
const directory = await this.getDirectory();
|
|
45
53
|
|
|
46
|
-
// Try the key as a subfolder name
|
|
54
|
+
// Try the key as a subfolder name
|
|
47
55
|
try {
|
|
48
56
|
const subfolderHandle = await directory.getDirectoryHandle(key);
|
|
49
57
|
const value = Reflect.construct(this.constructor, [subfolderHandle]);
|
|
@@ -60,7 +68,7 @@ export default class BrowserFileTree {
|
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
// Try the key as a file name
|
|
71
|
+
// Try the key as a file name
|
|
64
72
|
try {
|
|
65
73
|
const fileHandle = await directory.getFileHandle(key);
|
|
66
74
|
const file = await fileHandle.getFile();
|
|
@@ -76,6 +84,8 @@ export default class BrowserFileTree {
|
|
|
76
84
|
return undefined;
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
// Return the directory handle, creating it if necessary. We can't create the
|
|
88
|
+
// default value in the constructor because we need to await it.
|
|
79
89
|
async getDirectory() {
|
|
80
90
|
this.directory ??= await navigator.storage.getDirectory();
|
|
81
91
|
return this.directory;
|
|
@@ -85,9 +95,18 @@ export default class BrowserFileTree {
|
|
|
85
95
|
const directory = await this.getDirectory();
|
|
86
96
|
let keys = [];
|
|
87
97
|
// @ts-ignore
|
|
88
|
-
for await (const
|
|
98
|
+
for await (const entryKey of directory.keys()) {
|
|
99
|
+
// Check if the entry is a subfolder
|
|
100
|
+
const baseKey = trailingSlash.remove(entryKey);
|
|
101
|
+
const subfolderHandle = await directory
|
|
102
|
+
.getDirectoryHandle(baseKey)
|
|
103
|
+
.catch(() => null);
|
|
104
|
+
const isSubfolder = subfolderHandle !== null;
|
|
105
|
+
|
|
106
|
+
const key = trailingSlash.toggle(entryKey, isSubfolder);
|
|
89
107
|
keys.push(key);
|
|
90
108
|
}
|
|
109
|
+
|
|
91
110
|
// Filter out unhelpful file names.
|
|
92
111
|
keys = keys.filter((key) => !hiddenFileNames.includes(key));
|
|
93
112
|
keys.sort(naturalOrder);
|
|
@@ -96,12 +115,13 @@ export default class BrowserFileTree {
|
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
async set(key, value) {
|
|
118
|
+
const baseKey = trailingSlash.remove(key);
|
|
99
119
|
const directory = await this.getDirectory();
|
|
100
120
|
|
|
101
121
|
if (value === undefined) {
|
|
102
122
|
// Delete file.
|
|
103
123
|
try {
|
|
104
|
-
await directory.removeEntry(
|
|
124
|
+
await directory.removeEntry(baseKey);
|
|
105
125
|
} catch (error) {
|
|
106
126
|
// If the file didn't exist, ignore the error.
|
|
107
127
|
if (
|
|
@@ -133,13 +153,15 @@ export default class BrowserFileTree {
|
|
|
133
153
|
|
|
134
154
|
if (isWriteable) {
|
|
135
155
|
// Write file.
|
|
136
|
-
const fileHandle = await directory.getFileHandle(
|
|
156
|
+
const fileHandle = await directory.getFileHandle(baseKey, {
|
|
157
|
+
create: true,
|
|
158
|
+
});
|
|
137
159
|
const writable = await fileHandle.createWritable();
|
|
138
160
|
await writable.write(value);
|
|
139
161
|
await writable.close();
|
|
140
162
|
} else if (Tree.isTreelike(value)) {
|
|
141
163
|
// Treat value as a tree and write it out as a subdirectory.
|
|
142
|
-
const subdirectory = await directory.getDirectoryHandle(
|
|
164
|
+
const subdirectory = await directory.getDirectoryHandle(baseKey, {
|
|
143
165
|
create: true,
|
|
144
166
|
});
|
|
145
167
|
const destTree = Reflect.construct(this.constructor, [subdirectory]);
|
package/src/DeepMapTree.js
CHANGED
|
@@ -16,8 +16,8 @@ export default class DeepMapTree extends MapTree {
|
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
/** @returns {boolean} */
|
|
20
|
+
isSubtree(value) {
|
|
21
21
|
return value instanceof Map || Tree.isAsyncTree(value);
|
|
22
22
|
}
|
|
23
23
|
}
|
package/src/DeepObjectTree.js
CHANGED
|
@@ -4,19 +4,16 @@ import { isPlainObject } from "./utilities.js";
|
|
|
4
4
|
export default class DeepObjectTree extends ObjectTree {
|
|
5
5
|
async get(key) {
|
|
6
6
|
let value = await super.get(key);
|
|
7
|
-
|
|
8
|
-
const isPlain =
|
|
9
|
-
value instanceof Array ||
|
|
10
|
-
(isPlainObject(value) && !Tree.isAsyncTree(value));
|
|
11
|
-
if (isPlain) {
|
|
7
|
+
if (value instanceof Array || isPlainObject(value)) {
|
|
12
8
|
value = Reflect.construct(this.constructor, [value]);
|
|
13
9
|
}
|
|
14
|
-
|
|
15
10
|
return value;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
13
|
+
/** @returns {boolean} */
|
|
14
|
+
isSubtree(value) {
|
|
15
|
+
return (
|
|
16
|
+
value instanceof Array || isPlainObject(value) || Tree.isAsyncTree(value)
|
|
17
|
+
);
|
|
21
18
|
}
|
|
22
19
|
}
|
package/src/DeferredTree.js
CHANGED
|
@@ -14,12 +14,14 @@ import { Tree } from "./internal.js";
|
|
|
14
14
|
export default class DeferredTree {
|
|
15
15
|
/**
|
|
16
16
|
* @param {Function|Promise<any>} loader
|
|
17
|
+
* @param {{ deep?: boolean }} [options]
|
|
17
18
|
*/
|
|
18
|
-
constructor(loader) {
|
|
19
|
+
constructor(loader, options) {
|
|
19
20
|
this.loader = loader;
|
|
20
21
|
this.treePromise = null;
|
|
21
22
|
this._tree = null;
|
|
22
23
|
this._parentUntilLoaded = null;
|
|
24
|
+
this._deep = options?.deep ?? false;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
async get(key) {
|
|
@@ -60,7 +62,7 @@ export default class DeferredTree {
|
|
|
60
62
|
|
|
61
63
|
// Use a promise to ensure the treelike is only converted to a tree once.
|
|
62
64
|
this.treePromise ??= this.loadResult().then((treelike) => {
|
|
63
|
-
this._tree = Tree.from(treelike);
|
|
65
|
+
this._tree = Tree.from(treelike, { deep: this._deep });
|
|
64
66
|
if (this._parentUntilLoaded) {
|
|
65
67
|
// Now that the tree has been loaded, we can set its parent if it hasn't
|
|
66
68
|
// already been set.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import SiteTree from "./SiteTree.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A [SiteTree](SiteTree.html) that implements the [JSON Keys](jsonKeys.html)
|
|
5
|
+
* protocol. This enables a `keys()` method that can return the keys of a site
|
|
6
|
+
* route even though such a mechanism is not built into the HTTP protocol.
|
|
7
|
+
*/
|
|
8
|
+
export default class ExplorableSiteTree extends SiteTree {
|
|
9
|
+
constructor(...args) {
|
|
10
|
+
super(...args);
|
|
11
|
+
this.serverKeysPromise = undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getServerKeys() {
|
|
15
|
+
// We use a promise to ensure we only check for keys once.
|
|
16
|
+
const href = new URL(".keys.json", this.href).href;
|
|
17
|
+
this.serverKeysPromise ??= fetch(href)
|
|
18
|
+
.then((response) => (response.ok ? response.text() : null))
|
|
19
|
+
.then((text) => {
|
|
20
|
+
try {
|
|
21
|
+
return text ? JSON.parse(text) : null;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Got a response, but it's not JSON. Most likely the site doesn't
|
|
24
|
+
// actually have a .keys.json file, and is returning a Not Found page,
|
|
25
|
+
// but hasn't set the correct 404 status code.
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return this.serverKeysPromise;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns the keys of the site route. For this to work, the route must have a
|
|
34
|
+
* `.keys.json` file that contains a JSON array of string keys.
|
|
35
|
+
*
|
|
36
|
+
* @returns {Promise<Iterable<string>>}
|
|
37
|
+
*/
|
|
38
|
+
async keys() {
|
|
39
|
+
const serverKeys = await this.getServerKeys();
|
|
40
|
+
return serverKeys ?? [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
processResponse(response) {
|
|
44
|
+
// If the response was redirected to a route that ends with a slash, and the
|
|
45
|
+
// site is an explorable site, we return a tree for the new route.
|
|
46
|
+
if (response.ok && response.redirected && response.url.endsWith("/")) {
|
|
47
|
+
return Reflect.construct(this.constructor, [response.url]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return super.processResponse(response);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/FileTree.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { Tree } from "./internal.js";
|
|
5
|
+
import * as trailingSlash from "./trailingSlash.js";
|
|
5
6
|
import {
|
|
6
7
|
getRealmObjectPrototype,
|
|
7
8
|
hiddenFileNames,
|
|
@@ -50,12 +51,18 @@ export default class FileTree {
|
|
|
50
51
|
|
|
51
52
|
async get(key) {
|
|
52
53
|
if (key == null) {
|
|
53
|
-
// Reject nullish key
|
|
54
|
+
// Reject nullish key
|
|
54
55
|
throw new ReferenceError(
|
|
55
56
|
`${this.constructor.name}: Cannot get a null or undefined key.`
|
|
56
57
|
);
|
|
57
58
|
}
|
|
59
|
+
if (key === "") {
|
|
60
|
+
// Can't have a file with no name
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
58
63
|
|
|
64
|
+
// Remove trailing slash if present
|
|
65
|
+
key = trailingSlash.remove(key);
|
|
59
66
|
const filePath = path.resolve(this.dirname, key);
|
|
60
67
|
|
|
61
68
|
let stats;
|
|
@@ -73,7 +80,7 @@ export default class FileTree {
|
|
|
73
80
|
// Return subdirectory as a tree
|
|
74
81
|
value = Reflect.construct(this.constructor, [filePath]);
|
|
75
82
|
} else {
|
|
76
|
-
// Return file contents as a standard Uint8Array
|
|
83
|
+
// Return file contents as a standard Uint8Array
|
|
77
84
|
const buffer = await fs.readFile(filePath);
|
|
78
85
|
value = Uint8Array.from(buffer);
|
|
79
86
|
}
|
|
@@ -82,12 +89,6 @@ export default class FileTree {
|
|
|
82
89
|
return value;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
async isKeyForSubtree(key) {
|
|
86
|
-
const filePath = path.join(this.dirname, key);
|
|
87
|
-
const stats = await stat(filePath);
|
|
88
|
-
return stats ? stats.isDirectory() : false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
92
|
/**
|
|
92
93
|
* Enumerate the names of the files/subdirectories in this directory.
|
|
93
94
|
*/
|
|
@@ -102,7 +103,9 @@ export default class FileTree {
|
|
|
102
103
|
entries = [];
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
let names = entries.map((entry) =>
|
|
106
|
+
let names = entries.map((entry) =>
|
|
107
|
+
trailingSlash.toggle(entry.name, entry.isDirectory())
|
|
108
|
+
);
|
|
106
109
|
|
|
107
110
|
// Filter out unhelpful file names.
|
|
108
111
|
names = names.filter((name) => !hiddenFileNames.includes(name));
|
|
@@ -121,7 +124,8 @@ export default class FileTree {
|
|
|
121
124
|
async set(key, value) {
|
|
122
125
|
// Where are we going to write this value?
|
|
123
126
|
const stringKey = key != null ? String(key) : "";
|
|
124
|
-
const
|
|
127
|
+
const baseKey = trailingSlash.remove(stringKey);
|
|
128
|
+
const destPath = path.resolve(this.dirname, baseKey);
|
|
125
129
|
|
|
126
130
|
if (value === undefined) {
|
|
127
131
|
// Delete the file or directory.
|
package/src/MapTree.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
|
+
import * as trailingSlash from "./trailingSlash.js";
|
|
2
3
|
import { setParent } from "./utilities.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -23,18 +24,40 @@ export default class MapTree {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
async get(key) {
|
|
26
|
-
|
|
27
|
+
// Try key as is
|
|
28
|
+
let value = this.map.get(key);
|
|
29
|
+
if (value === undefined) {
|
|
30
|
+
// Try the other variation of the key
|
|
31
|
+
const alternateKey = trailingSlash.toggle(key);
|
|
32
|
+
value = this.map.get(alternateKey);
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
// Key doesn't exist
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
value = await value;
|
|
40
|
+
|
|
41
|
+
if (value === undefined) {
|
|
42
|
+
// Key exists but value is undefined
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
setParent(value, this);
|
|
28
47
|
return value;
|
|
29
48
|
}
|
|
30
49
|
|
|
31
|
-
|
|
32
|
-
|
|
50
|
+
/** @returns {boolean} */
|
|
51
|
+
isSubtree(value) {
|
|
33
52
|
return Tree.isAsyncTree(value);
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
async keys() {
|
|
37
|
-
|
|
56
|
+
const keys = [];
|
|
57
|
+
for (const [key, value] of this.map.entries()) {
|
|
58
|
+
keys.push(trailingSlash.toggle(key, this.isSubtree(value)));
|
|
59
|
+
}
|
|
60
|
+
return keys;
|
|
38
61
|
}
|
|
39
62
|
|
|
40
63
|
async set(key, value) {
|
package/src/ObjectTree.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
2
|
import * as symbols from "./symbols.js";
|
|
3
|
+
import * as trailingSlash from "./trailingSlash.js";
|
|
3
4
|
import { getRealmObjectPrototype, setParent } from "./utilities.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -32,7 +33,20 @@ export default class ObjectTree {
|
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
// Does the object have the key with or without a trailing slash?
|
|
37
|
+
const existingKey = findExistingKey(this.object, key);
|
|
38
|
+
if (existingKey === null) {
|
|
39
|
+
// Key doesn't exist
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let value = await this.object[existingKey];
|
|
44
|
+
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
// Key exists but value is undefined
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
setParent(value, this);
|
|
37
51
|
|
|
38
52
|
if (typeof value === "function" && !Object.hasOwn(this.object, key)) {
|
|
@@ -43,8 +57,8 @@ export default class ObjectTree {
|
|
|
43
57
|
return value;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
/** @returns {boolean} */
|
|
61
|
+
isSubtree(value) {
|
|
48
62
|
return Tree.isAsyncTree(value);
|
|
49
63
|
}
|
|
50
64
|
|
|
@@ -67,7 +81,17 @@ export default class ObjectTree {
|
|
|
67
81
|
(descriptor.enumerable ||
|
|
68
82
|
(descriptor.get !== undefined && descriptor.set !== undefined))
|
|
69
83
|
)
|
|
70
|
-
.map(([name]) =>
|
|
84
|
+
.map(([name, descriptor]) =>
|
|
85
|
+
trailingSlash.has(name)
|
|
86
|
+
? // Preserve existing slash
|
|
87
|
+
name
|
|
88
|
+
: // Add a slash if the value is a plain property and a subtree
|
|
89
|
+
trailingSlash.toggle(
|
|
90
|
+
name,
|
|
91
|
+
descriptor.value !== undefined &&
|
|
92
|
+
this.isSubtree(descriptor.value)
|
|
93
|
+
)
|
|
94
|
+
);
|
|
71
95
|
for (const name of propertyNames) {
|
|
72
96
|
result.add(name);
|
|
73
97
|
}
|
|
@@ -83,13 +107,36 @@ export default class ObjectTree {
|
|
|
83
107
|
* @param {any} value
|
|
84
108
|
*/
|
|
85
109
|
async set(key, value) {
|
|
110
|
+
const existingKey = findExistingKey(this.object, key);
|
|
111
|
+
|
|
86
112
|
if (value === undefined) {
|
|
87
|
-
// Delete the key.
|
|
88
|
-
|
|
113
|
+
// Delete the key if it exists.
|
|
114
|
+
if (existingKey !== null) {
|
|
115
|
+
delete this.object[existingKey];
|
|
116
|
+
}
|
|
89
117
|
} else {
|
|
118
|
+
// If the key exists under a different form, delete the existing key.
|
|
119
|
+
if (existingKey !== null && existingKey !== key) {
|
|
120
|
+
delete this.object[existingKey];
|
|
121
|
+
}
|
|
122
|
+
|
|
90
123
|
// Set the value for the key.
|
|
91
124
|
this.object[key] = value;
|
|
92
125
|
}
|
|
126
|
+
|
|
93
127
|
return this;
|
|
94
128
|
}
|
|
95
129
|
}
|
|
130
|
+
|
|
131
|
+
function findExistingKey(object, key) {
|
|
132
|
+
// First try key as is
|
|
133
|
+
if (key in object) {
|
|
134
|
+
return key;
|
|
135
|
+
}
|
|
136
|
+
// Try alternate form
|
|
137
|
+
const alternateKey = trailingSlash.toggle(key);
|
|
138
|
+
if (alternateKey in object) {
|
|
139
|
+
return alternateKey;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
package/src/SetTree.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Tree } from "./internal.js";
|
|
2
1
|
import { setParent } from "./utilities.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -29,11 +28,6 @@ export default class SetTree {
|
|
|
29
28
|
return value;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
async isKeyForSubtree(key) {
|
|
33
|
-
const value = this.values[key];
|
|
34
|
-
return Tree.isAsyncTree(value);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
31
|
async keys() {
|
|
38
32
|
return this.values.keys();
|
|
39
33
|
}
|
package/src/SiteTree.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as jsonKeys from "./jsonKeys.js";
|
|
1
|
+
import * as trailingSlash from "./trailingSlash.js";
|
|
3
2
|
import { setParent } from "./utilities.js";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -20,16 +19,14 @@ export default class SiteTree {
|
|
|
20
19
|
href = new URL(href, window.location.href).href;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
href += "/";
|
|
26
|
-
}
|
|
22
|
+
// Add trailing slash if not present; URL should represent a directory.
|
|
23
|
+
href = trailingSlash.add(href);
|
|
27
24
|
|
|
28
25
|
this.href = href;
|
|
29
|
-
this.keysPromise = undefined;
|
|
30
26
|
this.parent = null;
|
|
31
27
|
}
|
|
32
28
|
|
|
29
|
+
/** @returns {Promise<any>} */
|
|
33
30
|
async get(key) {
|
|
34
31
|
if (key == null) {
|
|
35
32
|
// Reject nullish key.
|
|
@@ -38,25 +35,16 @@ export default class SiteTree {
|
|
|
38
35
|
);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
// If
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// that necessitates an extra network request per SiteTree instance. In many
|
|
45
|
-
// cases, that can be avoided.
|
|
46
|
-
if (key === "" && (await this.hasKeysJson())) {
|
|
47
|
-
key = "index.html";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const href = new URL(key, this.href).href;
|
|
51
|
-
|
|
52
|
-
// If the (possibly adjusted) route ends with a slash and the site is an
|
|
53
|
-
// explorable site, we return a tree for the indicated route.
|
|
54
|
-
if (href.endsWith("/") && (await this.hasKeysJson())) {
|
|
38
|
+
// If the keys ends with a slash, return a tree for the indicated route.
|
|
39
|
+
if (trailingSlash.has(key)) {
|
|
40
|
+
const href = new URL(key, this.href).href;
|
|
55
41
|
const value = Reflect.construct(this.constructor, [href]);
|
|
56
42
|
setParent(value, this);
|
|
57
43
|
return value;
|
|
58
44
|
}
|
|
59
45
|
|
|
46
|
+
const href = new URL(key, this.href).href;
|
|
47
|
+
|
|
60
48
|
// Fetch the data at the given route.
|
|
61
49
|
let response;
|
|
62
50
|
try {
|
|
@@ -64,65 +52,20 @@ export default class SiteTree {
|
|
|
64
52
|
} catch (error) {
|
|
65
53
|
return undefined;
|
|
66
54
|
}
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (response.redirected && response.url.endsWith("/")) {
|
|
72
|
-
// If the response is redirected to a route that ends with a slash, and
|
|
73
|
-
// the site is an explorable site, we return a tree for the new route.
|
|
74
|
-
if (await this.hasKeysJson()) {
|
|
75
|
-
return Reflect.construct(this.constructor, [response.url]);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const mediaType = response.headers?.get("Content-Type");
|
|
80
|
-
if (SiteTree.mediaTypeIsText(mediaType)) {
|
|
81
|
-
return response.text();
|
|
82
|
-
} else {
|
|
83
|
-
const buffer = response.arrayBuffer();
|
|
84
|
-
setParent(buffer, this);
|
|
85
|
-
return buffer;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async getKeyDictionary() {
|
|
90
|
-
// We use a promise to ensure we only check for keys once.
|
|
91
|
-
const href = new URL(".keys.json", this.href).href;
|
|
92
|
-
this.keysPromise ??= fetch(href)
|
|
93
|
-
.then((response) => (response.ok ? response.text() : null))
|
|
94
|
-
.then((text) => {
|
|
95
|
-
try {
|
|
96
|
-
return text ? jsonKeys.parse(text) : null;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
// Got a response, but it's not JSON. Most likely the site doesn't
|
|
99
|
-
// actually have a .keys.json file, and is returning a Not Found page,
|
|
100
|
-
// but hasn't set the correct 404 status code.
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
return this.keysPromise;
|
|
105
|
-
}
|
|
106
55
|
|
|
107
|
-
|
|
108
|
-
const keyDictionary = await this.getKeyDictionary();
|
|
109
|
-
return keyDictionary !== null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async isKeyForSubtree(key) {
|
|
113
|
-
const keyDictionary = await this.getKeyDictionary();
|
|
114
|
-
if (keyDictionary) {
|
|
115
|
-
return keyDictionary[key];
|
|
116
|
-
} else {
|
|
117
|
-
// Expensive check, since this fetches the key's value.
|
|
118
|
-
const value = await this.get(key);
|
|
119
|
-
return Tree.isAsyncTree(value);
|
|
120
|
-
}
|
|
56
|
+
return this.processResponse(response);
|
|
121
57
|
}
|
|
122
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Returns an empty set of keys.
|
|
61
|
+
*
|
|
62
|
+
* For a variation of `SiteTree` that can return the keys for a site route,
|
|
63
|
+
* see [ExplorableSiteTree](ExplorableSiteTree.html).
|
|
64
|
+
*
|
|
65
|
+
* @returns {Promise<Iterable<string>>}
|
|
66
|
+
*/
|
|
123
67
|
async keys() {
|
|
124
|
-
|
|
125
|
-
return keyDictionary ? Object.keys(keyDictionary) : [];
|
|
68
|
+
return [];
|
|
126
69
|
}
|
|
127
70
|
|
|
128
71
|
// Return true if the given media type is a standard text type.
|
|
@@ -155,15 +98,19 @@ export default class SiteTree {
|
|
|
155
98
|
return this.href;
|
|
156
99
|
}
|
|
157
100
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
101
|
+
processResponse(response) {
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const mediaType = response.headers?.get("Content-Type");
|
|
107
|
+
if (SiteTree.mediaTypeIsText(mediaType)) {
|
|
108
|
+
return response.text();
|
|
109
|
+
} else {
|
|
110
|
+
const buffer = response.arrayBuffer();
|
|
111
|
+
setParent(buffer, this);
|
|
112
|
+
return buffer;
|
|
113
|
+
}
|
|
167
114
|
}
|
|
168
115
|
|
|
169
116
|
get url() {
|
package/src/Tree.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ export function from(obj: any, options?: { deep?: boolean, parent?: AsyncTree|nu
|
|
|
9
9
|
export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
|
|
10
10
|
export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
|
|
11
11
|
export function isAsyncTree(obj: any): obj is AsyncTree;
|
|
12
|
-
export function isKeyForSubtree(tree: AsyncTree, obj: any): Promise<boolean>;
|
|
13
12
|
export function isTraversable(obj: any): boolean;
|
|
14
13
|
export function isTreelike(obj: any): obj is Treelike;
|
|
15
14
|
export function map(tree: Treelike, valueFn: ValueKeyFn): AsyncTree;
|