@weborigami/async-tree 0.3.0 → 0.3.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/index.ts CHANGED
@@ -46,6 +46,7 @@ export type Treelike =
46
46
  export type TreeMapOptions = {
47
47
  deep?: boolean;
48
48
  description?: string;
49
+ extension?: string;
49
50
  needsSourceValue?: boolean;
50
51
  inverseKey?: KeyFn;
51
52
  key?: KeyFn;
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.3.0",
3
+ "version": "0.3.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
+ "dependencies": {
10
+ "@weborigami/types": "0.3.2"
11
+ },
9
12
  "devDependencies": {
10
13
  "@types/node": "22.13.13",
14
+ "puppeteer": "24.6.1",
11
15
  "typescript": "5.8.2"
12
16
  },
13
- "dependencies": {
14
- "@weborigami/types": "0.3.0"
15
- },
16
17
  "scripts": {
18
+ "headlessTest": "node test/browser/headlessTest.js",
17
19
  "test": "node --test --test-reporter=spec",
18
20
  "typecheck": "tsc"
19
21
  }
@@ -0,0 +1,26 @@
1
+ import puppeteer from "puppeteer";
2
+
3
+ let failed = false;
4
+
5
+ const browser = await puppeteer.launch({
6
+ args: ["--allow-file-access-from-files", "--disable-web-security"],
7
+ });
8
+ const page = await browser.newPage();
9
+
10
+ page.on("console", (msg) => {
11
+ const text = msg.text();
12
+ if (text.includes("❌")) {
13
+ console.error(text);
14
+ failed = true;
15
+ }
16
+ });
17
+
18
+ // load your local index.html
19
+ // const url = new URL("index.html", import.meta.url);
20
+ const url = "http://localhost:5000/test/browser/index.html";
21
+ await page.goto(url);
22
+ await browser.close();
23
+ if (!failed) {
24
+ console.log("✅ All tests passed");
25
+ }
26
+ process.exit(failed ? 1 : 0);
package/shared.js CHANGED
@@ -15,10 +15,10 @@ export { default as addNextPrevious } from "./src/operations/addNextPrevious.js"
15
15
  export { default as cache } from "./src/operations/cache.js";
16
16
  export { default as cachedKeyFunctions } from "./src/operations/cachedKeyFunctions.js";
17
17
  export { default as concat } from "./src/operations/concat.js";
18
- export { default as concatTrees } from "./src/operations/concatTrees.js";
19
18
  export { default as deepMerge } from "./src/operations/deepMerge.js";
20
19
  export { default as deepReverse } from "./src/operations/deepReverse.js";
21
20
  export { default as deepTake } from "./src/operations/deepTake.js";
21
+ export { default as deepText } from "./src/operations/deepText.js";
22
22
  export { default as deepValues } from "./src/operations/deepValues.js";
23
23
  export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
24
24
  export { default as extensionKeyFunctions } from "./src/operations/extensionKeyFunctions.js";
@@ -29,6 +29,7 @@ export { default as map } from "./src/operations/map.js";
29
29
  export { default as mask } from "./src/operations/mask.js";
30
30
  export { default as merge } from "./src/operations/merge.js";
31
31
  export { default as paginate } from "./src/operations/paginate.js";
32
+ export { default as parseExtensions } from "./src/operations/parseExtensions.js";
32
33
  export { default as reverse } from "./src/operations/reverse.js";
33
34
  export { default as scope } from "./src/operations/scope.js";
34
35
  export { default as sort } from "./src/operations/sort.js";
@@ -6,11 +6,17 @@ import SiteTree from "./SiteTree.js";
6
6
  * route even though such a mechanism is not built into the HTTP protocol.
7
7
  */
8
8
  export default class ExplorableSiteTree extends SiteTree {
9
- constructor(...args) {
10
- super(...args);
9
+ /**
10
+ * @param {string} href
11
+ */
12
+ constructor(href) {
13
+ super(href);
11
14
  this.serverKeysPromise = undefined;
12
15
  }
13
16
 
17
+ /**
18
+ * @returns {Promise<string[]>}
19
+ */
14
20
  async getServerKeys() {
15
21
  // We use a promise to ensure we only check for keys once.
16
22
  const href = new URL(".keys.json", this.href).href;
@@ -32,8 +38,6 @@ export default class ExplorableSiteTree extends SiteTree {
32
38
  /**
33
39
  * Returns the keys of the site route. For this to work, the route must have a
34
40
  * `.keys.json` file that contains a JSON array of string keys.
35
- *
36
- * @returns {Promise<Iterable<string>>}
37
41
  */
38
42
  async keys() {
39
43
  const serverKeys = await this.getServerKeys();
@@ -91,6 +91,8 @@ export default class FileTree {
91
91
 
92
92
  /**
93
93
  * Enumerate the names of the files/subdirectories in this directory.
94
+ *
95
+ * @returns {Promise<string[]>}
94
96
  */
95
97
  async keys() {
96
98
  let entries;
@@ -16,6 +16,9 @@ import { setParent } from "../utilities.js";
16
16
  */
17
17
  export default class MapTree {
18
18
  /**
19
+ * Constructs a new `MapTree` instance. This `iterable` parameter can be a
20
+ * `Map` instance, or any other iterable of key-value pairs.
21
+ *
19
22
  * @param {Iterable} [iterable]
20
23
  */
21
24
  constructor(iterable = []) {
@@ -9,7 +9,7 @@ import concat from "./concat.js";
9
9
  * @param {TemplateStringsArray} strings
10
10
  * @param {...any} values
11
11
  */
12
- export default async function concatTrees(strings, ...values) {
12
+ export default async function deepText(strings, ...values) {
13
13
  // Convert all the values to strings
14
14
  const valueTexts = await Promise.all(
15
15
  values.map((value) =>
@@ -19,8 +19,8 @@ export default function extensionKeyFunctions(
19
19
  resultExtension = sourceExtension;
20
20
  }
21
21
 
22
- checkDeprecatedExtensionWithoutDot(resultExtension);
23
- checkDeprecatedExtensionWithoutDot(sourceExtension);
22
+ checkExtension(resultExtension);
23
+ checkExtension(sourceExtension);
24
24
 
25
25
  return {
26
26
  async inverseKey(resultKey, tree) {
@@ -38,7 +38,7 @@ export default function extensionKeyFunctions(
38
38
  };
39
39
  }
40
40
 
41
- function checkDeprecatedExtensionWithoutDot(extension) {
41
+ function checkExtension(extension) {
42
42
  if (extension && extension !== "/" && !extension.startsWith(".")) {
43
43
  throw new RangeError(
44
44
  `The extension "${extension}" must start with a period.`
@@ -1,6 +1,9 @@
1
1
  import { Tree } from "../internal.js";
2
2
  import * as trailingSlash from "../trailingSlash.js";
3
3
  import { assertIsTreelike } from "../utilities.js";
4
+ import cachedKeyFunctions from "./cachedKeyFunctions.js";
5
+ import extensionKeyFunctions from "./extensionKeyFunctions.js";
6
+ import parseExtensions from "./parseExtensions.js";
4
7
 
5
8
  /**
6
9
  * Transform the keys and/or values of a tree.
@@ -115,6 +118,7 @@ export default function map(treelike, options = {}) {
115
118
  function validateOptions(options) {
116
119
  let deep;
117
120
  let description;
121
+ let extension;
118
122
  let inverseKeyFn;
119
123
  let keyFn;
120
124
  let needsSourceValue;
@@ -126,26 +130,51 @@ function validateOptions(options) {
126
130
  } else {
127
131
  deep = options.deep;
128
132
  description = options.description;
133
+ extension = options.extension;
129
134
  inverseKeyFn = options.inverseKey;
130
135
  keyFn = options.key;
131
136
  needsSourceValue = options.needsSourceValue;
132
137
  valueFn = options.value;
133
138
  }
134
139
 
135
- deep ??= false;
136
- description ??= "key/value map";
137
- // @ts-ignore
138
- inverseKeyFn ??= valueFn?.inverseKey;
139
- // @ts-ignore
140
- keyFn ??= valueFn?.key;
141
- needsSourceValue ??= true;
142
-
143
- if ((keyFn && !inverseKeyFn) || (!keyFn && inverseKeyFn)) {
140
+ if (extension && (keyFn || inverseKeyFn)) {
144
141
  throw new TypeError(
145
- `map: You must specify both key and inverseKey functions, or neither.`
142
+ `map: You can't specify extensions and also a key or inverseKey function`
146
143
  );
147
144
  }
148
145
 
146
+ if (extension) {
147
+ // Use the extension mapping to generate key and inverseKey functions
148
+ const parsed = parseExtensions(extension);
149
+ const keyFns = extensionKeyFunctions(
150
+ parsed.sourceExtension,
151
+ parsed.resultExtension
152
+ );
153
+ keyFn = keyFns.key;
154
+ inverseKeyFn = keyFns.inverseKey;
155
+ } else {
156
+ // If key or inverseKey weren't specified, look for sidecar functions
157
+ inverseKeyFn ??= valueFn?.inverseKey;
158
+ keyFn ??= valueFn?.key;
159
+
160
+ if (!keyFn && inverseKeyFn) {
161
+ throw new TypeError(
162
+ `map: You can't specify an inverseKey function without a key function`
163
+ );
164
+ }
165
+
166
+ if (keyFn && !inverseKeyFn) {
167
+ // Only keyFn was provided, so we need to generate the inverseKeyFn
168
+ const keyFns = cachedKeyFunctions(keyFn, deep);
169
+ keyFn = keyFns.key;
170
+ inverseKeyFn = keyFns.inverseKey;
171
+ }
172
+ }
173
+
174
+ deep ??= false;
175
+ description ??= "key/value map";
176
+ needsSourceValue ??= true;
177
+
149
178
  return {
150
179
  deep,
151
180
  description,
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Given a string specifying an extension or a mapping of one extension to another,
3
+ * return the source and result extensions.
4
+ *
5
+ * Syntax:
6
+ * .foo source and result extension are the same
7
+ * .foo→.bar Unicode Rightwards Arrow
8
+ * .foo→ Unicode Rightwards Arrow, no result extension
9
+ * →.bar Unicode Rightwards Arrow, no source extension
10
+ * .foo->.bar hyphen and greater-than sign
11
+ *
12
+ * @param {string} specifier
13
+ */
14
+ export default function parseExtensions(specifier) {
15
+ const lowercase = specifier?.toLowerCase() ?? "";
16
+ const extensionRegex =
17
+ /^((?<sourceExtension>\/|\.\S*)?\s*(→|->)\s*(?<resultExtension>\/|\.\S*)?)|(?<extension>\/|\.\S*)$/;
18
+ const match = lowercase.match(extensionRegex);
19
+ if (!match) {
20
+ throw new Error(`Invalid file extension specifier "${specifier}".`);
21
+ }
22
+
23
+ // @ts-ignore
24
+ let { extension, resultExtension, sourceExtension } = match.groups;
25
+ if (extension) {
26
+ // foo
27
+ return {
28
+ resultExtension: extension,
29
+ sourceExtension: extension,
30
+ };
31
+ } else {
32
+ // foo→bar
33
+
34
+ if (resultExtension === undefined && sourceExtension === undefined) {
35
+ throw new Error(
36
+ `A file extension mapping must indicate a source or result extension: "${specifier}".`
37
+ );
38
+ }
39
+
40
+ resultExtension ??= "";
41
+ sourceExtension ??= "";
42
+ return { resultExtension, sourceExtension };
43
+ }
44
+ }
@@ -13,22 +13,45 @@
13
13
  </script>
14
14
  <!-- Omit FileTree.test.js, which is Node.js only -->
15
15
  <!-- Omit SiteTree.test.js, which requires mocks -->
16
+
17
+ <!-- Unclear why drivers/constantTree.test.js won't load -->
18
+
16
19
  <script type="module" src="../Tree.test.js"></script>
17
20
  <script type="module" src="../drivers/BrowserFileTree.test.js"></script>
21
+ <script type="module" src="../drivers/DeepMapTree.test.js"></script>
22
+ <script type="module" src="../drivers/DeepObjectTree.test.js"></script>
18
23
  <script type="module" src="../drivers/DeferredTree.test.js"></script>
19
24
  <script type="module" src="../drivers/FunctionTree.test.js"></script>
20
25
  <script type="module" src="../drivers/MapTree.test.js"></script>
21
26
  <script type="module" src="../drivers/ObjectTree.test.js"></script>
22
27
  <script type="module" src="../drivers/SetTree.test.js"></script>
28
+ <script type="module" src="../drivers/calendarTree.test.js"></script>
29
+ <script type="module" src="../operations/addNextPrevious.test.js"></script>
30
+ <script type="module" src="../operations/cache.test.js"></script>
23
31
  <script type="module" src="../operations/cache.test.js"></script>
24
- <script type="module" src="../operations/merge.test.js"></script>
25
- <script type="module" src="../operations/deepMerge.test.js"></script>
26
32
  <script type="module" src="../operations/cachedKeyFunctions.test.js"></script>
27
- <script
28
- type="module"
29
- src="../operations/keyFunctionsForExtensions.test.js"
30
- ></script>
33
+ <script type="module" src="../operations/concat.test.js"></script>
34
+ <script type="module" src="../operations/deepMerge.test.js"></script>
35
+ <script type="module" src="../operations/deepReverse.test.js"></script>
36
+ <script type="module" src="../operations/deepTake.test.js"></script>
37
+ <script type="module" src="../operations/deepText.test.js"></script>
38
+ <script type="module" src="../operations/deepValues.test.js"></script>
39
+ <script type="module" src="../operations/deepValuesIterator.test.js"></script>
40
+ <script type="module" src="../operations/extensionKeyFunctions.test.js"></script>
41
+ <script type="module" src="../operations/filter.test.js"></script>
42
+ <script type="module" src="../operations/globKeys.test.js"></script>
43
+ <script type="module" src="../operations/group.test.js"></script>
44
+ <script type="module" src="../operations/invokeFunctions.test.js"></script>
31
45
  <script type="module" src="../operations/map.test.js"></script>
46
+ <script type="module" src="../operations/mask.test.js"></script>
47
+ <script type="module" src="../operations/merge.test.js"></script>
48
+ <script type="module" src="../operations/paginate.test.js"></script>
49
+ <script type="module" src="../operations/parseExtensions.test.js"></script>
50
+ <script type="module" src="../operations/regExpKeys.test.js"></script>
51
+ <script type="module" src="../operations/reverse.test.js"></script>
52
+ <script type="module" src="../operations/scope.test.js"></script>
53
+ <script type="module" src="../operations/sort.test.js"></script>
54
+ <script type="module" src="../operations/take.test.js"></script>
32
55
  <script type="module" src="../utilities.test.js"></script>
33
56
  </head>
34
57
  <body></body>
@@ -25,7 +25,8 @@ export async function describe(name, fn) {
25
25
  const name = result.name;
26
26
  const message = result.result === "fail" ? `: ${result.message}` : "";
27
27
  const skipped = result.result === "skipped" ? " [skipped]" : "";
28
- console.log(`${marker} ${name}${message}${skipped}`);
28
+ const fn = result.result === "fail" ? "error" : "log";
29
+ console[fn](`${marker} ${name}${message}${skipped}`);
29
30
  }
30
31
  console.groupEnd();
31
32
  }
@@ -39,16 +39,16 @@ describe("addNextPrevious", () => {
39
39
  assert.deepEqual(result, [
40
40
  {
41
41
  value: "Alice",
42
- nextKey: 1,
42
+ nextKey: "1",
43
43
  },
44
44
  {
45
45
  value: "Bob",
46
- nextKey: 2,
47
- previousKey: 0,
46
+ nextKey: "2",
47
+ previousKey: "0",
48
48
  },
49
49
  {
50
50
  value: "Carol",
51
- previousKey: 1,
51
+ previousKey: "1",
52
52
  },
53
53
  ]);
54
54
  });
@@ -1,12 +1,12 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
- import concatTrees from "../../src/operations/concatTrees.js";
3
+ import deepText from "../../src/operations/deepText.js";
4
4
 
5
- describe("concatTrees", () => {
5
+ describe("deepText", () => {
6
6
  test("joins strings and values together", async () => {
7
7
  const array = [1, 2, 3];
8
8
  const object = { person1: "Alice", person2: "Bob" };
9
- const result = await concatTrees`a ${array} b ${object} c`;
9
+ const result = await deepText`a ${array} b ${object} c`;
10
10
  assert.equal(result, "a 123 b AliceBob c");
11
11
  });
12
12
  });
@@ -68,6 +68,20 @@ describe("map", () => {
68
68
  });
69
69
  });
70
70
 
71
+ test("if only given a key, will generate an inverseKey", async () => {
72
+ const tree = {
73
+ a: "letter a",
74
+ b: "letter b",
75
+ };
76
+ const underscoreKeys = map(tree, {
77
+ key: addUnderscore,
78
+ });
79
+ assert.deepEqual(await Tree.plain(underscoreKeys), {
80
+ _a: "letter a",
81
+ _b: "letter b",
82
+ });
83
+ });
84
+
71
85
  test("maps keys and values", async () => {
72
86
  const tree = {
73
87
  a: "letter a",
@@ -102,7 +116,7 @@ describe("map", () => {
102
116
  });
103
117
  });
104
118
 
105
- test("value can provide a default key and inverse key functions", async () => {
119
+ test("value can provide a default key and inverse key sidecar functions", async () => {
106
120
  const uppercase = (s) => s.toUpperCase();
107
121
  uppercase.key = addUnderscore;
108
122
  uppercase.inverseKey = removeUnderscore;
@@ -177,6 +191,40 @@ describe("map", () => {
177
191
  });
178
192
  });
179
193
 
194
+ test("can change a key's extension", async () => {
195
+ const treelike = {
196
+ "file1.lower": "will be mapped",
197
+ file2: "won't be mapped",
198
+ "file3.foo": "won't be mapped",
199
+ };
200
+ const fixture = await map(treelike, {
201
+ extension: ".lower->.upper",
202
+ value: (sourceValue) => sourceValue.toUpperCase(),
203
+ });
204
+ assert.deepEqual(await Tree.plain(fixture), {
205
+ "file1.upper": "WILL BE MAPPED",
206
+ });
207
+ });
208
+
209
+ test("can manipulate extensions deeply", async () => {
210
+ const treelike = {
211
+ "file1.txt": 1,
212
+ more: {
213
+ "file2.txt": 2,
214
+ },
215
+ };
216
+ const fixture = await map(treelike, {
217
+ deep: true,
218
+ extension: ".txt->",
219
+ });
220
+ assert.deepEqual(await Tree.plain(fixture), {
221
+ file1: 1,
222
+ more: {
223
+ file2: 2,
224
+ },
225
+ });
226
+ });
227
+
180
228
  test("needsSourceValue can be set to false in cases where the value isn't necessary", async () => {
181
229
  let flag = false;
182
230
  const tree = new FunctionTree(() => {
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import parseExtensions from "../../src/operations/parseExtensions.js";
4
+
5
+ describe("keyMapsForExtensions", () => {
6
+ test("source and result extension are the same", async () => {
7
+ assert.deepEqual(parseExtensions(".foo"), {
8
+ sourceExtension: ".foo",
9
+ resultExtension: ".foo",
10
+ });
11
+ });
12
+
13
+ test("change extension", async () => {
14
+ assert.deepEqual(parseExtensions(".foo->.bar"), {
15
+ sourceExtension: ".foo",
16
+ resultExtension: ".bar",
17
+ });
18
+ // with Unicode Rightwards Arrow
19
+ assert.deepEqual(parseExtensions(".foo→.bar"), {
20
+ sourceExtension: ".foo",
21
+ resultExtension: ".bar",
22
+ });
23
+ });
24
+
25
+ test("add extension", async () => {
26
+ assert.deepEqual(parseExtensions("->.foo"), {
27
+ sourceExtension: "",
28
+ resultExtension: ".foo",
29
+ });
30
+ assert.deepEqual(parseExtensions("→.foo"), {
31
+ sourceExtension: "",
32
+ resultExtension: ".foo",
33
+ });
34
+ });
35
+
36
+ test("remove extension", async () => {
37
+ assert.deepEqual(parseExtensions(".foo->"), {
38
+ sourceExtension: ".foo",
39
+ resultExtension: "",
40
+ });
41
+ assert.deepEqual(parseExtensions(".foo→"), {
42
+ sourceExtension: ".foo",
43
+ resultExtension: "",
44
+ });
45
+ });
46
+
47
+ test("slash is a valid extension", async () => {
48
+ assert.deepEqual(parseExtensions("/"), {
49
+ sourceExtension: "/",
50
+ resultExtension: "/",
51
+ });
52
+ assert.deepEqual(parseExtensions(".foo->/"), {
53
+ sourceExtension: ".foo",
54
+ resultExtension: "/",
55
+ });
56
+ assert.deepEqual(parseExtensions("/->.bar"), {
57
+ sourceExtension: "/",
58
+ resultExtension: ".bar",
59
+ });
60
+ });
61
+ });