@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.
- package/index.ts +30 -0
- package/main.js +21 -0
- package/package.json +24 -0
- package/src/compiler/code.d.ts +3 -0
- package/src/compiler/compile.js +55 -0
- package/src/compiler/origami.pegjs +277 -0
- package/src/compiler/parse.js +2292 -0
- package/src/compiler/parserHelpers.js +26 -0
- package/src/runtime/EventTargetMixin.d.ts +9 -0
- package/src/runtime/EventTargetMixin.js +117 -0
- package/src/runtime/ExpressionTree.js +20 -0
- package/src/runtime/FileLoadersTransform.d.ts +5 -0
- package/src/runtime/FileLoadersTransform.js +43 -0
- package/src/runtime/ImportModulesMixin.d.ts +5 -0
- package/src/runtime/ImportModulesMixin.js +48 -0
- package/src/runtime/InheritScopeMixin.js +34 -0
- package/src/runtime/InheritScopeMixin.ts +9 -0
- package/src/runtime/InvokeFunctionsTransform.d.ts +5 -0
- package/src/runtime/InvokeFunctionsTransform.js +27 -0
- package/src/runtime/OrigamiFiles.d.ts +11 -0
- package/src/runtime/OrigamiFiles.js +9 -0
- package/src/runtime/OrigamiTransform.d.ts +11 -0
- package/src/runtime/OrigamiTransform.js +11 -0
- package/src/runtime/OrigamiTree.js +4 -0
- package/src/runtime/ReadMe.md +1 -0
- package/src/runtime/Scope.js +89 -0
- package/src/runtime/TreeEvent.js +6 -0
- package/src/runtime/WatchFilesMixin.d.ts +5 -0
- package/src/runtime/WatchFilesMixin.js +58 -0
- package/src/runtime/concatTreeValues.js +46 -0
- package/src/runtime/evaluate.js +90 -0
- package/src/runtime/expressionFunction.js +33 -0
- package/src/runtime/extname.js +20 -0
- package/src/runtime/format.js +126 -0
- package/src/runtime/functionResultsMap.js +28 -0
- package/src/runtime/internal.js +20 -0
- package/src/runtime/ops.js +222 -0
- package/test/compiler/compile.test.js +64 -0
- package/test/compiler/parse.test.js +389 -0
- package/test/runtime/EventTargetMixin.test.js +68 -0
- package/test/runtime/ExpressionTree.test.js +27 -0
- package/test/runtime/FileLoadersTransform.test.js +41 -0
- package/test/runtime/InheritScopeMixin.test.js +29 -0
- package/test/runtime/OrigamiFiles.test.js +37 -0
- package/test/runtime/Scope.test.js +37 -0
- package/test/runtime/concatTreeValues.js +20 -0
- package/test/runtime/evaluate.test.js +55 -0
- package/test/runtime/fixtures/foo.js +1 -0
- package/test/runtime/fixtures/makeTest/a +1 -0
- package/test/runtime/fixtures/makeTest/b = a +0 -0
- package/test/runtime/fixtures/metagraphs/foo.txt +1 -0
- package/test/runtime/fixtures/metagraphs/greeting = this('world').js +3 -0
- package/test/runtime/fixtures/metagraphs/obj = this.json +5 -0
- package/test/runtime/fixtures/metagraphs/sample.txt = this().js +3 -0
- package/test/runtime/fixtures/metagraphs/string = this.json +1 -0
- package/test/runtime/fixtures/metagraphs/value = fn() +0 -0
- package/test/runtime/fixtures/programs/context.yaml +4 -0
- package/test/runtime/fixtures/programs/files.yaml +2 -0
- package/test/runtime/fixtures/programs/obj.yaml +3 -0
- package/test/runtime/fixtures/programs/simple.yaml +2 -0
- package/test/runtime/fixtures/subgraph = this.js +5 -0
- package/test/runtime/fixtures/templates/greet.orit +4 -0
- package/test/runtime/fixtures/templates/index.orit +15 -0
- package/test/runtime/fixtures/templates/names.yaml +3 -0
- package/test/runtime/fixtures/templates/plain.txt +1 -0
- package/test/runtime/fixtures/virtualKeys/.keys.json +1 -0
- package/test/runtime/format.test.js +66 -0
- package/test/runtime/functionResultsMap.test.js +27 -0
- package/test/runtime/ops.test.js +111 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Tree, isPlainObject } from "@weborigami/async-tree";
|
|
2
|
+
import { format, ops } from "./internal.js";
|
|
3
|
+
|
|
4
|
+
const expressionSymbol = Symbol("expression");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate the given code and return the result.
|
|
8
|
+
*
|
|
9
|
+
* `this` should be the scope used to look up references found in the code.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
12
|
+
* @typedef {import("../../../language/src/compiler/code.js").Code} Code
|
|
13
|
+
*
|
|
14
|
+
* @this {Treelike|null}
|
|
15
|
+
* @param {Code} code
|
|
16
|
+
*/
|
|
17
|
+
export default async function evaluate(code) {
|
|
18
|
+
const scope = this;
|
|
19
|
+
|
|
20
|
+
if (code === ops.scope) {
|
|
21
|
+
// ops.scope is a placeholder for the context's scope.
|
|
22
|
+
return scope;
|
|
23
|
+
} else if (!(code instanceof Array)) {
|
|
24
|
+
// Simple scalar; return as is.
|
|
25
|
+
return code;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let evaluated;
|
|
29
|
+
const unevaluatedFns = [ops.lambda, ops.object, ops.tree];
|
|
30
|
+
if (unevaluatedFns.includes(code[0])) {
|
|
31
|
+
// Don't evaluate instructions, use as is.
|
|
32
|
+
evaluated = code;
|
|
33
|
+
} else {
|
|
34
|
+
// Evaluate each instruction in the code.
|
|
35
|
+
evaluated = await Promise.all(
|
|
36
|
+
code.map((instruction) => evaluate.call(scope, instruction))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The head of the array is a tree or function, the rest are args or keys.
|
|
41
|
+
let [fn, ...args] = evaluated;
|
|
42
|
+
|
|
43
|
+
if (!fn) {
|
|
44
|
+
// The code wants to invoke something that's not in scope.
|
|
45
|
+
throw ReferenceError(
|
|
46
|
+
`Couldn't find function or tree key: ${format(code[0])}`
|
|
47
|
+
);
|
|
48
|
+
} else if (!(fn instanceof Object)) {
|
|
49
|
+
throw TypeError(`Can't invoke primitive value: ${format(code[0])}`);
|
|
50
|
+
} else if (!(fn instanceof Function) && typeof fn.unpack === "function") {
|
|
51
|
+
// The object has a unpack function; see if it returns a function.
|
|
52
|
+
const unpacked = await fn.unpack();
|
|
53
|
+
if (unpacked instanceof Function) {
|
|
54
|
+
fn = unpacked;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Execute the function or traverse the tree.
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result =
|
|
62
|
+
fn instanceof Function
|
|
63
|
+
? // Invoke the function
|
|
64
|
+
await fn.call(scope, ...args)
|
|
65
|
+
: // Traverse the tree.
|
|
66
|
+
await Tree.traverseOrThrow(fn, ...args);
|
|
67
|
+
} catch (/** @type {any} */ error) {
|
|
68
|
+
const message = `Error triggered by Origami expression: ${format(code)}`;
|
|
69
|
+
throw new Error(message, { cause: error });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// To aid debugging, add the expression source to the result.
|
|
73
|
+
if (
|
|
74
|
+
result &&
|
|
75
|
+
typeof result === "object" &&
|
|
76
|
+
Object.isExtensible(result) &&
|
|
77
|
+
!isPlainObject(result)
|
|
78
|
+
) {
|
|
79
|
+
try {
|
|
80
|
+
result[expressionSymbol] = format(code);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Setting a Symbol-keyed property on some objects fails with `TypeError:
|
|
83
|
+
// Cannot convert a Symbol value to a string` but it's unclear why
|
|
84
|
+
// implicit casting of the symbol to a string occurs. Since this is not a
|
|
85
|
+
// vital operation, we ignore such errors.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
|
|
2
|
+
|
|
3
|
+
import { evaluate, format } from "./internal.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Given parsed Origami code, return a function that executes that code.
|
|
7
|
+
*
|
|
8
|
+
* @param {string|number|Array} code - parsed Origami code
|
|
9
|
+
* @param {string} [name] - optional name of the function
|
|
10
|
+
*/
|
|
11
|
+
export function createExpressionFunction(code, name) {
|
|
12
|
+
/** @this {AsyncTree|null} */
|
|
13
|
+
async function fn() {
|
|
14
|
+
return evaluate.call(this, code);
|
|
15
|
+
}
|
|
16
|
+
if (name) {
|
|
17
|
+
Object.defineProperty(fn, "name", { value: name });
|
|
18
|
+
}
|
|
19
|
+
fn.code = code;
|
|
20
|
+
fn.source = format(code);
|
|
21
|
+
fn.toString = () => fn.source;
|
|
22
|
+
return fn;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return true if the given object is a function that executes an Origami
|
|
27
|
+
* expression.
|
|
28
|
+
*
|
|
29
|
+
* @param {any} obj
|
|
30
|
+
*/
|
|
31
|
+
export function isExpressionFunction(obj) {
|
|
32
|
+
return typeof obj === "function" && obj.code;
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* If the given path ends in an extension, return it. Otherwise, return the
|
|
3
|
+
* empty string.
|
|
4
|
+
*
|
|
5
|
+
* This is meant as a basic replacement for the standard Node `path.extname`.
|
|
6
|
+
* That standard function inaccurately returns an extension for a path that
|
|
7
|
+
* includes a near-final extension but ends in a final slash, like "foo.txt/".
|
|
8
|
+
* Node thinks that path has a ".txt" extension, but for our purposes it
|
|
9
|
+
* doesn't.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} path
|
|
12
|
+
*/
|
|
13
|
+
export default function extname(path) {
|
|
14
|
+
// We want at least one character before the dot, then a dot, then a non-empty
|
|
15
|
+
// sequence of characters after the dot that aren't slahes or dots.
|
|
16
|
+
const extnameRegex = /[^/](?<ext>\.[^/\.]+)$/;
|
|
17
|
+
const match = path.match(extnameRegex);
|
|
18
|
+
const extension = match?.groups?.ext.toLowerCase() ?? "";
|
|
19
|
+
return extension;
|
|
20
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ops } from "./internal.js";
|
|
2
|
+
|
|
3
|
+
export default function format(code, implicitFunctionCall = false) {
|
|
4
|
+
if (code === null) {
|
|
5
|
+
return "";
|
|
6
|
+
} else if (typeof code === "string") {
|
|
7
|
+
return `'${code}'`;
|
|
8
|
+
} else if (typeof code === "symbol") {
|
|
9
|
+
return `«${code.description}»`;
|
|
10
|
+
} else if (!(code instanceof Array)) {
|
|
11
|
+
return code;
|
|
12
|
+
} else {
|
|
13
|
+
switch (code[0]) {
|
|
14
|
+
case ops.assign:
|
|
15
|
+
return formatAssignment(code);
|
|
16
|
+
|
|
17
|
+
case ops.concat:
|
|
18
|
+
return formatTemplate(code);
|
|
19
|
+
|
|
20
|
+
case ops.lambda:
|
|
21
|
+
return formatLambda(code);
|
|
22
|
+
|
|
23
|
+
case ops.object:
|
|
24
|
+
return formatObject(code);
|
|
25
|
+
|
|
26
|
+
case ops.scope:
|
|
27
|
+
return formatScopeTraversal(code, implicitFunctionCall);
|
|
28
|
+
|
|
29
|
+
case ops.tree:
|
|
30
|
+
return formatTree(code);
|
|
31
|
+
|
|
32
|
+
default:
|
|
33
|
+
return code[0] instanceof Array
|
|
34
|
+
? formatFunctionCall(code)
|
|
35
|
+
: "** Unknown Origami code **";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatArgument(arg) {
|
|
41
|
+
return typeof arg === "string" ? `'${arg}'` : format(arg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatArguments(args) {
|
|
45
|
+
const allStrings = args.every((arg) => typeof arg === "string");
|
|
46
|
+
return allStrings
|
|
47
|
+
? // Use tree traversal syntax.
|
|
48
|
+
formatSlashPath(args)
|
|
49
|
+
: // Use function invocation syntax.
|
|
50
|
+
formatArgumentsList(args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatArgumentsList(args) {
|
|
54
|
+
const formatted = args.map((arg) => formatArgument(arg));
|
|
55
|
+
const list = formatted.join(", ");
|
|
56
|
+
return `(${list})`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatAssignment(code) {
|
|
60
|
+
const [_, declaration, expression] = code;
|
|
61
|
+
return `${declaration} = ${format(expression)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatFunctionCall(code) {
|
|
65
|
+
const [fn, ...args] = code;
|
|
66
|
+
let formattedFn = format(fn);
|
|
67
|
+
if (formattedFn.includes("/") || formattedFn.includes("(")) {
|
|
68
|
+
formattedFn = `(${formattedFn})`;
|
|
69
|
+
}
|
|
70
|
+
return `${formattedFn}${formatArguments(args)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatObject(code) {
|
|
74
|
+
const [_, ...entries] = code;
|
|
75
|
+
const formatted = entries.map(([key, value]) => {
|
|
76
|
+
return value === null ? key : `${key}: ${format(value)}`;
|
|
77
|
+
});
|
|
78
|
+
return formatted ? `{ ${formatted.join(", ")} }` : "{}";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatName(name) {
|
|
82
|
+
return typeof name === "string"
|
|
83
|
+
? name
|
|
84
|
+
: name instanceof Array
|
|
85
|
+
? `(${format(name)})`
|
|
86
|
+
: format(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatLambda(code) {
|
|
90
|
+
return `=${format(code[1])}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatScopeTraversal(code, implicitFunctionCall = false) {
|
|
94
|
+
const operands = code.slice(1);
|
|
95
|
+
const name = formatName(operands[0]);
|
|
96
|
+
if (operands.length === 1) {
|
|
97
|
+
return implicitFunctionCall ? `${name}()` : name;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const args = formatArguments(operands.slice(1));
|
|
101
|
+
return `${name}${args}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatSlashPath(args) {
|
|
105
|
+
return "/" + args.join("/");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatTemplate(code) {
|
|
109
|
+
const args = code.slice(1);
|
|
110
|
+
const formatted = args.map((arg) =>
|
|
111
|
+
typeof arg === "string" ? arg : `{{${format(arg)}}}`
|
|
112
|
+
);
|
|
113
|
+
return `\`${formatted.join("")}\``;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatTree(code) {
|
|
117
|
+
const [_, ...entries] = code;
|
|
118
|
+
const formatted = entries.map(([key, value]) => {
|
|
119
|
+
const rhs =
|
|
120
|
+
typeof value === "function" && value.code !== undefined
|
|
121
|
+
? value.code
|
|
122
|
+
: value;
|
|
123
|
+
return `${key} = ${format(rhs)}`;
|
|
124
|
+
});
|
|
125
|
+
return formatted ? `{ ${formatted.join(", ")} }` : "{}";
|
|
126
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { map, 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
|
+
* @type {import("@weborigami/async-tree").TreeTransform}
|
|
9
|
+
*/
|
|
10
|
+
export default function functionResultsMap(tree) {
|
|
11
|
+
return map({
|
|
12
|
+
description: "functionResultsMap",
|
|
13
|
+
|
|
14
|
+
valueMap: async (sourceValue, sourceKey, tree) => {
|
|
15
|
+
let resultValue;
|
|
16
|
+
if (typeof sourceValue === "function") {
|
|
17
|
+
const scope = Scope.getScope(tree);
|
|
18
|
+
resultValue = await sourceValue.call(scope);
|
|
19
|
+
if (Tree.isAsyncTree(resultValue)) {
|
|
20
|
+
resultValue.parent = tree;
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
resultValue = sourceValue;
|
|
24
|
+
}
|
|
25
|
+
return resultValue;
|
|
26
|
+
},
|
|
27
|
+
})(tree);
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//
|
|
2
|
+
// The runtime includes a number of modules with circular dependencies. This
|
|
3
|
+
// module exists to explicitly set the loading order for those modules. To
|
|
4
|
+
// enforce use of this loading order, other modules should only load the modules
|
|
5
|
+
// below via this module.
|
|
6
|
+
//
|
|
7
|
+
// About this pattern: https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
export * as ops from "./ops.js";
|
|
11
|
+
|
|
12
|
+
export { default as evaluate } from "./evaluate.js";
|
|
13
|
+
|
|
14
|
+
export { default as format } from "./format.js";
|
|
15
|
+
|
|
16
|
+
export * as expressionFunction from "./expressionFunction.js";
|
|
17
|
+
|
|
18
|
+
export { default as ExpressionTree } from "./ExpressionTree.js";
|
|
19
|
+
|
|
20
|
+
export { default as OrigamiTree } from "./OrigamiTree.js";
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
3
|
+
* @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SiteTree } from "@weborigami/async-tree";
|
|
7
|
+
import FileLoadersTransform from "./FileLoadersTransform.js";
|
|
8
|
+
import OrigamiFiles from "./OrigamiFiles.js";
|
|
9
|
+
import Scope from "./Scope.js";
|
|
10
|
+
import concatTreeValues from "./concatTreeValues.js";
|
|
11
|
+
import { OrigamiTree, evaluate, expressionFunction } from "./internal.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Construct an array.
|
|
15
|
+
*
|
|
16
|
+
* @this {AsyncTree|null}
|
|
17
|
+
* @param {any[]} items
|
|
18
|
+
*/
|
|
19
|
+
export async function array(...items) {
|
|
20
|
+
return Array(...items);
|
|
21
|
+
}
|
|
22
|
+
array.toString = () => "«ops.array»";
|
|
23
|
+
|
|
24
|
+
// The assign op is a placeholder for an assignment declaration.
|
|
25
|
+
// It is only used during parsing -- it shouldn't be executed.
|
|
26
|
+
export const assign = "«ops.assign»";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Concatenate the given arguments.
|
|
30
|
+
*
|
|
31
|
+
* @this {AsyncTree|null}
|
|
32
|
+
* @param {any[]} args
|
|
33
|
+
*/
|
|
34
|
+
export async function concat(...args) {
|
|
35
|
+
return concatTreeValues.call(this, args);
|
|
36
|
+
}
|
|
37
|
+
concat.toString = () => "«ops.concat»";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Given a protocol, a host, and a list of keys, construct an href.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} protocol
|
|
43
|
+
* @param {string} host
|
|
44
|
+
* @param {...string|Symbol} keys
|
|
45
|
+
*/
|
|
46
|
+
function constructHref(protocol, host, ...keys) {
|
|
47
|
+
let href = [host, ...keys].join("/");
|
|
48
|
+
if (!href.startsWith(protocol)) {
|
|
49
|
+
if (!href.startsWith("//")) {
|
|
50
|
+
href = `//${href}`;
|
|
51
|
+
}
|
|
52
|
+
href = `${protocol}${href}`;
|
|
53
|
+
}
|
|
54
|
+
return href;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fetch the resource at the given href.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} href
|
|
61
|
+
*/
|
|
62
|
+
async function fetchResponse(href) {
|
|
63
|
+
const response = await fetch(href);
|
|
64
|
+
return response.ok ? await response.arrayBuffer() : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Construct a files tree for the filesystem root.
|
|
69
|
+
*
|
|
70
|
+
* @this {AsyncTree|null}
|
|
71
|
+
*/
|
|
72
|
+
export async function filesRoot() {
|
|
73
|
+
/** @type {AsyncTree} */
|
|
74
|
+
let root = new OrigamiFiles("/");
|
|
75
|
+
|
|
76
|
+
// The root itself needs a scope so that expressions evaluated within it
|
|
77
|
+
// (e.g., Origami expressions loaded from .ori files) will have access to
|
|
78
|
+
// things like the built-in functions.
|
|
79
|
+
root = Scope.treeWithScope(root, this);
|
|
80
|
+
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieve a web resource via HTTP.
|
|
86
|
+
*
|
|
87
|
+
* @this {AsyncTree|null}
|
|
88
|
+
* @param {string} host
|
|
89
|
+
* @param {...string|Symbol} keys
|
|
90
|
+
*/
|
|
91
|
+
export async function http(host, ...keys) {
|
|
92
|
+
const href = constructHref("http:", host, ...keys);
|
|
93
|
+
return fetchResponse(href);
|
|
94
|
+
}
|
|
95
|
+
http.toString = () => "«ops.http»";
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Retrieve a web resource via HTTPS.
|
|
99
|
+
*
|
|
100
|
+
* @this {AsyncTree|null}
|
|
101
|
+
* @param {string} host
|
|
102
|
+
* @param {...string|Symbol} keys
|
|
103
|
+
*/
|
|
104
|
+
export function https(host, ...keys) {
|
|
105
|
+
const href = constructHref("https:", host, ...keys);
|
|
106
|
+
return fetchResponse(href);
|
|
107
|
+
}
|
|
108
|
+
https.toString = () => "«ops.https»";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Search the inherited scope -- i.e., exclude the current tree -- for the
|
|
112
|
+
* given key.
|
|
113
|
+
*
|
|
114
|
+
* @this {AsyncTree|null}
|
|
115
|
+
* @param {*} key
|
|
116
|
+
*/
|
|
117
|
+
export async function inherited(key) {
|
|
118
|
+
const scope = this;
|
|
119
|
+
const scopeTrees = /** @type {any} */ (scope).trees ?? scope;
|
|
120
|
+
const inheritedScope = new Scope(...scopeTrees.slice(1));
|
|
121
|
+
return inheritedScope.get(key);
|
|
122
|
+
}
|
|
123
|
+
inherited.toString = () => "«ops.inherited»";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Return a function that will invoke the given code.
|
|
127
|
+
*
|
|
128
|
+
* @typedef {import("../../../language/src/compiler/code.js").Code} Code
|
|
129
|
+
* @this {AsyncTree|null}
|
|
130
|
+
* @param {Code} code
|
|
131
|
+
*/
|
|
132
|
+
export function lambda(code) {
|
|
133
|
+
/** @this {AsyncTree|null} */
|
|
134
|
+
async function invoke(input) {
|
|
135
|
+
// Add ambients to scope.
|
|
136
|
+
const ambients = {
|
|
137
|
+
_: input,
|
|
138
|
+
"@recurse": invoke,
|
|
139
|
+
};
|
|
140
|
+
const scope = new Scope(ambients, this);
|
|
141
|
+
const result = await evaluate.call(scope, code);
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
invoke.code = code;
|
|
145
|
+
return invoke;
|
|
146
|
+
}
|
|
147
|
+
lambda.toString = () => "«ops.lambda»";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Construct an object. The keys will be the same as the given `obj`
|
|
151
|
+
* parameter's, and the values will be the results of evaluating the
|
|
152
|
+
* corresponding code values in `obj`.
|
|
153
|
+
*
|
|
154
|
+
* @this {AsyncTree|null}
|
|
155
|
+
* @param {any[]} entries
|
|
156
|
+
*/
|
|
157
|
+
export async function object(...entries) {
|
|
158
|
+
const scope = this;
|
|
159
|
+
const promises = entries.map(async ([key, value]) => [
|
|
160
|
+
key,
|
|
161
|
+
await evaluate.call(scope, value),
|
|
162
|
+
]);
|
|
163
|
+
const evaluated = await Promise.all(promises);
|
|
164
|
+
return Object.fromEntries(evaluated);
|
|
165
|
+
}
|
|
166
|
+
object.toString = () => "«ops.object»";
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Construct an tree. This is similar to ops.object but the values are turned
|
|
170
|
+
* into functions rather than being immediately evaluated, and the result is an
|
|
171
|
+
* OrigamiTree.
|
|
172
|
+
*
|
|
173
|
+
* @this {AsyncTree|null}
|
|
174
|
+
* @param {any[]} entries
|
|
175
|
+
*/
|
|
176
|
+
export async function tree(...entries) {
|
|
177
|
+
const fns = entries.map(([key, code]) => {
|
|
178
|
+
const value =
|
|
179
|
+
code instanceof Array
|
|
180
|
+
? expressionFunction.createExpressionFunction(code)
|
|
181
|
+
: code;
|
|
182
|
+
return [key, value];
|
|
183
|
+
});
|
|
184
|
+
const object = Object.fromEntries(fns);
|
|
185
|
+
return new OrigamiTree(object);
|
|
186
|
+
}
|
|
187
|
+
tree.toString = () => "«ops.tree»";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A website tree via HTTP.
|
|
191
|
+
*
|
|
192
|
+
* @this {AsyncTree|null}
|
|
193
|
+
* @param {string} host
|
|
194
|
+
* @param {...string|Symbol} keys
|
|
195
|
+
*/
|
|
196
|
+
export function treeHttp(host, ...keys) {
|
|
197
|
+
const href = constructHref("http:", host, ...keys);
|
|
198
|
+
/** @type {AsyncTree} */
|
|
199
|
+
let result = new (FileLoadersTransform(SiteTree))(href);
|
|
200
|
+
result = Scope.treeWithScope(result, this);
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
treeHttp.toString = () => "«ops.treeHttp»";
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A website tree via HTTPS.
|
|
207
|
+
*
|
|
208
|
+
* @this {AsyncTree|null}
|
|
209
|
+
* @param {string} host
|
|
210
|
+
* @param {...string|Symbol} keys
|
|
211
|
+
*/
|
|
212
|
+
export function treeHttps(host, ...keys) {
|
|
213
|
+
const href = constructHref("https:", host, ...keys);
|
|
214
|
+
/** @type {AsyncTree} */
|
|
215
|
+
let result = new (FileLoadersTransform(SiteTree))(href);
|
|
216
|
+
result = Scope.treeWithScope(result, this);
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
treeHttps.toString = () => "«ops.treeHttps»";
|
|
220
|
+
|
|
221
|
+
// The scope op is a placeholder for the tree's scope.
|
|
222
|
+
export const scope = "«ops.scope»";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ObjectTree, Tree } from "@weborigami/async-tree";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import * as compile from "../../src/compiler/compile.js";
|
|
5
|
+
|
|
6
|
+
const scope = new ObjectTree({
|
|
7
|
+
greet: (name) => `Hello, ${name}!`,
|
|
8
|
+
name: "Alice",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe("compile", () => {
|
|
12
|
+
test("array", async () => {
|
|
13
|
+
await assertCompile("[]", []);
|
|
14
|
+
await assertCompile("[ 1, 2, 3, ]", [1, 2, 3]);
|
|
15
|
+
await assertCompile("[\n'a'\n'b'\n'c'\n]", ["a", "b", "c"]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("functionComposition", async () => {
|
|
19
|
+
await assertCompile("greet()", "Hello, undefined!");
|
|
20
|
+
await assertCompile("greet(name)", "Hello, Alice!");
|
|
21
|
+
await assertCompile("greet 'world'", "Hello, world!");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("tree", async () => {
|
|
25
|
+
const fn = compile.expression("{ message = greet(name) }");
|
|
26
|
+
const tree = await fn.call(null);
|
|
27
|
+
tree.scope = scope;
|
|
28
|
+
assert.deepEqual(await Tree.plain(tree), {
|
|
29
|
+
message: "Hello, Alice!",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("number", async () => {
|
|
34
|
+
await assertCompile("1", 1);
|
|
35
|
+
await assertCompile("3.14159", 3.14159);
|
|
36
|
+
await assertCompile("-1", -1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("object", async () => {
|
|
40
|
+
await assertCompile("{a:1, b:2}", { a: 1, b: 2 });
|
|
41
|
+
await assertCompile("{ a: { b: { c: 0 } } }", { a: { b: { c: 0 } } });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("templateDocument", async () => {
|
|
45
|
+
const fn = compile.templateDocument("Documents can contain ` backticks");
|
|
46
|
+
const templateFn = await fn.call(scope);
|
|
47
|
+
const value = await templateFn.call(null);
|
|
48
|
+
assert.deepEqual(value, "Documents can contain ` backticks");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("templateLiteral", async () => {
|
|
52
|
+
await assertCompile("`Hello, {{name}}!`", "Hello, Alice!");
|
|
53
|
+
await assertCompile(
|
|
54
|
+
"`escape characters with \\`backslash\\``",
|
|
55
|
+
"escape characters with `backslash`"
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function assertCompile(text, expected) {
|
|
61
|
+
const fn = compile.expression(text);
|
|
62
|
+
const result = await fn.call(scope);
|
|
63
|
+
assert.deepEqual(result, expected);
|
|
64
|
+
}
|