@weborigami/origami 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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/ReadMe.md +3 -0
  3. package/exports/PathTransform.d.ts +5 -0
  4. package/exports/PathTransform.js +18 -0
  5. package/exports/buildExports.js +109 -0
  6. package/exports/exports.js +121 -0
  7. package/index.ts +25 -0
  8. package/package.json +40 -0
  9. package/src/builtins/!.js +21 -0
  10. package/src/builtins/@apply.js +6 -0
  11. package/src/builtins/@arrows.js +34 -0
  12. package/src/builtins/@builtins.js +18 -0
  13. package/src/builtins/@cache.js +36 -0
  14. package/src/builtins/@config.js +25 -0
  15. package/src/builtins/@copy.js +71 -0
  16. package/src/builtins/@crawl.js +507 -0
  17. package/src/builtins/@debug.js +89 -0
  18. package/src/builtins/@document.js +18 -0
  19. package/src/builtins/@equals.js +6 -0
  20. package/src/builtins/@explore.js +68 -0
  21. package/src/builtins/@false.js +1 -0
  22. package/src/builtins/@files.js +22 -0
  23. package/src/builtins/@filter.js +23 -0
  24. package/src/builtins/@globs.js +23 -0
  25. package/src/builtins/@help.js +49 -0
  26. package/src/builtins/@http.js +19 -0
  27. package/src/builtins/@https.js +19 -0
  28. package/src/builtins/@if.js +27 -0
  29. package/src/builtins/@image/format.js +5 -0
  30. package/src/builtins/@image/resize.js +5 -0
  31. package/src/builtins/@index.js +72 -0
  32. package/src/builtins/@inherited.js +17 -0
  33. package/src/builtins/@inline.js +29 -0
  34. package/src/builtins/@invoke.js +30 -0
  35. package/src/builtins/@js.js +33 -0
  36. package/src/builtins/@json.js +22 -0
  37. package/src/builtins/@loaders/css.js +4 -0
  38. package/src/builtins/@loaders/htm.js +4 -0
  39. package/src/builtins/@loaders/html.js +4 -0
  40. package/src/builtins/@loaders/js.js +14 -0
  41. package/src/builtins/@loaders/json.js +8 -0
  42. package/src/builtins/@loaders/md.js +4 -0
  43. package/src/builtins/@loaders/mjs.js +4 -0
  44. package/src/builtins/@loaders/ori.js +21 -0
  45. package/src/builtins/@loaders/orit.js +48 -0
  46. package/src/builtins/@loaders/txt.js +33 -0
  47. package/src/builtins/@loaders/xhtml.js +4 -0
  48. package/src/builtins/@loaders/yaml.js +18 -0
  49. package/src/builtins/@loaders/yml.js +4 -0
  50. package/src/builtins/@map.js +182 -0
  51. package/src/builtins/@match.js +92 -0
  52. package/src/builtins/@mdHtml.js +45 -0
  53. package/src/builtins/@new.js +6 -0
  54. package/src/builtins/@node.js +15 -0
  55. package/src/builtins/@not.js +6 -0
  56. package/src/builtins/@or.js +6 -0
  57. package/src/builtins/@ori.js +83 -0
  58. package/src/builtins/@pack.js +13 -0
  59. package/src/builtins/@parse/json.js +7 -0
  60. package/src/builtins/@parse/yaml.js +9 -0
  61. package/src/builtins/@project.js +71 -0
  62. package/src/builtins/@repeat.js +8 -0
  63. package/src/builtins/@rss.js +49 -0
  64. package/src/builtins/@scope/extend.js +22 -0
  65. package/src/builtins/@scope/get.js +25 -0
  66. package/src/builtins/@scope/invoke.js +22 -0
  67. package/src/builtins/@scope/set.js +25 -0
  68. package/src/builtins/@serve.js +74 -0
  69. package/src/builtins/@shell.js +16 -0
  70. package/src/builtins/@stdin.js +26 -0
  71. package/src/builtins/@svg.js +42 -0
  72. package/src/builtins/@tree/concat.js +21 -0
  73. package/src/builtins/@tree/count.js +24 -0
  74. package/src/builtins/@tree/defineds.js +37 -0
  75. package/src/builtins/@tree/dot.js +201 -0
  76. package/src/builtins/@tree/exceptions.js +50 -0
  77. package/src/builtins/@tree/first.js +28 -0
  78. package/src/builtins/@tree/flowSvg.js +55 -0
  79. package/src/builtins/@tree/fn.js +34 -0
  80. package/src/builtins/@tree/from.js +27 -0
  81. package/src/builtins/@tree/fromJson.js +6 -0
  82. package/src/builtins/@tree/fromYaml.js +24 -0
  83. package/src/builtins/@tree/groupBy.js +39 -0
  84. package/src/builtins/@tree/inners.js +44 -0
  85. package/src/builtins/@tree/isAsyncTree.js +17 -0
  86. package/src/builtins/@tree/keys.js +24 -0
  87. package/src/builtins/@tree/keysJson.js +44 -0
  88. package/src/builtins/@tree/map.d.ts +19 -0
  89. package/src/builtins/@tree/merge.js +47 -0
  90. package/src/builtins/@tree/mergeDeep.js +44 -0
  91. package/src/builtins/@tree/nextKey.js +29 -0
  92. package/src/builtins/@tree/parent.js +24 -0
  93. package/src/builtins/@tree/paths.js +35 -0
  94. package/src/builtins/@tree/plain.js +22 -0
  95. package/src/builtins/@tree/previousKey.js +29 -0
  96. package/src/builtins/@tree/reverse.js +51 -0
  97. package/src/builtins/@tree/setDeep.js +45 -0
  98. package/src/builtins/@tree/shuffle.js +31 -0
  99. package/src/builtins/@tree/sitemap.js +59 -0
  100. package/src/builtins/@tree/sort.js +25 -0
  101. package/src/builtins/@tree/sortBy.js +40 -0
  102. package/src/builtins/@tree/static.js +51 -0
  103. package/src/builtins/@tree/table.js +74 -0
  104. package/src/builtins/@tree/take.js +40 -0
  105. package/src/builtins/@tree/values.js +23 -0
  106. package/src/builtins/@tree/valuesDeep.js +23 -0
  107. package/src/builtins/@treeHttp.js +19 -0
  108. package/src/builtins/@treeHttps.js +19 -0
  109. package/src/builtins/@true.js +1 -0
  110. package/src/builtins/@unpack.js +13 -0
  111. package/src/builtins/@watch.js +108 -0
  112. package/src/builtins/@with.js +22 -0
  113. package/src/builtins/@yaml.js +23 -0
  114. package/src/builtins/~.js +9 -0
  115. package/src/cli/cli.js +86 -0
  116. package/src/cli/defaultModuleExport.js +16 -0
  117. package/src/cli/showUsage.js +86 -0
  118. package/src/common/CommandModulesTransform.d.ts +5 -0
  119. package/src/common/CommandModulesTransform.js +37 -0
  120. package/src/common/ConstantTree.js +17 -0
  121. package/src/common/ExplorableSiteTransform.d.ts +5 -0
  122. package/src/common/ExplorableSiteTransform.js +77 -0
  123. package/src/common/FilterTree.js +60 -0
  124. package/src/common/GlobTree.js +67 -0
  125. package/src/common/ShuffleTransform.js +29 -0
  126. package/src/common/TextDocument.js +57 -0
  127. package/src/common/addValueKeyToScope.js +30 -0
  128. package/src/common/arrowFunctionsMap.js +35 -0
  129. package/src/common/processUnpackedContent.js +39 -0
  130. package/src/common/serialize.d.ts +8 -0
  131. package/src/common/serialize.js +138 -0
  132. package/src/common/utilities.d.ts +7 -0
  133. package/src/common/utilities.js +132 -0
  134. package/src/misc/OriCommandTransform.d.ts +5 -0
  135. package/src/misc/OriCommandTransform.js +54 -0
  136. package/src/misc/assertScopeIsDefined.js +7 -0
  137. package/src/misc/explore.orit +241 -0
  138. package/src/misc/yamlOrigamiTag.js +17 -0
  139. package/src/server/mediaTypes.js +97 -0
  140. package/src/server/server.js +258 -0
@@ -0,0 +1,25 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+ import { keySymbol } from "../../common/utilities.js";
4
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
5
+
6
+ /**
7
+ * Return a copy of the given tree that has the indicated trees as its scope.
8
+ *
9
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
11
+ * @param {Treelike} treelike
12
+ * @param {...(Treelike|null)} scopeTrees
13
+ * @this {AsyncTree|null}
14
+ */
15
+ export default function setScope(treelike, ...scopeTrees) {
16
+ assertScopeIsDefined(this);
17
+ const tree = Tree.from(treelike);
18
+ const scope = scopeTrees.length === 0 ? this : new Scope(...scopeTrees);
19
+ const result = Scope.treeWithScope(tree, scope);
20
+ result[keySymbol] = tree[keySymbol];
21
+ return result;
22
+ }
23
+
24
+ setScope.usage = `@scope/set <tree>, <...trees>\tReturns a tree copy with the given scope`;
25
+ setScope.documentation = "https://graphorigami.org/cli/builtins.html#@scope";
@@ -0,0 +1,74 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import http from "node:http";
3
+ import { createServer } from "node:net";
4
+ import process from "node:process";
5
+ import ExplorableSiteTransform from "../common/ExplorableSiteTransform.js";
6
+ import { isTransformApplied, transformObject } from "../common/utilities.js";
7
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
8
+ import { requestListener } from "../server/server.js";
9
+ import debug from "./@debug.js";
10
+ import watch from "./@watch.js";
11
+
12
+ const defaultPort = 5000;
13
+
14
+ /**
15
+ * Start a local web server for the indicated tree.
16
+ *
17
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
18
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
19
+ *
20
+ * @param {Treelike} treelike
21
+ * @param {number} [port]
22
+ * @this {AsyncTree|null}
23
+ */
24
+ export default async function serve(treelike, port) {
25
+ assertScopeIsDefined(this);
26
+ let tree;
27
+ if (treelike) {
28
+ tree = Tree.from(treelike);
29
+
30
+ // TODO: Instead of applying ExplorableSiteTransform, apply a transform
31
+ // that just maps the empty string to index.html.
32
+ if (!isTransformApplied(ExplorableSiteTransform, tree)) {
33
+ tree = transformObject(ExplorableSiteTransform, tree);
34
+ }
35
+ } else {
36
+ // By default, watch the default tree and add default pages.
37
+ const withDefaults = await debug.call(this);
38
+ tree = await watch.call(this, withDefaults);
39
+ }
40
+
41
+ if (port === undefined) {
42
+ if (process.env.PORT) {
43
+ // Use the port specified in the environment.
44
+ port = parseInt(process.env.PORT);
45
+ } else {
46
+ // Find an open port.
47
+ port = await findOpenPort(defaultPort);
48
+ }
49
+ }
50
+
51
+ // @ts-ignore
52
+ http.createServer(requestListener(tree)).listen(port, undefined, () => {
53
+ console.log(
54
+ `Server running at http://localhost:${port}. Press Ctrl+C to stop.`
55
+ );
56
+ });
57
+ }
58
+
59
+ // Return the first open port number on or after the given port number.
60
+ // From https://gist.github.com/mikeal/1840641?permalink_comment_id=2896667#gistcomment-2896667
61
+ function findOpenPort(port) {
62
+ const server = createServer();
63
+ return new Promise((resolve, reject) =>
64
+ server
65
+ .on("error", (/** @type {any} */ error) =>
66
+ error.code === "EADDRINUSE" ? server.listen(++port) : reject(error)
67
+ )
68
+ .on("listening", () => server.close(() => resolve(port)))
69
+ .listen(port)
70
+ );
71
+ }
72
+
73
+ serve.usage = `@serve <tree>, [port]\tStart a web server for the tree`;
74
+ serve.documentation = "https://graphorigami.org/language/@serve.html";
@@ -0,0 +1,16 @@
1
+ import { exec as callbackExec } from "node:child_process";
2
+ import util from "node:util";
3
+ const exec = util.promisify(callbackExec);
4
+
5
+ export default async function shell(command) {
6
+ try {
7
+ const { stdout } = await exec(command);
8
+ return stdout;
9
+ } catch (err) {
10
+ console.error(err);
11
+ return undefined;
12
+ }
13
+ }
14
+
15
+ shell.usage = `@shell <command>\tExecutes the shell command and returns the output`;
16
+ shell.documentation = "https://graphorigami.org/language/@shell.html";
@@ -0,0 +1,26 @@
1
+ import process from "node:process";
2
+
3
+ export default async function stdin() {
4
+ return readAll(process.stdin);
5
+ }
6
+
7
+ function readAll(readable) {
8
+ return new Promise((resolve) => {
9
+ const chunks = [];
10
+
11
+ readable.on("readable", () => {
12
+ let chunk;
13
+ while (null !== (chunk = readable.read())) {
14
+ chunks.push(chunk);
15
+ }
16
+ });
17
+
18
+ readable.on("end", () => {
19
+ const buffer = Buffer.concat(chunks);
20
+ resolve(buffer);
21
+ });
22
+ });
23
+ }
24
+
25
+ stdin.usage = `@stdin\tReturns the contents of the standard input stream`;
26
+ stdin.documentation = "https://graphorigami.org/language/@stdin.html";
@@ -0,0 +1,42 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import graphviz from "graphviz-wasm";
3
+ import assertScopeIsDefined from "../misc/assertScopeIsDefined.js";
4
+ import dot from "./@tree/dot.js";
5
+
6
+ let graphvizLoaded = false;
7
+
8
+ /**
9
+ * Render a tree visually in SVG format.
10
+ *
11
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
12
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
13
+ * @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
14
+ *
15
+ * @this {AsyncTree|null}
16
+ * @param {Treelike} [treelike]
17
+ * @param {PlainObject} [options]
18
+ */
19
+ export default async function svg(treelike, options = {}) {
20
+ assertScopeIsDefined(this);
21
+ if (!graphvizLoaded) {
22
+ await graphviz.loadWASM();
23
+ graphvizLoaded = true;
24
+ }
25
+ treelike = treelike ?? (await this?.get("@current"));
26
+ if (treelike === undefined) {
27
+ return undefined;
28
+ }
29
+ const tree = Tree.from(treelike);
30
+ const dotText = await dot.call(this, tree, options);
31
+ if (dotText === undefined) {
32
+ return undefined;
33
+ }
34
+ const svgText = await graphviz.layout(dotText, "svg");
35
+ /** @type {any} */
36
+ const result = new String(svgText);
37
+ result.unpack = () => tree;
38
+ return result;
39
+ }
40
+
41
+ svg.usage = `@svg <tree>\tRender a tree visually as in SVG format`;
42
+ svg.documentation = "https://graphorigami.org/language/@svg.html";
@@ -0,0 +1,21 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { ops } from "@weborigami/language";
3
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
4
+
5
+ /**
6
+ * Concatenate the text content of objects or trees.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ *
10
+ * @this {AsyncTree|null}
11
+ * @param {any[]} args
12
+ */
13
+ export default async function concat(...args) {
14
+ assertScopeIsDefined(this);
15
+ const tree =
16
+ args.length === 0 ? await this?.get("@current") : Tree.from(args);
17
+ return ops.concat.call(this, tree);
18
+ }
19
+
20
+ concat.usage = `concat <...objs>\tConcatenate text and/or trees of text`;
21
+ concat.documentation = "https://graphorigami.org/cli/@tree.html#concat";
@@ -0,0 +1,24 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
3
+
4
+ /**
5
+ * Return the number of keys in the tree.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
9
+ * @this {AsyncTree|null}
10
+ * @param {Treelike} [treelike]
11
+ */
12
+ export default async function count(treelike) {
13
+ assertScopeIsDefined(this);
14
+ treelike = treelike ?? (await this?.get("@current"));
15
+ if (treelike === undefined) {
16
+ return undefined;
17
+ }
18
+ const tree = await Tree.from(treelike);
19
+ const keys = [...(await tree.keys())];
20
+ return keys.length;
21
+ }
22
+
23
+ count.usage = `count <treelike>\tReturn the number of keys in the tree`;
24
+ count.documentation = "https://graphorigami.org/cli/@tree.html#count";
@@ -0,0 +1,37 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
4
+
5
+ /**
6
+ * Return only the defined (not `undefined`) values in the tree.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
10
+ *
11
+ * @this {AsyncTree|null}
12
+ * @param {Treelike} treelike
13
+ */
14
+ export default async function defineds(treelike) {
15
+ assertScopeIsDefined(this);
16
+ treelike = treelike ?? (await this?.get("@current"));
17
+ if (treelike === undefined) {
18
+ throw new TypeError("A treelike argument is required");
19
+ }
20
+
21
+ /** @type {AsyncTree} */
22
+ let result = await Tree.mapReduce(treelike, null, async (values, keys) => {
23
+ const result = {};
24
+ let someValuesExist = false;
25
+ for (let i = 0; i < keys.length; i++) {
26
+ const value = values[i];
27
+ if (value != null) {
28
+ someValuesExist = true;
29
+ result[keys[i]] = values[i];
30
+ }
31
+ }
32
+ return someValuesExist ? result : null;
33
+ });
34
+
35
+ result = Scope.treeWithScope(result, this);
36
+ return result;
37
+ }
@@ -0,0 +1,201 @@
1
+ import { Tree, isPlainObject, isStringLike } from "@weborigami/async-tree";
2
+ import { extname } from "@weborigami/language";
3
+ import * as serialize from "../../common/serialize.js";
4
+ import { keySymbol } from "../../common/utilities.js";
5
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
6
+
7
+ /**
8
+ * Render a tree in DOT format.
9
+ *
10
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
11
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
12
+ * @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
13
+ *
14
+ * @this {AsyncTree|null}
15
+ * @param {Treelike} [treelike]
16
+ * @param {PlainObject} [options]
17
+ */
18
+ export default async function dot(treelike, options = {}) {
19
+ assertScopeIsDefined(this);
20
+ treelike = treelike ?? (await this?.get("@current"));
21
+ if (treelike === undefined) {
22
+ return undefined;
23
+ }
24
+ const tree = Tree.from(treelike);
25
+ const rootLabel = tree[keySymbol] ?? "";
26
+ const treeArcs = await statements(tree, "", rootLabel, options);
27
+ return `digraph g {
28
+ bgcolor="transparent";
29
+ nodesep=1;
30
+ rankdir=LR;
31
+ ranksep=1.5;
32
+ node [color=gray70; fillcolor="white"; fontname="Helvetica"; fontsize="10"; nojustify=true; style="filled"; shape=box];
33
+ edge [arrowhead=vee; arrowsize=0.75; color=gray60; fontname="Helvetica"; fontsize="10"; labeldistance=5];
34
+
35
+ ${treeArcs.join("\n")}
36
+ }`;
37
+ }
38
+
39
+ // Return true if the text appears to contain non-printable binary characters.
40
+ function probablyBinary(text) {
41
+ // https://stackoverflow.com/a/1677660/76472
42
+ return /[\x00-\x08\x0E-\x1F]/.test(text);
43
+ }
44
+
45
+ async function statements(tree, nodePath, nodeLabel, options) {
46
+ let result = [];
47
+ const createLinks = options.createLinks ?? true;
48
+
49
+ // Add a node for the root of this (sub)tree.
50
+ const rootUrl = nodePath || ".";
51
+ const url = createLinks ? `; URL="${rootUrl}"` : "";
52
+ const rootLabel = nodeLabel ? `; xlabel="${nodeLabel}"` : "";
53
+ result.push(
54
+ ` "${nodePath}" [shape=circle${rootLabel}; label=""; color=gray40; width=0.15${url}];`
55
+ );
56
+
57
+ // Draw edges and collect labels for the nodes they lead to.
58
+ let nodes = {};
59
+ for (const key of await tree.keys()) {
60
+ const destPath = nodePath ? `${nodePath}/${key}` : key;
61
+ const arc = ` "${nodePath}" -> "${destPath}" [label="${key}"];`;
62
+ result.push(arc);
63
+
64
+ let isError = false;
65
+ let value;
66
+ try {
67
+ value = await tree.get(key);
68
+ } catch (/** @type {any} */ error) {
69
+ isError = true;
70
+ value =
71
+ error.name && error.message
72
+ ? `${error.name}: ${error.message}`
73
+ : error.name ?? error.message ?? error;
74
+ }
75
+
76
+ // We expand certain types of files known to contain trees.
77
+ const extension = key ? extname(key).toLowerCase() : "";
78
+ const expand =
79
+ {
80
+ ".json": true,
81
+ ".yaml": true,
82
+ }[extension] ?? extension === "";
83
+
84
+ const expandable =
85
+ value instanceof Array || isPlainObject(value) || Tree.isAsyncTree(value);
86
+ if (expand && expandable) {
87
+ const subtree = Tree.from(value);
88
+ const subStatements = await statements(subtree, destPath, null, options);
89
+ result = result.concat(subStatements);
90
+ } else {
91
+ const label = isStringLike(value)
92
+ ? String(value)
93
+ : await serialize.toYaml(value);
94
+ nodes[key] = { label };
95
+ if (isError) {
96
+ nodes[key].isError = true;
97
+ }
98
+ }
99
+ }
100
+
101
+ // If we have more than one label, we'll focus on the labels' differences.
102
+ // We'll use the first label as a representative baseline for all labels but
103
+ // the first (which will use the second label as a baseline).
104
+ const values = Object.values(nodes);
105
+ const showLabelDiffs = values.length > 1;
106
+ const label1 = showLabelDiffs ? String(values[0].label) : undefined;
107
+ const label2 = showLabelDiffs ? String(values[1].label) : undefined;
108
+
109
+ // Trim labels.
110
+ let i = 0;
111
+ for (const key of Object.keys(nodes)) {
112
+ let label = String(nodes[key].label);
113
+ if (probablyBinary(label)) {
114
+ nodes[key].label = "[binary data]";
115
+ } else if (label) {
116
+ let clippedStart = false;
117
+ let clippedEnd = false;
118
+
119
+ if (showLabelDiffs) {
120
+ const baseline = i === 0 ? label2 : label1;
121
+ const diff = stringDiff(baseline, label);
122
+ if (diff !== label) {
123
+ label = diff;
124
+ clippedStart = true;
125
+ }
126
+ }
127
+
128
+ label = label.trim();
129
+
130
+ if (label.length > 40) {
131
+ // Long text, just use the beginning
132
+ label = label.slice(0, 40);
133
+ clippedEnd = true;
134
+ }
135
+
136
+ // Left justify node label using weird Dot escape character
137
+ // See https://stackoverflow.com/a/13104953/76472
138
+ const endsWithNewline = label.endsWith("\n");
139
+ label = label.replace(/\n/g, "\\l");
140
+
141
+ label = label.replace(/"/g, '\\"'); // Escape quotes
142
+ label = label.replace(/[\ \t]+/g, " "); // Collapse spaces and tabs
143
+
144
+ // Add ellipses if we clipped the label. We'd prefer to end with a real
145
+ // ellipsis, but GraphViz warns about "non-ASCII character 226" if we do.
146
+ // (That's not even the ellipsis character!) We could use a real ellipsis
147
+ // for the start, but then they might look different.
148
+ if (clippedStart) {
149
+ label = "..." + label;
150
+ }
151
+ if (clippedEnd) {
152
+ label += "...";
153
+ }
154
+
155
+ if (!endsWithNewline) {
156
+ // See note above
157
+ label += "\\l";
158
+ }
159
+
160
+ nodes[key].label = label;
161
+ }
162
+ i++;
163
+ }
164
+
165
+ // Draw labels.
166
+ for (const key in nodes) {
167
+ const node = nodes[key];
168
+ const icon = node.isError ? "⚠️ " : "";
169
+ const label = `label="${icon}${node.label}"`;
170
+ const color = node.isError ? `; color="red"` : "";
171
+ const fill = node.isError ? `; fillcolor="#FFF4F4"` : "";
172
+ const destPath = nodePath ? `${nodePath}/${key}` : key;
173
+ const url = createLinks ? `; URL="${destPath}"` : "";
174
+ result.push(` "${destPath}" [${label}${color}${fill}${url}];`);
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ // Return the second string, removing the initial portion it shares with the
181
+ // first string. The returned string will start with the first non-whitespace
182
+ // character of the first line that differs from the first string.
183
+ function stringDiff(first, second) {
184
+ let i = 0;
185
+ // Find point of first difference.
186
+ while (i < first.length && i < second.length && first[i] === second[i]) {
187
+ i++;
188
+ }
189
+ // Back up to start of that line.
190
+ while (i > 0 && second[i - 1] !== "\n") {
191
+ i--;
192
+ }
193
+ // Move forward to first non-whitespace character.
194
+ while (i < second.length && /\s/.test(second[i])) {
195
+ i++;
196
+ }
197
+ return second.slice(i);
198
+ }
199
+
200
+ dot.usage = `dot <tree>\tRender a tree visually in dot language`;
201
+ dot.documentation = "https://graphorigami.org/cli/builtins.html#dot";
@@ -0,0 +1,50 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
4
+ import defineds from "./defineds.js";
5
+
6
+ /**
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
9
+ * @this {AsyncTree|null}
10
+ * @param {Treelike} treelike
11
+ */
12
+ export default async function exceptions(treelike) {
13
+ assertScopeIsDefined(this);
14
+ treelike = treelike ?? (await this?.get("@current"));
15
+
16
+ /** @type {AsyncTree} */
17
+ let exceptionsTree = new ExceptionsTree(treelike);
18
+ exceptionsTree = Scope.treeWithScope(exceptionsTree, this);
19
+ return defineds.call(this, exceptionsTree);
20
+ }
21
+
22
+ /**
23
+ * @implements {AsyncTree}
24
+ */
25
+ class ExceptionsTree {
26
+ constructor(treelike) {
27
+ this.tree = Tree.from(treelike);
28
+ }
29
+
30
+ async get(key) {
31
+ try {
32
+ const value = await this.tree.get(key);
33
+ return Tree.isAsyncTree(value)
34
+ ? Reflect.construct(this.constructor, [value])
35
+ : undefined;
36
+ } catch (/** @type {any} */ error) {
37
+ return error.name && error.message
38
+ ? `${error.name}: ${error.message}`
39
+ : error.name ?? error.message ?? error;
40
+ }
41
+ }
42
+
43
+ async keys() {
44
+ return this.tree.keys();
45
+ }
46
+ }
47
+
48
+ exceptions.usage = `exceptions tree\tReturn a tree of exceptions thrown in the tree`;
49
+ exceptions.documentation =
50
+ "https://graphorigami.org/cli/builtins.html#exceptions";
@@ -0,0 +1,28 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
3
+
4
+ /**
5
+ * Return the first value in the tree.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
9
+ * @this {AsyncTree|null}
10
+ * @param {Treelike} [treelike]
11
+ */
12
+ export default async function first(treelike) {
13
+ assertScopeIsDefined(this);
14
+ treelike = treelike ?? (await this?.get("@current"));
15
+ if (treelike === undefined) {
16
+ return undefined;
17
+ }
18
+ const tree = Tree.from(treelike);
19
+ for (const key of await tree.keys()) {
20
+ // Just return first value immediately.
21
+ const value = await tree.get(key);
22
+ return value;
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ first.usage = `first <tree>\tReturns the first value in the tree.`;
28
+ first.documentation = "https://graphorigami.org/cli/builtins.html#first";
@@ -0,0 +1,55 @@
1
+ import graphviz from "graphviz-wasm";
2
+
3
+ let graphvizLoaded = false;
4
+
5
+ export default async function flowSvg(flow) {
6
+ if (!graphvizLoaded) {
7
+ await graphviz.loadWASM();
8
+ graphvizLoaded = true;
9
+ }
10
+ const dot = flowDot(flow);
11
+ const svg = await graphviz.layout(dot, "svg");
12
+ return svg;
13
+ }
14
+
15
+ function flowDot(flow) {
16
+ const nodes = [];
17
+ const edges = [];
18
+ for (const [key, record] of Object.entries(flow)) {
19
+ const dependencies = record.dependencies ?? [];
20
+ let label = record.label ?? key;
21
+ if (record.undefined) {
22
+ label += " (?)";
23
+ }
24
+ const virtualNode = dependencies.length > 0;
25
+ const url = record.url ?? key;
26
+ const nodeLabel = `label="${label}"`;
27
+ const nodeUrl = `URL=".scope/${url}"`;
28
+ const nodeShape = record.undefined ? `shape="none"` : "";
29
+ const nodeStyle = virtualNode ? `style="dashed"` : null;
30
+ const attributes = [nodeLabel, nodeShape, nodeStyle, nodeUrl].filter(
31
+ (attribute) => attribute
32
+ );
33
+ const nodeDot = ` "${key}" [${attributes.join("; ")}];`;
34
+ nodes.push(nodeDot);
35
+
36
+ for (const dependency of dependencies) {
37
+ edges.push(` "${dependency}" -> "${key}";`);
38
+ }
39
+ }
40
+
41
+ return `digraph dataflow {
42
+ nodesep=1;
43
+ rankdir=LR;
44
+ ranksep=1.5;
45
+ node [color="gray70"; fillcolor="white"; fontname="Helvetica"; fontsize="10"; nojustify="true"; style="filled"; shape="box"];
46
+ edge [arrowhead="onormal"; arrowsize="0.75"; color="gray60"; fontname="Helvetica"; fontsize="10"; labeldistance="5"];
47
+
48
+ ${nodes.join("\n")}
49
+
50
+ ${edges.join("\n")}
51
+ }`;
52
+ }
53
+
54
+ // flowSvg.usage = `flowSvg <dataflow>\tRenders the output of dataflow() as an SVG`;
55
+ // flowSvg.documentation = "https://graphorigami.org/cli/builtins.html#flowSvg";
@@ -0,0 +1,34 @@
1
+ import { FunctionTree } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+ import { toFunction } from "../../common/utilities.js";
4
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
5
+
6
+ /**
7
+ * Create a tree from a function and a set of keys.
8
+ *
9
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
10
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
11
+ * @typedef {import("../../../index.ts").Invocable} Invocable
12
+ *
13
+ * @this {AsyncTree|null}
14
+ * @param {Invocable} [invocable]
15
+ */
16
+ export default async function fn(invocable, keys = []) {
17
+ assertScopeIsDefined(this);
18
+ invocable = invocable ?? (await this?.get("@current"));
19
+ if (invocable === undefined) {
20
+ return undefined;
21
+ }
22
+ const invocableFn = toFunction(invocable);
23
+
24
+ /** @this {AsyncTree|null} */
25
+ async function extendedFn(key) {
26
+ const ambientsTree = Scope.treeWithScope({ "@key": key }, this);
27
+ return invocableFn.call(ambientsTree, key);
28
+ }
29
+
30
+ return new FunctionTree(extendedFn, keys);
31
+ }
32
+
33
+ fn.usage = `fn <fn>, [<keys>]\tCreate a tree from a function and a set of keys`;
34
+ fn.documentation = "https://graphorigami.org/cli/tree.html#fn";
@@ -0,0 +1,27 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import { Scope } from "@weborigami/language";
3
+ import assertScopeIsDefined from "../../misc/assertScopeIsDefined.js";
4
+
5
+ /**
6
+ * Cast the indicated treelike to a tree.
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
10
+ * @this {AsyncTree|null}
11
+ * @param {Treelike} [treelike]
12
+ */
13
+ export default async function tree(treelike) {
14
+ assertScopeIsDefined(this);
15
+ treelike = treelike ?? (await this?.get("@current"));
16
+ if (treelike === undefined) {
17
+ return undefined;
18
+ }
19
+
20
+ /** @type {AsyncTree} */
21
+ let result = Tree.from(treelike);
22
+ result = Scope.treeWithScope(result, this);
23
+ return result;
24
+ }
25
+
26
+ tree.usage = `from <treelike>\tConvert JSON, YAML, function, or plain object to a tree`;
27
+ tree.documentation = "https://graphorigami.org/cli/builtins.html#tree";
@@ -0,0 +1,6 @@
1
+ export default async function fromJson(text) {
2
+ return text ? JSON.parse(text) : undefined;
3
+ }
4
+
5
+ fromJson.usage = `fromJson <text>\tParse text as JSON`;
6
+ fromJson.documentation = "https://graphorigami.org/cli/builtins.html#fromJson";