@weborigami/language 0.0.35

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 (69) hide show
  1. package/index.ts +30 -0
  2. package/main.js +21 -0
  3. package/package.json +24 -0
  4. package/src/compiler/code.d.ts +3 -0
  5. package/src/compiler/compile.js +55 -0
  6. package/src/compiler/origami.pegjs +277 -0
  7. package/src/compiler/parse.js +2292 -0
  8. package/src/compiler/parserHelpers.js +26 -0
  9. package/src/runtime/EventTargetMixin.d.ts +9 -0
  10. package/src/runtime/EventTargetMixin.js +117 -0
  11. package/src/runtime/ExpressionTree.js +20 -0
  12. package/src/runtime/FileLoadersTransform.d.ts +5 -0
  13. package/src/runtime/FileLoadersTransform.js +43 -0
  14. package/src/runtime/ImportModulesMixin.d.ts +5 -0
  15. package/src/runtime/ImportModulesMixin.js +48 -0
  16. package/src/runtime/InheritScopeMixin.js +34 -0
  17. package/src/runtime/InheritScopeMixin.ts +9 -0
  18. package/src/runtime/InvokeFunctionsTransform.d.ts +5 -0
  19. package/src/runtime/InvokeFunctionsTransform.js +27 -0
  20. package/src/runtime/OrigamiFiles.d.ts +11 -0
  21. package/src/runtime/OrigamiFiles.js +9 -0
  22. package/src/runtime/OrigamiTransform.d.ts +11 -0
  23. package/src/runtime/OrigamiTransform.js +11 -0
  24. package/src/runtime/OrigamiTree.js +4 -0
  25. package/src/runtime/ReadMe.md +1 -0
  26. package/src/runtime/Scope.js +89 -0
  27. package/src/runtime/TreeEvent.js +6 -0
  28. package/src/runtime/WatchFilesMixin.d.ts +5 -0
  29. package/src/runtime/WatchFilesMixin.js +58 -0
  30. package/src/runtime/concatTreeValues.js +46 -0
  31. package/src/runtime/evaluate.js +90 -0
  32. package/src/runtime/expressionFunction.js +33 -0
  33. package/src/runtime/extname.js +20 -0
  34. package/src/runtime/format.js +126 -0
  35. package/src/runtime/functionResultsMap.js +28 -0
  36. package/src/runtime/internal.js +20 -0
  37. package/src/runtime/ops.js +222 -0
  38. package/test/compiler/compile.test.js +64 -0
  39. package/test/compiler/parse.test.js +389 -0
  40. package/test/runtime/EventTargetMixin.test.js +68 -0
  41. package/test/runtime/ExpressionTree.test.js +27 -0
  42. package/test/runtime/FileLoadersTransform.test.js +41 -0
  43. package/test/runtime/InheritScopeMixin.test.js +29 -0
  44. package/test/runtime/OrigamiFiles.test.js +37 -0
  45. package/test/runtime/Scope.test.js +37 -0
  46. package/test/runtime/concatTreeValues.js +20 -0
  47. package/test/runtime/evaluate.test.js +55 -0
  48. package/test/runtime/fixtures/foo.js +1 -0
  49. package/test/runtime/fixtures/makeTest/a +1 -0
  50. package/test/runtime/fixtures/makeTest/b = a +0 -0
  51. package/test/runtime/fixtures/metagraphs/foo.txt +1 -0
  52. package/test/runtime/fixtures/metagraphs/greeting = this('world').js +3 -0
  53. package/test/runtime/fixtures/metagraphs/obj = this.json +5 -0
  54. package/test/runtime/fixtures/metagraphs/sample.txt = this().js +3 -0
  55. package/test/runtime/fixtures/metagraphs/string = this.json +1 -0
  56. package/test/runtime/fixtures/metagraphs/value = fn() +0 -0
  57. package/test/runtime/fixtures/programs/context.yaml +4 -0
  58. package/test/runtime/fixtures/programs/files.yaml +2 -0
  59. package/test/runtime/fixtures/programs/obj.yaml +3 -0
  60. package/test/runtime/fixtures/programs/simple.yaml +2 -0
  61. package/test/runtime/fixtures/subgraph = this.js +5 -0
  62. package/test/runtime/fixtures/templates/greet.orit +4 -0
  63. package/test/runtime/fixtures/templates/index.orit +15 -0
  64. package/test/runtime/fixtures/templates/names.yaml +3 -0
  65. package/test/runtime/fixtures/templates/plain.txt +1 -0
  66. package/test/runtime/fixtures/virtualKeys/.keys.json +1 -0
  67. package/test/runtime/format.test.js +66 -0
  68. package/test/runtime/functionResultsMap.test.js +27 -0
  69. package/test/runtime/ops.test.js +111 -0
@@ -0,0 +1,26 @@
1
+ import * as ops from "../runtime/ops.js";
2
+
3
+ // Parser helpers
4
+
5
+ export function makeFunctionCall(target, chain) {
6
+ let value = target;
7
+ // The chain is an array of arguments (which are themselves arrays). We
8
+ // successively apply the top-level elements of that chain to build up the
9
+ // function composition.
10
+ for (const args of chain) {
11
+ value = [value, ...args];
12
+ }
13
+ return value;
14
+ }
15
+
16
+ export function makeTemplate(parts) {
17
+ // Drop empty/null strings.
18
+ const filtered = parts.filter((part) => part);
19
+ // Return a concatenation of the parts. If there are no parts, return the
20
+ // empty string. If there's just one string, return that directly.
21
+ return filtered.length === 0
22
+ ? ""
23
+ : filtered.length === 1 && typeof filtered[0] === "string"
24
+ ? filtered[0]
25
+ : [ops.concat, ...filtered];
26
+ }
@@ -0,0 +1,9 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const EventTargetMixin: Mixin<{
4
+ addEventListener(type: string, listener: EventListener): void;
5
+ dispatchEvent(event: Event): boolean;
6
+ removeEventListener(type: string, listener: EventListener): void;
7
+ }>;
8
+
9
+ export default EventTargetMixin;
@@ -0,0 +1,117 @@
1
+ const listenersKey = Symbol("listeners");
2
+
3
+ export default function EventTargetMixin(Base) {
4
+ // Based on https://github.com/piranna/EventTarget.js
5
+ return class EventTarget extends Base {
6
+ constructor(...args) {
7
+ super(...args);
8
+ this[listenersKey] = {};
9
+ }
10
+
11
+ addEventListener(type, callback) {
12
+ if (!callback) {
13
+ return;
14
+ }
15
+
16
+ let listenersOfType = this[listenersKey][type];
17
+ if (!listenersOfType) {
18
+ this[listenersKey][type] = [];
19
+ listenersOfType = this[listenersKey][type];
20
+ }
21
+
22
+ // Don't add the same callback twice.
23
+ if (listenersOfType.includes(callback)) {
24
+ return;
25
+ }
26
+
27
+ listenersOfType.push(callback);
28
+ }
29
+
30
+ dispatchEvent(event) {
31
+ if (!(event instanceof Event)) {
32
+ throw TypeError("Argument to dispatchEvent must be an Event");
33
+ }
34
+
35
+ let stopImmediatePropagation = false;
36
+ let defaultPrevented = false;
37
+
38
+ if (!event.cancelable) {
39
+ Object.defineProperty(event, "cancelable", {
40
+ value: true,
41
+ enumerable: true,
42
+ });
43
+ }
44
+ if (!event.defaultPrevented) {
45
+ Object.defineProperty(event, "defaultPrevented", {
46
+ get: () => defaultPrevented,
47
+ enumerable: true,
48
+ });
49
+ }
50
+ // 2023-09-11: Setting isTrusted causes exception on Glitch
51
+ // if (!event.isTrusted) {
52
+ // Object.defineProperty(event, "isTrusted", {
53
+ // value: false,
54
+ // enumerable: true,
55
+ // });
56
+ // }
57
+ if (!event.target) {
58
+ Object.defineProperty(event, "target", {
59
+ value: this,
60
+ enumerable: true,
61
+ });
62
+ }
63
+ if (!event.timeStamp) {
64
+ Object.defineProperty(event, "timeStamp", {
65
+ value: new Date().getTime(),
66
+ enumerable: true,
67
+ });
68
+ }
69
+
70
+ event.preventDefault = function () {
71
+ if (this.cancelable) {
72
+ defaultPrevented = true;
73
+ }
74
+ };
75
+ event.stopImmediatePropagation = function () {
76
+ stopImmediatePropagation = true;
77
+ };
78
+ event.stopPropagation = function () {
79
+ // This is a no-op because we don't support event bubbling.
80
+ };
81
+
82
+ const type = event.type;
83
+ const listenersOfType = this[listenersKey][type] || [];
84
+ for (const listener of listenersOfType) {
85
+ if (stopImmediatePropagation) {
86
+ break;
87
+ }
88
+ listener.call(this, event);
89
+ }
90
+
91
+ return !event.defaultPrevented;
92
+ }
93
+
94
+ removeEventListener(type, callback) {
95
+ if (!callback) {
96
+ return;
97
+ }
98
+
99
+ let listenersOfType = this[listenersKey][type];
100
+ if (!listenersOfType) {
101
+ return;
102
+ }
103
+
104
+ // Remove callback from listeners.
105
+ listenersOfType = listenersOfType.filter(
106
+ (listener) => listener !== callback
107
+ );
108
+
109
+ // If there are no more listeners for this type, remove the type.
110
+ if (listenersOfType.length === 0) {
111
+ delete this[listenersKey][type];
112
+ } else {
113
+ this[listenersKey][type] = listenersOfType;
114
+ }
115
+ }
116
+ };
117
+ }
@@ -0,0 +1,20 @@
1
+ import { ObjectTree } from "@weborigami/async-tree";
2
+ import InvokeFunctionsTransform from "./InvokeFunctionsTransform.js";
3
+ import { expressionFunction } from "./internal.js";
4
+
5
+ export default class ExpressionTree extends InvokeFunctionsTransform(
6
+ ObjectTree
7
+ ) {
8
+ // Return the unevaluated expressions in the original object.
9
+ expressions() {
10
+ const obj = /** @type {any} */ (this).object;
11
+ const result = {};
12
+ for (const key in obj) {
13
+ const value = obj[key];
14
+ if (expressionFunction.isExpressionFunction(value)) {
15
+ result[key] = value.code;
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const FileLoadersTransform: Mixin<{}>;
4
+
5
+ export default FileLoadersTransform;
@@ -0,0 +1,43 @@
1
+ import { Tree, isStringLike } from "@weborigami/async-tree";
2
+ import Scope from "./Scope.js";
3
+ import extname from "./extname.js";
4
+
5
+ /**
6
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
+ * @typedef {import("../../index.js").Constructor<AsyncTree>} AsyncTreeConstructor
8
+ * @typedef {import("../../index.js").FileUnpackFunction} FileUnpackFunction
9
+ *
10
+ * @param {AsyncTreeConstructor} Base
11
+ */
12
+ export default function FileLoadersTransform(Base) {
13
+ return class FileLoaders extends Base {
14
+ async get(key) {
15
+ let value = await super.get(key);
16
+
17
+ // If the value is string-like and the key has an extension, look for a
18
+ // loader that handles that extension and call it. The value will
19
+ // typically be a Buffer loaded from the file system, but could also be a
20
+ // string-like object defined by a user function.
21
+ if (isStringLike(value) && isStringLike(key)) {
22
+ const extension = extname(String(key)).toLowerCase().slice(1);
23
+ if (extension) {
24
+ /** @type {any} */
25
+ const scope = Scope.getScope(this);
26
+ /** @type {FileUnpackFunction} */
27
+ const unpackFn = await Tree.traverse(scope, "@loaders", extension);
28
+ if (unpackFn) {
29
+ const input = value;
30
+ // If the input is a plain string, convert it to a String so we can
31
+ // attach data to it.
32
+ value = new String(input);
33
+ const parent = this;
34
+ value.parent = parent;
35
+ value.unpack = () => unpackFn(input, { key, parent });
36
+ }
37
+ }
38
+ }
39
+
40
+ return value;
41
+ }
42
+ };
43
+ }
@@ -0,0 +1,5 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const ImportModulesMixin: Mixin<{}>;
4
+
5
+ export default ImportModulesMixin;
@@ -0,0 +1,48 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+
5
+ /**
6
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
+ * @typedef {import("../../index.ts").Constructor<AsyncTree & { dirname: string }>} BaseConstructor
8
+ * @param {BaseConstructor} Base
9
+ */
10
+ export default function ImportModulesMixin(Base) {
11
+ return class ImportModules extends Base {
12
+ async import(...keys) {
13
+ const filePath = path.join(this.dirname, ...keys);
14
+ // On Windows, absolute paths must be valid file:// URLs.
15
+ const fileUrl = pathToFileURL(filePath);
16
+ const modulePath = fileUrl.href;
17
+
18
+ // Try to load the module.
19
+ let obj;
20
+ try {
21
+ obj = await import(modulePath);
22
+ } catch (/** @type {any} */ error) {
23
+ if (error.code !== "ERR_MODULE_NOT_FOUND") {
24
+ throw error;
25
+ }
26
+
27
+ // Does the module exist as a file?
28
+ let stats;
29
+ try {
30
+ stats = await fs.stat(filePath);
31
+ } catch (error) {
32
+ // Ignore errors.
33
+ }
34
+ if (stats) {
35
+ // Module exists, but we can't load it, probably due to a syntax error.
36
+ throw new SyntaxError(`Error loading ${filePath}`);
37
+ }
38
+
39
+ // Module doesn't exist.
40
+ return undefined;
41
+ }
42
+
43
+ // If the module loaded and defines a default export, return that, otherwise
44
+ // return the overall module.
45
+ return typeof obj === "object" && "default" in obj ? obj.default : obj;
46
+ }
47
+ };
48
+ }
@@ -0,0 +1,34 @@
1
+ import Scope from "./Scope.js";
2
+
3
+ const scopeKey = Symbol("scope");
4
+
5
+ /**
6
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
+ * @typedef {import("../../index.js").Constructor<AsyncTree>} AsyncTreeConstructor
8
+ * @param {AsyncTreeConstructor} Base
9
+ */
10
+ export default function InheritScopeMixin(Base) {
11
+ return class InheritScope extends Base {
12
+ constructor(...args) {
13
+ super(...args);
14
+ this[scopeKey] = null;
15
+ }
16
+
17
+ /** @type {import("@weborigami/types").AsyncTree} */
18
+ get scope() {
19
+ if (this[scopeKey] === null) {
20
+ if (this.parent) {
21
+ // Add parent to this tree's scope.
22
+ this[scopeKey] = new Scope(this, Scope.getScope(this.parent));
23
+ } else {
24
+ // Scope is just the tree itself.
25
+ this[scopeKey] = this;
26
+ }
27
+ }
28
+ return this[scopeKey];
29
+ }
30
+ set scope(scope) {
31
+ this[scopeKey] = scope;
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,9 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ import type { AsyncTree } from "@weborigami/types";
4
+
5
+ declare const InheritScopeMixin: Mixin<{
6
+ scope: AsyncTree|null;
7
+ }>;
8
+
9
+ export default InheritScopeMixin;
@@ -0,0 +1,5 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const InvokeFunctionsTransform: Mixin<{}>;
4
+
5
+ export default InvokeFunctionsTransform;
@@ -0,0 +1,27 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import Scope from "./Scope.js";
3
+
4
+ /**
5
+ * When using `get` to retrieve a value from a tree, if the value is a
6
+ * function, invoke it and return the result.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {import("../../index.js").Constructor<AsyncTree>} AsyncTreeConstructor
10
+ * @param {AsyncTreeConstructor} Base
11
+ */
12
+ export default function InvokeFunctionsTransform(Base) {
13
+ return class InvokeFunctions extends Base {
14
+ async get(key) {
15
+ let value = await super.get(key);
16
+ if (typeof value === "function") {
17
+ const scope = Scope.getScope(this);
18
+ value = await value.call(scope);
19
+
20
+ if (Tree.isAsyncTree(value)) {
21
+ value.parent = this;
22
+ }
23
+ }
24
+ return value;
25
+ }
26
+ };
27
+ }
@@ -0,0 +1,11 @@
1
+ import { FileTree } from "@weborigami/async-tree";
2
+ import EventTargetMixin from "./EventTargetMixin.js";
3
+ import ImportModulesMixin from "./ImportModulesMixin.js";
4
+ import OrigamiTransform from "./OrigamiTransform.js";
5
+ import WatchFilesMixin from "./WatchFilesMixin.js";
6
+
7
+ export default class OrigamiFiles extends OrigamiTransform(
8
+ (
9
+ ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
10
+ )
11
+ ) {}
@@ -0,0 +1,9 @@
1
+ import { FileTree } from "@weborigami/async-tree";
2
+ import EventTargetMixin from "./EventTargetMixin.js";
3
+ import ImportModulesMixin from "./ImportModulesMixin.js";
4
+ import OrigamiTransform from "./OrigamiTransform.js";
5
+ import WatchFilesMixin from "./WatchFilesMixin.js";
6
+
7
+ export default class OrigamiFiles extends OrigamiTransform(
8
+ ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
9
+ ) {}
@@ -0,0 +1,11 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ import type { AsyncTree } from "@weborigami/types";
4
+
5
+ // TODO: Figure out how to import declarations from InheritScopeMixin and
6
+ // FileLoadersTransform and apply them here.
7
+ declare const OrigamiTransform: Mixin<{
8
+ scope: AsyncTree|null;
9
+ }>;
10
+
11
+ export default OrigamiTransform;
@@ -0,0 +1,11 @@
1
+ import FileLoadersTransform from "./FileLoadersTransform.js";
2
+ import InheritScopeMixin from "./InheritScopeMixin.js";
3
+
4
+ /**
5
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
6
+ * @typedef {import("../../index.js").Constructor<AsyncTree>} AsyncTreeConstructor
7
+ * @param {AsyncTreeConstructor} Base
8
+ */
9
+ export default function OrigamiTransform(Base) {
10
+ return class Origami extends InheritScopeMixin(FileLoadersTransform(Base)) {};
11
+ }
@@ -0,0 +1,4 @@
1
+ import { ExpressionTree } from "./internal.js";
2
+ import OrigamiTransform from "./OrigamiTransform.js";
3
+
4
+ export default class OrigamiTree extends OrigamiTransform(ExpressionTree) {}
@@ -0,0 +1 @@
1
+ Modules necessary to evaluate Origami expressions
@@ -0,0 +1,89 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+
3
+ /**
4
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
5
+ * @implements {AsyncTree}
6
+ */
7
+ export default class Scope {
8
+ constructor(...treelikes) {
9
+ const filtered = treelikes.filter((treelike) => treelike != undefined);
10
+ const trees = filtered.map((treelike) => Tree.from(treelike));
11
+
12
+ // If a tree argument has a `trees` property, use that instead.
13
+ const scopes = trees.flatMap(
14
+ (tree) => /** @type {any} */ (tree).trees ?? tree
15
+ );
16
+
17
+ this.trees = scopes;
18
+ }
19
+
20
+ async get(key) {
21
+ for (const tree of this.trees) {
22
+ const value = await tree.get(key);
23
+ if (value !== undefined) {
24
+ return value;
25
+ }
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ /**
31
+ * If the given tree has a `scope` property, return that. If the tree has a
32
+ * `parent` property, construct a scope for the tree and its parent.
33
+ * Otherwise, return the tree itself.
34
+ *
35
+ * @param {AsyncTree|null|undefined} tree
36
+ * @returns {AsyncTree|null}
37
+ */
38
+ static getScope(tree) {
39
+ if (!tree) {
40
+ return null;
41
+ } else if ("scope" in tree) {
42
+ return /** @type {any} */ (tree).scope;
43
+ } else if (Tree.isAsyncTree(tree)) {
44
+ return new Scope(tree, this.getScope(tree.parent));
45
+ } else {
46
+ return tree;
47
+ }
48
+ }
49
+
50
+ async keys() {
51
+ const keys = new Set();
52
+ for (const tree of this.trees) {
53
+ for (const key of await tree.keys()) {
54
+ keys.add(key);
55
+ }
56
+ }
57
+ return keys;
58
+ }
59
+
60
+ /**
61
+ * Return a new tree equivalent to the given tree, but with the given scope.
62
+ *
63
+ * The tree itself will be automatically included at the front of the scope.
64
+ *
65
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
66
+ * @param {Treelike} treelike
67
+ * @param {Treelike|null} scope
68
+ * @returns {AsyncTree & { scope: AsyncTree }}
69
+ */
70
+ static treeWithScope(treelike, scope) {
71
+ // If the treelike was already a tree, create a copy of it.
72
+ const tree = Tree.isAsyncTree(treelike)
73
+ ? Object.create(treelike)
74
+ : Tree.from(treelike);
75
+ tree.scope = new Scope(tree, scope);
76
+ return tree;
77
+ }
78
+
79
+ async unwatch() {
80
+ for (const tree of this.trees) {
81
+ await /** @type {any} */ (tree).unwatch?.();
82
+ }
83
+ }
84
+ async watch() {
85
+ for (const tree of this.trees) {
86
+ await /** @type {any} */ (tree).watch?.();
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,6 @@
1
+ export default class TreeEvent extends Event {
2
+ constructor(type, options = {}) {
3
+ super(type, options);
4
+ this.options = options;
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ import { Mixin } from "../../index.ts";
2
+
3
+ declare const WatchFilesMixin: Mixin<{}>;
4
+
5
+ export default WatchFilesMixin;
@@ -0,0 +1,58 @@
1
+ import * as fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import Watcher from "watcher";
4
+ import TreeEvent from "./TreeEvent.js";
5
+
6
+ // Map of paths to trees used by watcher
7
+ const pathTreeMap = new Map();
8
+
9
+ export default function WatchFilesMixin(Base) {
10
+ return class WatchFiles extends Base {
11
+ addEventListener(type, listener) {
12
+ super.addEventListener(type, listener);
13
+ if (type === "change") {
14
+ this.watch();
15
+ }
16
+ }
17
+
18
+ onChange(key) {
19
+ // Reset cached values.
20
+ this.subfoldersMap = new Map();
21
+ this.dispatchEvent(new TreeEvent("change", { key }));
22
+ }
23
+
24
+ async unwatch() {
25
+ if (!this.watching) {
26
+ return;
27
+ }
28
+
29
+ this.watcher?.close();
30
+ this.watching = false;
31
+ }
32
+
33
+ // Turn on watching for the directory.
34
+ async watch() {
35
+ if (this.watching) {
36
+ return;
37
+ }
38
+ this.watching = true;
39
+
40
+ // Ensure the directory exists.
41
+ await fs.mkdir(this.dirname, { recursive: true });
42
+
43
+ this.watcher = new Watcher(this.dirname, {
44
+ ignoreInitial: true,
45
+ persistent: false,
46
+ });
47
+ this.watcher.on("all", (event, filePath) => {
48
+ const key = path.basename(filePath);
49
+ this.onChange(key);
50
+ });
51
+
52
+ // Add to the list of FileTree instances watching this directory.
53
+ const treeRefs = pathTreeMap.get(this.dirname) ?? [];
54
+ treeRefs.push(new WeakRef(this));
55
+ pathTreeMap.set(this.dirname, treeRefs);
56
+ }
57
+ };
58
+ }
@@ -0,0 +1,46 @@
1
+ import { Tree, getRealmObjectPrototype } from "@weborigami/async-tree";
2
+
3
+ /**
4
+ * Concatenate the text values in a tree.
5
+ *
6
+ * This is a map-reduce operation: convert everything to strings, then
7
+ * concatenate the strings.
8
+ *
9
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
+ *
11
+ * @this {AsyncTree|null}
12
+ * @param {import("@weborigami/async-tree").Treelike} treelike
13
+ */
14
+ export default async function concatTreeValues(treelike) {
15
+ const scope = this;
16
+ const mapFn = async (value) => getText(value, scope);
17
+ const reduceFn = (values) => values.join("");
18
+ return Tree.mapReduce(treelike, mapFn, reduceFn);
19
+ }
20
+
21
+ async function getText(value, scope) {
22
+ // If the value is a function (e.g., a lambda), call it and use its result.
23
+ if (typeof value === "function") {
24
+ value = await value.call(scope);
25
+ }
26
+
27
+ // Convert to text, preferring .toString but avoiding dumb Object.toString.
28
+ // Exception: if the result is an array, we'll concatenate the values.
29
+ let text;
30
+ if (!value) {
31
+ // Treat falsy values as the empty string.
32
+ text = "";
33
+ } else if (typeof value === "string") {
34
+ text = value;
35
+ } else if (
36
+ !(value instanceof Array) &&
37
+ value.toString !== getRealmObjectPrototype(value).toString
38
+ ) {
39
+ text = value.toString();
40
+ } else {
41
+ // Anything else maps to the empty string.
42
+ text = "";
43
+ }
44
+
45
+ return text;
46
+ }