@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.
- package/LICENSE +21 -0
- package/ReadMe.md +3 -0
- package/exports/PathTransform.d.ts +5 -0
- package/exports/PathTransform.js +18 -0
- package/exports/buildExports.js +109 -0
- package/exports/exports.js +121 -0
- package/index.ts +25 -0
- package/package.json +40 -0
- package/src/builtins/!.js +21 -0
- package/src/builtins/@apply.js +6 -0
- package/src/builtins/@arrows.js +34 -0
- package/src/builtins/@builtins.js +18 -0
- package/src/builtins/@cache.js +36 -0
- package/src/builtins/@config.js +25 -0
- package/src/builtins/@copy.js +71 -0
- package/src/builtins/@crawl.js +507 -0
- package/src/builtins/@debug.js +89 -0
- package/src/builtins/@document.js +18 -0
- package/src/builtins/@equals.js +6 -0
- package/src/builtins/@explore.js +68 -0
- package/src/builtins/@false.js +1 -0
- package/src/builtins/@files.js +22 -0
- package/src/builtins/@filter.js +23 -0
- package/src/builtins/@globs.js +23 -0
- package/src/builtins/@help.js +49 -0
- package/src/builtins/@http.js +19 -0
- package/src/builtins/@https.js +19 -0
- package/src/builtins/@if.js +27 -0
- package/src/builtins/@image/format.js +5 -0
- package/src/builtins/@image/resize.js +5 -0
- package/src/builtins/@index.js +72 -0
- package/src/builtins/@inherited.js +17 -0
- package/src/builtins/@inline.js +29 -0
- package/src/builtins/@invoke.js +30 -0
- package/src/builtins/@js.js +33 -0
- package/src/builtins/@json.js +22 -0
- package/src/builtins/@loaders/css.js +4 -0
- package/src/builtins/@loaders/htm.js +4 -0
- package/src/builtins/@loaders/html.js +4 -0
- package/src/builtins/@loaders/js.js +14 -0
- package/src/builtins/@loaders/json.js +8 -0
- package/src/builtins/@loaders/md.js +4 -0
- package/src/builtins/@loaders/mjs.js +4 -0
- package/src/builtins/@loaders/ori.js +21 -0
- package/src/builtins/@loaders/orit.js +48 -0
- package/src/builtins/@loaders/txt.js +33 -0
- package/src/builtins/@loaders/xhtml.js +4 -0
- package/src/builtins/@loaders/yaml.js +18 -0
- package/src/builtins/@loaders/yml.js +4 -0
- package/src/builtins/@map.js +182 -0
- package/src/builtins/@match.js +92 -0
- package/src/builtins/@mdHtml.js +45 -0
- package/src/builtins/@new.js +6 -0
- package/src/builtins/@node.js +15 -0
- package/src/builtins/@not.js +6 -0
- package/src/builtins/@or.js +6 -0
- package/src/builtins/@ori.js +83 -0
- package/src/builtins/@pack.js +13 -0
- package/src/builtins/@parse/json.js +7 -0
- package/src/builtins/@parse/yaml.js +9 -0
- package/src/builtins/@project.js +71 -0
- package/src/builtins/@repeat.js +8 -0
- package/src/builtins/@rss.js +49 -0
- package/src/builtins/@scope/extend.js +22 -0
- package/src/builtins/@scope/get.js +25 -0
- package/src/builtins/@scope/invoke.js +22 -0
- package/src/builtins/@scope/set.js +25 -0
- package/src/builtins/@serve.js +74 -0
- package/src/builtins/@shell.js +16 -0
- package/src/builtins/@stdin.js +26 -0
- package/src/builtins/@svg.js +42 -0
- package/src/builtins/@tree/concat.js +21 -0
- package/src/builtins/@tree/count.js +24 -0
- package/src/builtins/@tree/defineds.js +37 -0
- package/src/builtins/@tree/dot.js +201 -0
- package/src/builtins/@tree/exceptions.js +50 -0
- package/src/builtins/@tree/first.js +28 -0
- package/src/builtins/@tree/flowSvg.js +55 -0
- package/src/builtins/@tree/fn.js +34 -0
- package/src/builtins/@tree/from.js +27 -0
- package/src/builtins/@tree/fromJson.js +6 -0
- package/src/builtins/@tree/fromYaml.js +24 -0
- package/src/builtins/@tree/groupBy.js +39 -0
- package/src/builtins/@tree/inners.js +44 -0
- package/src/builtins/@tree/isAsyncTree.js +17 -0
- package/src/builtins/@tree/keys.js +24 -0
- package/src/builtins/@tree/keysJson.js +44 -0
- package/src/builtins/@tree/map.d.ts +19 -0
- package/src/builtins/@tree/merge.js +47 -0
- package/src/builtins/@tree/mergeDeep.js +44 -0
- package/src/builtins/@tree/nextKey.js +29 -0
- package/src/builtins/@tree/parent.js +24 -0
- package/src/builtins/@tree/paths.js +35 -0
- package/src/builtins/@tree/plain.js +22 -0
- package/src/builtins/@tree/previousKey.js +29 -0
- package/src/builtins/@tree/reverse.js +51 -0
- package/src/builtins/@tree/setDeep.js +45 -0
- package/src/builtins/@tree/shuffle.js +31 -0
- package/src/builtins/@tree/sitemap.js +59 -0
- package/src/builtins/@tree/sort.js +25 -0
- package/src/builtins/@tree/sortBy.js +40 -0
- package/src/builtins/@tree/static.js +51 -0
- package/src/builtins/@tree/table.js +74 -0
- package/src/builtins/@tree/take.js +40 -0
- package/src/builtins/@tree/values.js +23 -0
- package/src/builtins/@tree/valuesDeep.js +23 -0
- package/src/builtins/@treeHttp.js +19 -0
- package/src/builtins/@treeHttps.js +19 -0
- package/src/builtins/@true.js +1 -0
- package/src/builtins/@unpack.js +13 -0
- package/src/builtins/@watch.js +108 -0
- package/src/builtins/@with.js +22 -0
- package/src/builtins/@yaml.js +23 -0
- package/src/builtins/~.js +9 -0
- package/src/cli/cli.js +86 -0
- package/src/cli/defaultModuleExport.js +16 -0
- package/src/cli/showUsage.js +86 -0
- package/src/common/CommandModulesTransform.d.ts +5 -0
- package/src/common/CommandModulesTransform.js +37 -0
- package/src/common/ConstantTree.js +17 -0
- package/src/common/ExplorableSiteTransform.d.ts +5 -0
- package/src/common/ExplorableSiteTransform.js +77 -0
- package/src/common/FilterTree.js +60 -0
- package/src/common/GlobTree.js +67 -0
- package/src/common/ShuffleTransform.js +29 -0
- package/src/common/TextDocument.js +57 -0
- package/src/common/addValueKeyToScope.js +30 -0
- package/src/common/arrowFunctionsMap.js +35 -0
- package/src/common/processUnpackedContent.js +39 -0
- package/src/common/serialize.d.ts +8 -0
- package/src/common/serialize.js +138 -0
- package/src/common/utilities.d.ts +7 -0
- package/src/common/utilities.js +132 -0
- package/src/misc/OriCommandTransform.d.ts +5 -0
- package/src/misc/OriCommandTransform.js +54 -0
- package/src/misc/assertScopeIsDefined.js +7 -0
- package/src/misc/explore.orit +241 -0
- package/src/misc/yamlOrigamiTag.js +17 -0
- package/src/server/mediaTypes.js +97 -0
- 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";
|