@weborigami/async-tree 0.0.66-beta.1 → 0.0.66-beta.2
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/FileTree.js +14 -10
- package/src/MapTree.js +27 -4
- package/src/ObjectTree.js +53 -6
- package/src/OpenSiteTree.js +41 -0
- package/src/SetTree.js +0 -6
- package/src/SiteTree.js +24 -85
- package/src/Tree.d.ts +0 -1
- package/src/Tree.js +18 -38
- 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/FileTree.test.js +14 -7
- package/test/MapTree.test.js +21 -0
- package/test/ObjectTree.test.js +16 -12
- package/test/OpenSiteTree.test.js +113 -0
- 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
|
@@ -8,6 +8,7 @@ export { default as MapTree } from "./src/MapTree.js";
|
|
|
8
8
|
export { default as DeepMapTree } from "./src/DeepMapTree.js";
|
|
9
9
|
export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
|
|
10
10
|
export * as jsonKeys from "./src/jsonKeys.js";
|
|
11
|
+
export { default as OpenSiteTree } from "./src/OpenSiteTree.js";
|
|
11
12
|
export { default as cache } from "./src/operations/cache.js";
|
|
12
13
|
export { default as concat } from "./src/operations/concat.js";
|
|
13
14
|
export { default as deepMerge } from "./src/operations/deepMerge.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-beta.
|
|
3
|
+
"version": "0.0.66-beta.2",
|
|
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-beta.
|
|
14
|
+
"@weborigami/types": "0.0.66-beta.2"
|
|
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/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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import SiteTree from "./SiteTree.js";
|
|
2
|
+
|
|
3
|
+
export default class OpenSiteTree extends SiteTree {
|
|
4
|
+
constructor(...args) {
|
|
5
|
+
super(...args);
|
|
6
|
+
this.serverKeysPromise = undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async getServerKeys() {
|
|
10
|
+
// We use a promise to ensure we only check for keys once.
|
|
11
|
+
const href = new URL(".keys.json", this.href).href;
|
|
12
|
+
this.serverKeysPromise ??= fetch(href)
|
|
13
|
+
.then((response) => (response.ok ? response.text() : null))
|
|
14
|
+
.then((text) => {
|
|
15
|
+
try {
|
|
16
|
+
return text ? JSON.parse(text) : null;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
// Got a response, but it's not JSON. Most likely the site doesn't
|
|
19
|
+
// actually have a .keys.json file, and is returning a Not Found page,
|
|
20
|
+
// but hasn't set the correct 404 status code.
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return this.serverKeysPromise;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async keys() {
|
|
28
|
+
const serverKeys = await this.getServerKeys();
|
|
29
|
+
return serverKeys ?? [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
processResponse(response) {
|
|
33
|
+
// If the response was redirected to a route that ends with a slash, and the
|
|
34
|
+
// site is an explorable site, we return a tree for the new route.
|
|
35
|
+
if (response.ok && response.redirected && response.url.endsWith("/")) {
|
|
36
|
+
return Reflect.construct(this.constructor, [response.url]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return super.processResponse(response);
|
|
40
|
+
}
|
|
41
|
+
}
|
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,12 @@ 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
|
|
|
123
59
|
async keys() {
|
|
124
|
-
|
|
125
|
-
return keyDictionary ? Object.keys(keyDictionary) : [];
|
|
60
|
+
return [];
|
|
126
61
|
}
|
|
127
62
|
|
|
128
63
|
// Return true if the given media type is a standard text type.
|
|
@@ -155,15 +90,19 @@ export default class SiteTree {
|
|
|
155
90
|
return this.href;
|
|
156
91
|
}
|
|
157
92
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
93
|
+
processResponse(response) {
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const mediaType = response.headers?.get("Content-Type");
|
|
99
|
+
if (SiteTree.mediaTypeIsText(mediaType)) {
|
|
100
|
+
return response.text();
|
|
101
|
+
} else {
|
|
102
|
+
const buffer = response.arrayBuffer();
|
|
103
|
+
setParent(buffer, this);
|
|
104
|
+
return buffer;
|
|
105
|
+
}
|
|
167
106
|
}
|
|
168
107
|
|
|
169
108
|
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;
|