@weborigami/async-tree 0.5.8 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/browser.js +1 -1
  2. package/index.ts +43 -35
  3. package/main.js +1 -2
  4. package/package.json +4 -7
  5. package/shared.js +74 -12
  6. package/src/Tree.js +11 -2
  7. package/src/drivers/AsyncMap.js +237 -0
  8. package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +54 -38
  9. package/src/drivers/{calendarTree.js → CalendarMap.js} +80 -63
  10. package/src/drivers/ConstantMap.js +28 -0
  11. package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
  12. package/src/drivers/FileMap.js +238 -0
  13. package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
  14. package/src/drivers/ObjectMap.js +151 -0
  15. package/src/drivers/SetMap.js +13 -0
  16. package/src/drivers/{SiteTree.js → SiteMap.js} +17 -20
  17. package/src/drivers/SyncMap.js +260 -0
  18. package/src/jsonKeys.d.ts +2 -2
  19. package/src/jsonKeys.js +20 -5
  20. package/src/operations/addNextPrevious.js +35 -36
  21. package/src/operations/assign.js +27 -23
  22. package/src/operations/cache.js +30 -36
  23. package/src/operations/cachedKeyFunctions.js +1 -1
  24. package/src/operations/calendar.js +5 -0
  25. package/src/operations/child.js +35 -0
  26. package/src/operations/clear.js +13 -12
  27. package/src/operations/constant.js +5 -0
  28. package/src/operations/deepEntries.js +23 -0
  29. package/src/operations/deepMap.js +9 -9
  30. package/src/operations/deepMerge.js +36 -25
  31. package/src/operations/deepReverse.js +23 -16
  32. package/src/operations/deepTake.js +7 -7
  33. package/src/operations/deepText.js +4 -4
  34. package/src/operations/deepValues.js +3 -6
  35. package/src/operations/deepValuesIterator.js +11 -11
  36. package/src/operations/delete.js +8 -12
  37. package/src/operations/entries.js +17 -10
  38. package/src/operations/filter.js +9 -7
  39. package/src/operations/first.js +12 -10
  40. package/src/operations/forEach.js +10 -13
  41. package/src/operations/from.js +30 -39
  42. package/src/operations/globKeys.js +22 -17
  43. package/src/operations/group.js +2 -2
  44. package/src/operations/groupBy.js +24 -22
  45. package/src/operations/has.js +7 -9
  46. package/src/operations/indent.js +2 -2
  47. package/src/operations/inners.js +19 -15
  48. package/src/operations/invokeFunctions.js +22 -10
  49. package/src/operations/isAsyncMutableTree.js +5 -12
  50. package/src/operations/isAsyncTree.js +5 -20
  51. package/src/operations/isMap.js +39 -0
  52. package/src/operations/isMaplike.js +34 -0
  53. package/src/operations/isReadOnlyMap.js +14 -0
  54. package/src/operations/isTraversable.js +3 -3
  55. package/src/operations/isTreelike.js +5 -30
  56. package/src/operations/json.js +4 -12
  57. package/src/operations/keys.js +17 -8
  58. package/src/operations/length.js +9 -8
  59. package/src/operations/map.js +28 -30
  60. package/src/operations/mapExtension.js +20 -16
  61. package/src/operations/mapReduce.js +38 -24
  62. package/src/operations/mask.js +54 -29
  63. package/src/operations/match.js +13 -9
  64. package/src/operations/merge.js +43 -35
  65. package/src/operations/paginate.js +26 -18
  66. package/src/operations/parent.js +7 -7
  67. package/src/operations/paths.js +20 -22
  68. package/src/operations/plain.js +6 -6
  69. package/src/operations/reduce.js +16 -0
  70. package/src/operations/regExpKeys.js +21 -12
  71. package/src/operations/reverse.js +21 -15
  72. package/src/operations/root.js +6 -5
  73. package/src/operations/scope.js +31 -26
  74. package/src/operations/set.js +20 -0
  75. package/src/operations/shuffle.js +23 -16
  76. package/src/operations/size.js +13 -0
  77. package/src/operations/sort.js +55 -40
  78. package/src/operations/sync.js +14 -0
  79. package/src/operations/take.js +23 -11
  80. package/src/operations/text.js +4 -4
  81. package/src/operations/toFunction.js +7 -7
  82. package/src/operations/traverse.js +4 -4
  83. package/src/operations/traverseOrThrow.js +18 -9
  84. package/src/operations/traversePath.js +2 -2
  85. package/src/operations/values.js +18 -9
  86. package/src/operations/withKeys.js +22 -16
  87. package/src/symbols.js +1 -0
  88. package/src/utilities/castArraylike.js +24 -13
  89. package/src/utilities/getMapArgument.js +38 -0
  90. package/src/utilities/getParent.js +2 -2
  91. package/src/utilities/isStringlike.js +7 -5
  92. package/src/utilities/setParent.js +7 -7
  93. package/src/utilities/toFunction.js +2 -2
  94. package/src/utilities/toPlainValue.js +21 -19
  95. package/test/SampleAsyncMap.js +34 -0
  96. package/test/browser/assert.js +20 -0
  97. package/test/browser/index.html +53 -21
  98. package/test/drivers/AsyncMap.test.js +119 -0
  99. package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +50 -33
  100. package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
  101. package/test/drivers/ConstantMap.test.js +15 -0
  102. package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
  103. package/test/drivers/FileMap.test.js +156 -0
  104. package/test/drivers/FunctionMap.test.js +56 -0
  105. package/test/drivers/ObjectMap.test.js +194 -0
  106. package/test/drivers/SetMap.test.js +35 -0
  107. package/test/drivers/{SiteTree.test.js → SiteMap.test.js} +14 -10
  108. package/test/drivers/SyncMap.test.js +335 -0
  109. package/test/jsonKeys.test.js +18 -6
  110. package/test/operations/addNextPrevious.test.js +3 -2
  111. package/test/operations/assign.test.js +30 -35
  112. package/test/operations/cache.test.js +17 -12
  113. package/test/operations/cachedKeyFunctions.test.js +12 -9
  114. package/test/operations/child.test.js +34 -0
  115. package/test/operations/clear.test.js +6 -27
  116. package/test/operations/deepEntries.test.js +32 -0
  117. package/test/operations/deepMerge.test.js +23 -16
  118. package/test/operations/deepReverse.test.js +2 -2
  119. package/test/operations/deepTake.test.js +2 -2
  120. package/test/operations/deepText.test.js +4 -4
  121. package/test/operations/deepValuesIterator.test.js +2 -2
  122. package/test/operations/delete.test.js +2 -2
  123. package/test/operations/extensionKeyFunctions.test.js +6 -5
  124. package/test/operations/filter.test.js +3 -3
  125. package/test/operations/from.test.js +25 -31
  126. package/test/operations/globKeys.test.js +9 -9
  127. package/test/operations/groupBy.test.js +6 -5
  128. package/test/operations/inners.test.js +17 -14
  129. package/test/operations/invokeFunctions.test.js +2 -2
  130. package/test/operations/isMap.test.js +15 -0
  131. package/test/operations/isMaplike.test.js +15 -0
  132. package/test/operations/json.test.js +2 -2
  133. package/test/operations/keys.test.js +16 -3
  134. package/test/operations/map.test.js +40 -30
  135. package/test/operations/mapExtension.test.js +6 -6
  136. package/test/operations/mapReduce.test.js +14 -12
  137. package/test/operations/mask.test.js +16 -3
  138. package/test/operations/match.test.js +2 -2
  139. package/test/operations/merge.test.js +20 -14
  140. package/test/operations/paginate.test.js +5 -5
  141. package/test/operations/parent.test.js +3 -3
  142. package/test/operations/paths.test.js +20 -27
  143. package/test/operations/plain.test.js +8 -8
  144. package/test/operations/regExpKeys.test.js +22 -18
  145. package/test/operations/reverse.test.js +4 -3
  146. package/test/operations/scope.test.js +6 -5
  147. package/test/operations/set.test.js +11 -0
  148. package/test/operations/shuffle.test.js +3 -2
  149. package/test/operations/sort.test.js +7 -10
  150. package/test/operations/sync.test.js +43 -0
  151. package/test/operations/take.test.js +2 -2
  152. package/test/operations/toFunction.test.js +2 -2
  153. package/test/operations/traverse.test.js +17 -5
  154. package/test/operations/withKeys.test.js +2 -2
  155. package/test/utilities/castArrayLike.test.js +53 -0
  156. package/test/utilities/setParent.test.js +6 -6
  157. package/test/utilities/toFunction.test.js +2 -2
  158. package/test/utilities/toPlainValue.test.js +51 -12
  159. package/src/drivers/DeepMapTree.js +0 -23
  160. package/src/drivers/DeepObjectTree.js +0 -18
  161. package/src/drivers/DeferredTree.js +0 -81
  162. package/src/drivers/FileTree.js +0 -276
  163. package/src/drivers/MapTree.js +0 -70
  164. package/src/drivers/ObjectTree.js +0 -158
  165. package/src/drivers/SetTree.js +0 -34
  166. package/src/drivers/constantTree.js +0 -19
  167. package/src/drivers/limitConcurrency.js +0 -63
  168. package/src/internal.js +0 -16
  169. package/src/utilities/getTreeArgument.js +0 -43
  170. package/test/drivers/DeepMapTree.test.js +0 -17
  171. package/test/drivers/DeepObjectTree.test.js +0 -35
  172. package/test/drivers/DeferredTree.test.js +0 -22
  173. package/test/drivers/FileTree.test.js +0 -192
  174. package/test/drivers/FunctionTree.test.js +0 -46
  175. package/test/drivers/MapTree.test.js +0 -59
  176. package/test/drivers/ObjectTree.test.js +0 -163
  177. package/test/drivers/SetTree.test.js +0 -44
  178. package/test/drivers/constantTree.test.js +0 -13
  179. package/test/drivers/limitConcurrency.test.js +0 -41
  180. package/test/operations/isAsyncMutableTree.test.js +0 -17
  181. package/test/operations/isAsyncTree.test.js +0 -26
  182. package/test/operations/isTreelike.test.js +0 -13
@@ -1,20 +1,14 @@
1
1
  import setParent from "../utilities/setParent.js";
2
+ import SyncMap from "./SyncMap.js";
2
3
 
3
- /**
4
- * A tree defined by a function and an optional domain.
5
- *
6
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
- * @implements {AsyncTree}
8
- */
9
- export default class FunctionTree {
10
- /**
11
- * @param {function} fn the key->value function
12
- * @param {Iterable<any>} [domain] optional domain of the function
13
- */
4
+ export default class FunctionMap extends SyncMap {
14
5
  constructor(fn, domain = []) {
6
+ if (typeof fn !== "function") {
7
+ throw new TypeError("FunctionMap: first argument must be a function");
8
+ }
9
+ super();
15
10
  this.fn = fn;
16
11
  this.domain = domain;
17
- this.parent = null;
18
12
  }
19
13
 
20
14
  /**
@@ -22,25 +16,28 @@ export default class FunctionTree {
22
16
  *
23
17
  * @param {any} key
24
18
  */
25
- async get(key) {
26
- const value =
19
+ get(key) {
20
+ let value =
27
21
  this.fn.length <= 1
28
22
  ? // Function takes no arguments, one argument, or a variable number of
29
23
  // arguments: invoke it.
30
- await this.fn(key)
24
+ this.fn(key)
31
25
  : // Bind the key to the first parameter. Subsequent get calls will
32
26
  // eventually bind all parameters until only one remains. At that point,
33
27
  // the above condition will apply and the function will be invoked.
34
28
  Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
35
- setParent(value, this);
29
+ if (value instanceof Promise) {
30
+ value = value.then((v) => {
31
+ setParent(v, this);
32
+ return v;
33
+ });
34
+ } else {
35
+ setParent(value, this);
36
+ }
36
37
  return value;
37
38
  }
38
39
 
39
- /**
40
- * Enumerates the function's domain (if defined) as the tree's keys. If no domain
41
- * was defined, this returns an empty iterator.
42
- */
43
- async keys() {
44
- return this.domain;
40
+ keys() {
41
+ return this.domain[Symbol.iterator]();
45
42
  }
46
43
  }
@@ -0,0 +1,151 @@
1
+ import * as symbols from "../symbols.js";
2
+ import * as trailingSlash from "../trailingSlash.js";
3
+ import isPlainObject from "../utilities/isPlainObject.js";
4
+ import setParent from "../utilities/setParent.js";
5
+ import SyncMap from "./SyncMap.js";
6
+
7
+ /**
8
+ * Map wrapper for a JavaScript object or array
9
+ */
10
+ export default class ObjectMap extends SyncMap {
11
+ /**
12
+ * Wrap the given object in a `Map`.
13
+ *
14
+ * @param {any} object
15
+ * @param {{ deep?: boolean }} [options]
16
+ */
17
+ constructor(object = {}, options = {}) {
18
+ super();
19
+ // Note: we use `typeof` here instead of `instanceof Object` to allow for
20
+ // objects such as Node's `Module` class for representing an ES module.
21
+ if (typeof object !== "object" || object === null) {
22
+ throw new TypeError(
23
+ `${this.constructor.name}: Expected an object or array.`
24
+ );
25
+ }
26
+ this.object = object;
27
+ this.parent = object[symbols.parent] ?? null;
28
+ this.deep = options.deep === true;
29
+ }
30
+
31
+ delete(key) {
32
+ const existingKey = findExistingKey(this.object, key);
33
+ if (existingKey === null) {
34
+ return false;
35
+ }
36
+ delete this.object[existingKey];
37
+ return true;
38
+ }
39
+
40
+ get(key) {
41
+ // Does the object have the key with or without a trailing slash?
42
+ const existingKey = findExistingKey(this.object, key);
43
+ if (existingKey === null) {
44
+ // Key doesn't exist
45
+ return undefined;
46
+ }
47
+
48
+ let value = this.object[existingKey];
49
+
50
+ if (value === undefined) {
51
+ // Key exists but value is undefined
52
+ return undefined;
53
+ }
54
+
55
+ setParent(value, this);
56
+
57
+ // Is value an instance method?
58
+ const isInstanceMethod =
59
+ value instanceof Function && !Object.hasOwn(this.object, key);
60
+ if (isInstanceMethod) {
61
+ // Bind it to the object
62
+ value = value.bind(this.object);
63
+ }
64
+
65
+ if (this.deep && (value instanceof Array || isPlainObject(value))) {
66
+ // Construct submap for sub-objects and sub-arrays
67
+ value = Reflect.construct(this.constructor, [value, { deep: true }]);
68
+ }
69
+
70
+ return value;
71
+ }
72
+
73
+ /** @returns {boolean} */
74
+ isSubtree(value) {
75
+ if (value instanceof Map) {
76
+ return true;
77
+ }
78
+ return this.deep && (value instanceof Array || isPlainObject(value));
79
+ }
80
+
81
+ keys() {
82
+ // Defer to symbols.keys if defined
83
+ if (typeof this.object[symbols.keys] === "function") {
84
+ return this.object[symbols.keys]();
85
+ }
86
+
87
+ const result = new Set();
88
+
89
+ // Walk up the prototype chain
90
+ for (
91
+ let current = this.object;
92
+ current !== null;
93
+ current = Object.getPrototypeOf(current)
94
+ ) {
95
+ // Look at all the properties at this level of the prototype chain
96
+ const descriptors = Object.getOwnPropertyDescriptors(current);
97
+ for (const [name, descriptor] of Object.entries(descriptors)) {
98
+ if (name === "constructor" || name === "__proto__") {
99
+ continue; // Uninteresting property
100
+ }
101
+ // Skip non-enumerable properties unless they have get/set
102
+ if (
103
+ !descriptor.enumerable &&
104
+ descriptor.get === undefined &&
105
+ descriptor.set === undefined
106
+ ) {
107
+ continue;
108
+ }
109
+ // Preserve existing slash; add slash for subtrees
110
+ const key = trailingSlash.has(name)
111
+ ? name
112
+ : trailingSlash.toggle(
113
+ name,
114
+ descriptor.value !== undefined && this.isSubtree(descriptor.value)
115
+ );
116
+ result.add(key);
117
+ }
118
+ }
119
+
120
+ return result[Symbol.iterator]();
121
+ }
122
+
123
+ set(key, value) {
124
+ const existingKey = findExistingKey(this.object, key);
125
+
126
+ // If the key exists under a different form, delete the existing key.
127
+ if (existingKey !== null && existingKey !== key) {
128
+ delete this.object[existingKey];
129
+ }
130
+
131
+ // Set the value for the key.
132
+ this.object[key] = value;
133
+
134
+ return this;
135
+ }
136
+
137
+ trailingSlashKeys = true;
138
+ }
139
+
140
+ function findExistingKey(object, key) {
141
+ // First try key as is
142
+ if (key in object) {
143
+ return key;
144
+ }
145
+ // Try alternate form
146
+ const alternateKey = trailingSlash.toggle(key);
147
+ if (alternateKey in object) {
148
+ return alternateKey;
149
+ }
150
+ return null;
151
+ }
@@ -0,0 +1,13 @@
1
+ import SyncMap from "./SyncMap.js";
2
+
3
+ /**
4
+ * A map of Set objects.
5
+ */
6
+ export default class SetMap extends SyncMap {
7
+ /**
8
+ * @param {Set} set
9
+ */
10
+ constructor(set) {
11
+ super(set.entries());
12
+ }
13
+ }
@@ -1,19 +1,19 @@
1
1
  import * as trailingSlash from "../trailingSlash.js";
2
2
  import setParent from "../utilities/setParent.js";
3
+ import AsyncMap from "./AsyncMap.js";
3
4
 
4
5
  /**
5
6
  * A tree of values obtained via HTTP/HTTPS calls. These values will be strings
6
7
  * for HTTP responses with a MIME text type; otherwise they will be ArrayBuffer
7
8
  * instances.
8
- *
9
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
- * @implements {AsyncTree}
11
9
  */
12
- export default class SiteTree {
10
+ export default class SiteMap extends AsyncMap {
13
11
  /**
14
12
  * @param {string} href
15
13
  */
16
14
  constructor(href = globalThis?.location.href) {
15
+ super();
16
+
17
17
  if (href?.startsWith(".") && globalThis?.location !== undefined) {
18
18
  // URL represents a relative path; concatenate with current location.
19
19
  href = new URL(href, globalThis.location.href).href;
@@ -23,10 +23,9 @@ export default class SiteTree {
23
23
  href = trailingSlash.add(href);
24
24
 
25
25
  this.href = href;
26
- this.parent = null;
27
26
  }
28
27
 
29
- /** @returns {Promise<any>} */
28
+ /** @returns {Promise<ArrayBuffer|string|undefined>} */
30
29
  async get(key) {
31
30
  if (key == null) {
32
31
  // Reject nullish key.
@@ -35,20 +34,15 @@ export default class SiteTree {
35
34
  );
36
35
  }
37
36
 
38
- // A key with a trailing slash and no extension is for a folder; return a
39
- // subtree without making a network request.
40
- if (trailingSlash.has(key) && !key.includes(".")) {
37
+ // A key with a trailing slash is for a folder; return a subtree without
38
+ // making a network request.
39
+ if (trailingSlash.has(key)) {
41
40
  const href = new URL(key, this.href).href;
42
41
  const value = Reflect.construct(this.constructor, [href]);
43
42
  setParent(value, this);
44
43
  return value;
45
44
  }
46
45
 
47
- // HACK: For now we don't allow lookup of Origami extension handlers.
48
- if (key.endsWith(".handler")) {
49
- return undefined;
50
- }
51
-
52
46
  const href = new URL(key, this.href).href;
53
47
 
54
48
  // Fetch the data at the given route.
@@ -65,13 +59,13 @@ export default class SiteTree {
65
59
  /**
66
60
  * Returns an empty set of keys.
67
61
  *
68
- * For a variation of `SiteTree` that can return the keys for a site route,
69
- * see [ExplorableSiteTree](ExplorableSiteTree.html).
62
+ * For a variation of `SiteMap` that can return the keys for a site route,
63
+ * see [ExplorableSiteMap](ExplorableSiteMap.html).
70
64
  *
71
- * @returns {Promise<Iterable<string>>}
65
+ * @returns {AsyncIterableIterator<string>}
72
66
  */
73
- async keys() {
74
- return [];
67
+ async *keys() {
68
+ yield* [];
75
69
  }
76
70
 
77
71
  // Return true if the given media type is a standard text type.
@@ -102,13 +96,14 @@ export default class SiteTree {
102
96
  return this.href;
103
97
  }
104
98
 
99
+ /** @param {Response} response */
105
100
  processResponse(response) {
106
101
  if (!response.ok) {
107
102
  return undefined;
108
103
  }
109
104
 
110
105
  const mediaType = response.headers?.get("Content-Type");
111
- if (SiteTree.mediaTypeIsText(mediaType)) {
106
+ if (SiteMap.mediaTypeIsText(mediaType)) {
112
107
  return response.text();
113
108
  } else {
114
109
  const buffer = response.arrayBuffer();
@@ -117,6 +112,8 @@ export default class SiteTree {
117
112
  }
118
113
  }
119
114
 
115
+ trailingSlashKeys = true;
116
+
120
117
  get url() {
121
118
  return new URL(this.href);
122
119
  }
@@ -0,0 +1,260 @@
1
+ import * as trailingSlash from "../trailingSlash.js";
2
+ import setParent from "../utilities/setParent.js";
3
+
4
+ const previewSymbol = Symbol("preview");
5
+
6
+ /**
7
+ * A base class for creating custom Map subclasses for use in trees.
8
+ *
9
+ * Instances of SyncMap (and its subclasses) pass `instanceof Map`, and all Map
10
+ * methods have compatible signatures.
11
+ *
12
+ * Subclasses may be read-only or read-write. A read-only subclass overrides
13
+ * get() but not set() or delete(). A read-write subclass overrides all three
14
+ * methods.
15
+ *
16
+ * For use in trees, SyncMap instances may indicate a `parent` node. They can
17
+ * also indicate children subtrees using the trailing slash convention: a key
18
+ * for a subtree may optionally end with a slash. The get() and has() methods
19
+ * support optional trailing slashes on keys.
20
+ *
21
+ * @typedef {import("../../index.ts").SyncTree<SyncMap>} SyncTree
22
+ * @implements {SyncTree}
23
+ */
24
+ export default class SyncMap extends Map {
25
+ _initialized = false;
26
+
27
+ constructor(iterable) {
28
+ super(iterable);
29
+
30
+ /** @type {SyncMap|null} */
31
+ this._parent = null;
32
+
33
+ // Record self-reference for use in Map method calls that insist on the
34
+ // receiver being a Map instance. This allows method calls to work even when
35
+ // the prototype chain is extended via Object.create().
36
+ //
37
+ // We separately use this member to determine whether the constructor has
38
+ // been called to initialize the instance. See set().
39
+ this._self = this;
40
+ }
41
+
42
+ /**
43
+ * Return the child node for the given key, creating it if necessary.
44
+ */
45
+ child(key) {
46
+ let result = this.get(key);
47
+
48
+ // If child is already a map we can use it as is
49
+ if (!(result instanceof Map)) {
50
+ // Create new child node using no-arg constructor
51
+ result = new /** @type {any} */ (this.constructor)();
52
+ this.set(key, result);
53
+ }
54
+
55
+ result.parent = this;
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Removes all key/value entries from the map.
61
+ *
62
+ * Unlike the standard `Map.prototype.clear()`, this method invokes an
63
+ * overridden `keys()` and `delete()` to ensure proper behavior in subclasses.
64
+ *
65
+ * If the `readOnly` property is true, calling this method throws a
66
+ * `TypeError`.
67
+ */
68
+ clear() {
69
+ if (this.readOnly) {
70
+ throw new TypeError("clear() can't be called on a read-only map");
71
+ }
72
+ for (const key of this.keys()) {
73
+ this.delete(key);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Removes the entry for the given key, return true if an entry was removed
79
+ * and false if there was no entry for the key.
80
+ *
81
+ * If the `readOnly` property is true, calling this method throws a
82
+ * `TypeError`.
83
+ */
84
+ delete(key) {
85
+ if (this.readOnly) {
86
+ throw new TypeError("delete() can't be called on a read-only map");
87
+ }
88
+ return super.delete.call(this._self, key);
89
+ }
90
+
91
+ /**
92
+ * Returns a new `Iterator` object that contains a two-member array of [key,
93
+ * value] for each element in the map in insertion order.
94
+ *
95
+ * Unlike the standard `Map.prototype.clear()`, this method invokes an
96
+ * overridden `keys()` and `get()` to ensure proper behavior in subclasses.
97
+ */
98
+ entries() {
99
+ // We'd like to just define entries() as a generator but TypeScript
100
+ // complains that it doesn't match the Map interface. We define the
101
+ // generator internally and then cast it to the expected type.
102
+ const self = this;
103
+ function* gen() {
104
+ for (const key of self.keys()) {
105
+ yield [key, self.get(key)];
106
+ }
107
+ }
108
+ return /** @type {MapIterator<[any, any]>} */ (gen());
109
+ }
110
+
111
+ /**
112
+ * Calls `callback` once for each key/value pair in the map, in insertion order.
113
+ *
114
+ * Unlike the standard `Map.prototype.forEach()`, this method invokes an
115
+ * overridden `entries()` to ensure proper behavior in subclasses.
116
+ *
117
+ * @param {(value: any, key: any, thisArg: any) => void} callback
118
+ * @param {any?} thisArg
119
+ */
120
+ forEach(callback, thisArg = this) {
121
+ for (const [key, value] of this.entries()) {
122
+ callback(value, key, thisArg);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Returns the value associated with the key, or undefined if there is none.
128
+ */
129
+ get(key) {
130
+ let value = super.get.call(this._self, key);
131
+ if (value === undefined) {
132
+ // Try alternate key with trailing slash added or removed
133
+ value = super.get.call(this._self, trailingSlash.toggle(key));
134
+ }
135
+ if (value === undefined) {
136
+ return undefined;
137
+ }
138
+ setParent(value, this);
139
+ return value;
140
+ }
141
+
142
+ /**
143
+ * Returns true if the given key appears in the set returned by keys().
144
+ *
145
+ * It doesn't matter whether the value returned by get() is defined or not.
146
+ *
147
+ * If the requested key has a trailing slash but has no associated value, but
148
+ * the alternate form with a slash does appear, this returns true.
149
+ *
150
+ * @param {any} key
151
+ */
152
+ has(key) {
153
+ const keys = Array.from(this.keys());
154
+ return (
155
+ keys.includes(key) ||
156
+ (!trailingSlash.has(key) && keys.includes(trailingSlash.add(key)))
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Returns a new `Iterator` object that contains the keys for each element in
162
+ * the map in insertion order.
163
+ *
164
+ * @returns {MapIterator<any>}
165
+ */
166
+ keys() {
167
+ return super.keys.call(this._self);
168
+ }
169
+
170
+ /**
171
+ * The parent of this node in a tree.
172
+ */
173
+ get parent() {
174
+ return this._parent;
175
+ }
176
+ set parent(parent) {
177
+ this._parent = parent;
178
+ }
179
+
180
+ /**
181
+ * True if the object is read-only. This will be true if the `get()` method has
182
+ * been overridden but `set()` and `delete()` have not.
183
+ */
184
+ get readOnly() {
185
+ return (
186
+ this.get !== SyncMap.prototype.get &&
187
+ (this.set === SyncMap.prototype.set ||
188
+ this.delete === SyncMap.prototype.delete)
189
+ );
190
+ }
191
+
192
+ /**
193
+ * Adds a new entry with a specified key and value to this Map, or updates an
194
+ * existing entry if the key already exists.
195
+ *
196
+ * If the `readOnly` property is true, calling this method throws a `TypeError`.
197
+ */
198
+ set(key, value) {
199
+ // The Map constructor takes an optional `iterable` argument. If specified,
200
+ // then set() will be called during construction. We want to allow this to
201
+ // work even for read-only subclasses, so we allow set() to be called during
202
+ // initialization. Once the `_self` member is set, we know initialization is
203
+ // complete; after that point, calling set() on a read-only subclass will
204
+ // throw.
205
+ if (this._self !== undefined && this.readOnly) {
206
+ throw new TypeError("set() can't be called on a read-only map");
207
+ }
208
+ // If _self is not set, use the current instance as the receiver. This is
209
+ // necessary to let the constructor call `super()`.
210
+ const target = this._self ?? this;
211
+
212
+ return super.set.call(target, key, value);
213
+ }
214
+
215
+ /**
216
+ * Returns the number of keys in the map.
217
+ *
218
+ * The `size` property invokes an overridden `keys()` to ensure proper
219
+ * behavior in subclasses. Because a subclass may not enforce a direct
220
+ * correspondence between `keys()` and `get()`, the size may not reflect the
221
+ * number of values that can be retrieved.
222
+ */
223
+ get size() {
224
+ const keys = Array.from(this.keys());
225
+ return keys.length;
226
+ }
227
+
228
+ /**
229
+ * Returns the map's `entries()`.
230
+ */
231
+ [Symbol.iterator]() {
232
+ return this.entries();
233
+ }
234
+
235
+ trailingSlashKeys = false;
236
+
237
+ /**
238
+ * Returns a new `Iterator` object that contains the values for each element
239
+ * in the map in insertion order.
240
+ */
241
+ values() {
242
+ // See notes at entries()
243
+ const self = this;
244
+ function* gen() {
245
+ for (const key of self.keys()) {
246
+ yield self.get(key);
247
+ }
248
+ }
249
+ return /** @type {MapIterator<[any]>} */ (gen());
250
+ }
251
+ }
252
+
253
+ // For debugging we make entries() available as a gettable property.
254
+ Object.defineProperty(SyncMap.prototype, previewSymbol, {
255
+ configurable: true,
256
+ enumerable: false,
257
+ get: function () {
258
+ return Array.from(this.entries());
259
+ },
260
+ });
package/src/jsonKeys.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Treelike } from "../index.ts";
1
+ import { Maplike } from "../index.ts";
2
2
 
3
3
  export function parse(json: string): any;
4
- export function stringify(treelike: Treelike): Promise<string>;
4
+ export function stringify(maplike: Maplike): Promise<string>;
package/src/jsonKeys.js CHANGED
@@ -1,4 +1,8 @@
1
+ import entries from "./operations/entries.js";
1
2
  import from from "./operations/from.js";
3
+ import isMap from "./operations/isMap.js";
4
+ import keys from "./operations/keys.js";
5
+ import * as trailingSlash from "./trailingSlash.js";
2
6
 
3
7
  /**
4
8
  * Given a tree node, return a JSON string that can be written to a .keys.json
@@ -11,11 +15,22 @@ import from from "./operations/from.js";
11
15
  * "index.html" for a specific resource available at the node, or a string with
12
16
  * a trailing slash like "about/" for a subtree of that node.
13
17
  */
14
- export async function stringify(treelike) {
15
- const tree = from(treelike);
16
- let keys = Array.from(await tree.keys());
18
+ export async function stringify(maplike) {
19
+ const tree = from(maplike);
20
+
21
+ let treeKeys;
22
+ if (/** @type {any} */ (tree).trailingSlashKeys) {
23
+ treeKeys = await keys(tree);
24
+ } else {
25
+ // Use entries() to determine which keys are subtrees.
26
+ const treeEntries = await entries(tree);
27
+ treeKeys = treeEntries.map(([key, value]) =>
28
+ trailingSlash.toggle(key, isMap(value))
29
+ );
30
+ }
31
+
17
32
  // Skip the key `.keys.json` if present.
18
- keys = keys.filter((key) => key !== ".keys.json");
19
- const json = JSON.stringify(keys);
33
+ treeKeys = treeKeys.filter((key) => key !== ".keys.json");
34
+ const json = JSON.stringify(treeKeys);
20
35
  return json;
21
36
  }