@weborigami/language 0.0.41 → 0.0.43
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 +18 -0
- package/main.js +1 -1
- package/package.json +3 -3
- package/src/compiler/compile.js +20 -14
- package/src/compiler/origami.pegjs +157 -77
- package/src/compiler/parse.d.ts +3 -0
- package/src/compiler/parse.js +1223 -800
- package/src/compiler/parserHelpers.js +10 -0
- package/src/runtime/concatTreeValues.js +6 -0
- package/src/runtime/evaluate.js +24 -20
- package/src/runtime/expressionFunction.js +3 -4
- package/src/runtime/formatError.js +52 -0
- package/src/runtime/internal.js +0 -2
- package/src/runtime/ops.js +1 -1
- package/test/compiler/compile.test.js +1 -1
- package/test/compiler/parse.test.js +124 -31
- package/src/compiler/code.d.ts +0 -3
- package/src/runtime/format.js +0 -127
- package/test/runtime/format.test.js +0 -66
|
@@ -17,6 +17,16 @@ export function makeFunctionCall(target, chain) {
|
|
|
17
17
|
return value;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Similar to a function call, but the order is reversed.
|
|
21
|
+
export function makePipeline(steps) {
|
|
22
|
+
const [first, ...rest] = steps;
|
|
23
|
+
let value = first;
|
|
24
|
+
for (const args of rest) {
|
|
25
|
+
value = [args, value];
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
export function makeTemplate(parts) {
|
|
21
31
|
// Drop empty/null strings.
|
|
22
32
|
const filtered = parts.filter((part) => part);
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
isPlainObject,
|
|
5
5
|
} from "@weborigami/async-tree";
|
|
6
6
|
|
|
7
|
+
const textDecoder = new TextDecoder();
|
|
8
|
+
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Concatenate the text values in a tree.
|
|
9
12
|
*
|
|
@@ -52,6 +55,9 @@ async function getText(value, scope) {
|
|
|
52
55
|
text = "";
|
|
53
56
|
} else if (typeof value === "string") {
|
|
54
57
|
text = value;
|
|
58
|
+
} else if (value instanceof ArrayBuffer || value instanceof TypedArray) {
|
|
59
|
+
// Serialize data as UTF-8.
|
|
60
|
+
text = textDecoder.decode(value);
|
|
55
61
|
} else if (
|
|
56
62
|
!(value instanceof Array) &&
|
|
57
63
|
value.toString !== getRealmObjectPrototype(value).toString
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Tree, isPlainObject } from "@weborigami/async-tree";
|
|
2
|
-
import {
|
|
2
|
+
import { ops } from "./internal.js";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const codeSymbol = Symbol("code");
|
|
5
|
+
const sourceSymbol = Symbol("source");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Evaluate the given code and return the result.
|
|
@@ -9,10 +10,9 @@ const expressionSymbol = Symbol("expression");
|
|
|
9
10
|
* `this` should be the scope used to look up references found in the code.
|
|
10
11
|
*
|
|
11
12
|
* @typedef {import("@weborigami/async-tree").Treelike} Treelike
|
|
12
|
-
* @typedef {import("../../../language/src/compiler/code.js").Code} Code
|
|
13
13
|
*
|
|
14
14
|
* @this {Treelike|null}
|
|
15
|
-
* @param {
|
|
15
|
+
* @param {any} code
|
|
16
16
|
*/
|
|
17
17
|
export default async function evaluate(code) {
|
|
18
18
|
const scope = this;
|
|
@@ -42,9 +42,7 @@ export default async function evaluate(code) {
|
|
|
42
42
|
|
|
43
43
|
if (!fn) {
|
|
44
44
|
// The code wants to invoke something that's couldn't be found in scope.
|
|
45
|
-
throw ReferenceError(
|
|
46
|
-
`Couldn't find function or tree key: ${format(code[0])}`
|
|
47
|
-
);
|
|
45
|
+
throw ReferenceError(`${codeFragment(code[0])} is not defined`);
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
if (
|
|
@@ -57,9 +55,7 @@ export default async function evaluate(code) {
|
|
|
57
55
|
|
|
58
56
|
if (!Tree.isTreelike(fn)) {
|
|
59
57
|
throw TypeError(
|
|
60
|
-
|
|
61
|
-
code[0]
|
|
62
|
-
)}`
|
|
58
|
+
`${codeFragment(code[0])} didn't return a function that can be called`
|
|
63
59
|
);
|
|
64
60
|
}
|
|
65
61
|
|
|
@@ -71,26 +67,34 @@ export default async function evaluate(code) {
|
|
|
71
67
|
? await fn.call(scope, ...args) // Invoke the function
|
|
72
68
|
: await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
|
|
73
69
|
} catch (/** @type {any} */ error) {
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
if (!error.location) {
|
|
71
|
+
// Attach the location of the code we were evaluating.
|
|
72
|
+
error.location = /** @type {any} */ (code).location;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
// To aid debugging, add the
|
|
77
|
+
// To aid debugging, add the code to the result.
|
|
79
78
|
if (
|
|
80
79
|
result &&
|
|
81
80
|
typeof result === "object" &&
|
|
82
81
|
Object.isExtensible(result) &&
|
|
83
82
|
!isPlainObject(result)
|
|
84
83
|
) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Setting a Symbol-keyed property on some objects fails with `TypeError:
|
|
89
|
-
// Cannot convert a Symbol value to a string` but it's unclear why
|
|
90
|
-
// implicit casting of the symbol to a string occurs. Since this is not a
|
|
91
|
-
// vital operation, we ignore such errors.
|
|
84
|
+
result[codeSymbol] = code;
|
|
85
|
+
if (/** @type {any} */ (code).location) {
|
|
86
|
+
result[sourceSymbol] = codeFragment(code);
|
|
92
87
|
}
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
return result;
|
|
96
91
|
}
|
|
92
|
+
|
|
93
|
+
function codeFragment(code) {
|
|
94
|
+
if (code.location) {
|
|
95
|
+
const { source, start, end } = code.location;
|
|
96
|
+
return source.text.slice(start.offset, end.offset);
|
|
97
|
+
} else {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
|
|
2
2
|
|
|
3
|
-
import { evaluate
|
|
3
|
+
import { evaluate } from "./internal.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Given parsed Origami code, return a function that executes that code.
|
|
7
7
|
*
|
|
8
|
-
* @param {
|
|
8
|
+
* @param {import("../../index.js").Code} code - parsed Origami code
|
|
9
9
|
* @param {string} [name] - optional name of the function
|
|
10
10
|
*/
|
|
11
11
|
export function createExpressionFunction(code, name) {
|
|
@@ -17,8 +17,7 @@ export function createExpressionFunction(code, name) {
|
|
|
17
17
|
Object.defineProperty(fn, "name", { value: name });
|
|
18
18
|
}
|
|
19
19
|
fn.code = code;
|
|
20
|
-
fn.
|
|
21
|
-
fn.toString = () => fn.source;
|
|
20
|
+
fn.toString = () => code.source?.text;
|
|
22
21
|
return fn;
|
|
23
22
|
}
|
|
24
23
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Text we look for in an error stack to guess whether a given line represents a
|
|
2
|
+
// function in the Origami source code.
|
|
3
|
+
const origamiSourceSignals = [
|
|
4
|
+
"async-tree/src/",
|
|
5
|
+
"language/src/",
|
|
6
|
+
"origami/src/",
|
|
7
|
+
"at Scope.evaluate",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format an error for display in the console.
|
|
12
|
+
*
|
|
13
|
+
* @param {Error} error
|
|
14
|
+
*/
|
|
15
|
+
export default function formatError(error) {
|
|
16
|
+
let message;
|
|
17
|
+
if (error.stack) {
|
|
18
|
+
// Display the stack only until we reach the Origami source code.
|
|
19
|
+
message = "";
|
|
20
|
+
let lines = error.stack.split("\n");
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const line = lines[i];
|
|
23
|
+
if (origamiSourceSignals.some((signal) => line.includes(signal))) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
if (message) {
|
|
27
|
+
message += "\n";
|
|
28
|
+
}
|
|
29
|
+
message += lines[i];
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
message = error.toString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add location
|
|
36
|
+
let location = /** @type {any} */ (error).location;
|
|
37
|
+
if (location) {
|
|
38
|
+
let { source, start, end } = location;
|
|
39
|
+
let fragment = source.text.slice(start.offset, end.offset);
|
|
40
|
+
if (fragment.length === 0) {
|
|
41
|
+
// Use entire source.
|
|
42
|
+
fragment = source.text;
|
|
43
|
+
}
|
|
44
|
+
message += `\nevaluating: ${fragment}`;
|
|
45
|
+
if (typeof source === "object" && source.url) {
|
|
46
|
+
message += `\n at ${source.url.href}:${start.line}:${start.column}`;
|
|
47
|
+
} else if (source.text.includes("\n")) {
|
|
48
|
+
message += `\n at line ${start.line}, column ${start.column}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return message;
|
|
52
|
+
}
|
package/src/runtime/internal.js
CHANGED
|
@@ -11,8 +11,6 @@ export * as ops from "./ops.js";
|
|
|
11
11
|
|
|
12
12
|
export { default as evaluate } from "./evaluate.js";
|
|
13
13
|
|
|
14
|
-
export { default as format } from "./format.js";
|
|
15
|
-
|
|
16
14
|
export * as expressionFunction from "./expressionFunction.js";
|
|
17
15
|
|
|
18
16
|
export { default as ExpressionTree } from "./ExpressionTree.js";
|
package/src/runtime/ops.js
CHANGED
|
@@ -128,7 +128,7 @@ inherited.toString = () => "«ops.inherited»";
|
|
|
128
128
|
/**
|
|
129
129
|
* Return a function that will invoke the given code.
|
|
130
130
|
*
|
|
131
|
-
* @typedef {import("
|
|
131
|
+
* @typedef {import("../../index.ts").Code} Code
|
|
132
132
|
* @this {AsyncTree|null}
|
|
133
133
|
* @param {string[]} parameters
|
|
134
134
|
* @param {Code} code
|
|
@@ -49,7 +49,7 @@ describe("compile", () => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
test("templateLiteral", async () => {
|
|
52
|
-
await assertCompile("`Hello, {
|
|
52
|
+
await assertCompile("`Hello, ${name}!`", "Hello, Alice!");
|
|
53
53
|
await assertCompile(
|
|
54
54
|
"`escape characters with \\`backslash\\``",
|
|
55
55
|
"escape characters with `backslash`"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlainObject } from "@weborigami/async-tree";
|
|
1
2
|
import assert from "node:assert";
|
|
2
3
|
import { describe, test } from "node:test";
|
|
3
4
|
import { parse } from "../../src/compiler/parse.js";
|
|
@@ -15,6 +16,7 @@ describe("Origami parser", () => {
|
|
|
15
16
|
test("array", () => {
|
|
16
17
|
assertParse("array", "[]", [ops.array]);
|
|
17
18
|
assertParse("array", "[1, 2, 3]", [ops.array, 1, 2, 3]);
|
|
19
|
+
assertParse("array", "[ 1 , 2 , 3 ]", [ops.array, 1, 2, 3]);
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
test("assignment", () => {
|
|
@@ -68,11 +70,15 @@ describe("Origami parser", () => {
|
|
|
68
70
|
],
|
|
69
71
|
[[ops.scope, "files"], "snapshot"],
|
|
70
72
|
]);
|
|
71
|
-
assertParse("expr", "@map =`<li
|
|
73
|
+
assertParse("expr", "@map =`<li>${_}</li>`", [
|
|
72
74
|
[ops.scope, "@map"],
|
|
73
75
|
[ops.lambda, null, [ops.concat, "<li>", [ops.scope, "_"], "</li>"]],
|
|
74
76
|
]);
|
|
75
77
|
assertParse("expr", `"https://example.com"`, "https://example.com");
|
|
78
|
+
assertParse("expr", "'Hello' -> test.orit", [
|
|
79
|
+
[ops.scope, "test.orit"],
|
|
80
|
+
"Hello",
|
|
81
|
+
]);
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
test("expression", () => {
|
|
@@ -103,6 +109,18 @@ describe("Origami parser", () => {
|
|
|
103
109
|
],
|
|
104
110
|
]
|
|
105
111
|
);
|
|
112
|
+
|
|
113
|
+
// Consecutive slahes inside a path = empty string key
|
|
114
|
+
assertParse("expression", "path//key", [
|
|
115
|
+
ops.traverse,
|
|
116
|
+
[ops.scope, "path"],
|
|
117
|
+
"",
|
|
118
|
+
"key",
|
|
119
|
+
]);
|
|
120
|
+
// Single slash at start of something = absolute file path
|
|
121
|
+
assertParse("expression", "/path", [[ops.filesRoot], "path"]);
|
|
122
|
+
// Consecutive slashes at start of something = comment
|
|
123
|
+
assertParse("expression", "path //comment", [ops.scope, "path"]);
|
|
106
124
|
});
|
|
107
125
|
|
|
108
126
|
test("functionComposition", () => {
|
|
@@ -116,6 +134,11 @@ describe("Origami parser", () => {
|
|
|
116
134
|
[ops.scope, "a"],
|
|
117
135
|
[ops.scope, "b"],
|
|
118
136
|
]);
|
|
137
|
+
assertParse("functionComposition", "fn( a , b )", [
|
|
138
|
+
[ops.scope, "fn"],
|
|
139
|
+
[ops.scope, "a"],
|
|
140
|
+
[ops.scope, "b"],
|
|
141
|
+
]);
|
|
119
142
|
assertParse("functionComposition", "fn()(arg)", [
|
|
120
143
|
[[ops.scope, "fn"], undefined],
|
|
121
144
|
[ops.scope, "arg"],
|
|
@@ -167,36 +190,16 @@ describe("Origami parser", () => {
|
|
|
167
190
|
[ops.object, ["a", 1], ["b", 2]],
|
|
168
191
|
"b",
|
|
169
192
|
]);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
test("group", () => {
|
|
173
|
-
assertParse("group", "(hello)", [ops.scope, "hello"]);
|
|
174
|
-
assertParse("group", "(((nested)))", [ops.scope, "nested"]);
|
|
175
|
-
assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("host", () => {
|
|
179
|
-
assertParse("host", "abc", "abc");
|
|
180
|
-
assertParse("host", "abc:123", "abc:123");
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test("identifier", () => {
|
|
184
|
-
assertParse("identifier", "abc", "abc");
|
|
185
|
-
assertParse("identifier", "index.html", "index.html");
|
|
186
|
-
assertParse("identifier", "foo\\ bar", "foo bar");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("implicitParensCall", () => {
|
|
190
|
-
assertParse("implicitParensCall", "fn arg", [
|
|
193
|
+
assertParse("functionComposition", "fn arg", [
|
|
191
194
|
[ops.scope, "fn"],
|
|
192
195
|
[ops.scope, "arg"],
|
|
193
196
|
]);
|
|
194
|
-
assertParse("
|
|
197
|
+
assertParse("functionComposition", "fn 'a', 'b'", [
|
|
195
198
|
[ops.scope, "fn"],
|
|
196
199
|
"a",
|
|
197
200
|
"b",
|
|
198
201
|
]);
|
|
199
|
-
assertParse("
|
|
202
|
+
assertParse("functionComposition", "fn a(b), c", [
|
|
200
203
|
[ops.scope, "fn"],
|
|
201
204
|
[
|
|
202
205
|
[ops.scope, "a"],
|
|
@@ -204,27 +207,49 @@ describe("Origami parser", () => {
|
|
|
204
207
|
],
|
|
205
208
|
[ops.scope, "c"],
|
|
206
209
|
]);
|
|
207
|
-
assertParse("
|
|
210
|
+
assertParse("functionComposition", "fn1 fn2 'arg'", [
|
|
208
211
|
[ops.scope, "fn1"],
|
|
209
212
|
[[ops.scope, "fn2"], "arg"],
|
|
210
213
|
]);
|
|
211
|
-
assertParse("
|
|
214
|
+
assertParse("functionComposition", "(fn()) 'arg'", [
|
|
212
215
|
[[ops.scope, "fn"], undefined],
|
|
213
216
|
"arg",
|
|
214
217
|
]);
|
|
215
|
-
assertParse("
|
|
218
|
+
assertParse("functionComposition", "tree/key arg", [
|
|
219
|
+
[ops.traverse, [ops.scope, "tree"], "key"],
|
|
220
|
+
[ops.scope, "arg"],
|
|
221
|
+
]);
|
|
222
|
+
assertParse("functionComposition", "https://example.com/tree.yaml 'key'", [
|
|
216
223
|
[ops.https, "example.com", "tree.yaml"],
|
|
217
224
|
"key",
|
|
218
225
|
]);
|
|
219
226
|
});
|
|
220
227
|
|
|
228
|
+
test("group", () => {
|
|
229
|
+
assertParse("group", "(hello)", [ops.scope, "hello"]);
|
|
230
|
+
assertParse("group", "(((nested)))", [ops.scope, "nested"]);
|
|
231
|
+
assertParse("group", "(fn())", [[ops.scope, "fn"], undefined]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("host", () => {
|
|
235
|
+
assertParse("host", "abc", "abc");
|
|
236
|
+
assertParse("host", "abc:123", "abc:123");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("identifier", () => {
|
|
240
|
+
assertParse("identifier", "abc", "abc");
|
|
241
|
+
assertParse("identifier", "index.html", "index.html");
|
|
242
|
+
assertParse("identifier", "foo\\ bar", "foo bar");
|
|
243
|
+
assertParse("identifier", "x-y-z", "x-y-z");
|
|
244
|
+
});
|
|
245
|
+
|
|
221
246
|
test("lambda", () => {
|
|
222
247
|
assertParse("lambda", "=message", [
|
|
223
248
|
ops.lambda,
|
|
224
249
|
null,
|
|
225
250
|
[ops.scope, "message"],
|
|
226
251
|
]);
|
|
227
|
-
assertParse("lambda", "=`Hello, {
|
|
252
|
+
assertParse("lambda", "=`Hello, ${name}.`", [
|
|
228
253
|
ops.lambda,
|
|
229
254
|
null,
|
|
230
255
|
[ops.concat, "Hello, ", [ops.scope, "name"], "."],
|
|
@@ -235,7 +260,7 @@ describe("Origami parser", () => {
|
|
|
235
260
|
assertParse("leadingSlashPath", "/tree/", ["tree", ""]);
|
|
236
261
|
});
|
|
237
262
|
|
|
238
|
-
|
|
263
|
+
test("list", () => {
|
|
239
264
|
assertParse("list", "1", [1]);
|
|
240
265
|
assertParse("list", "1,2,3", [1, 2, 3]);
|
|
241
266
|
assertParse("list", "1, 2, 3,", [1, 2, 3]);
|
|
@@ -244,6 +269,10 @@ describe("Origami parser", () => {
|
|
|
244
269
|
assertParse("list", "'a' , 'b' , 'c'", ["a", "b", "c"]);
|
|
245
270
|
});
|
|
246
271
|
|
|
272
|
+
test("multiLineComment", () => {
|
|
273
|
+
assertParse("multiLineComment", "/*\nHello, world!\n*/", null);
|
|
274
|
+
});
|
|
275
|
+
|
|
247
276
|
test("number", () => {
|
|
248
277
|
assertParse("number", "123", 123);
|
|
249
278
|
assertParse("number", "-456", -456);
|
|
@@ -326,6 +355,20 @@ describe("Origami parser", () => {
|
|
|
326
355
|
assertParse("path", "tree/foo/bar", ["tree", "foo", "bar"]);
|
|
327
356
|
});
|
|
328
357
|
|
|
358
|
+
test("pipeline", () => {
|
|
359
|
+
assertParse("pipeline", "a -> b", [
|
|
360
|
+
[ops.scope, "b"],
|
|
361
|
+
[ops.scope, "a"],
|
|
362
|
+
]);
|
|
363
|
+
assertParse("pipeline", "input → one.js → two.js", [
|
|
364
|
+
[ops.scope, "two.js"],
|
|
365
|
+
[
|
|
366
|
+
[ops.scope, "one.js"],
|
|
367
|
+
[ops.scope, "input"],
|
|
368
|
+
],
|
|
369
|
+
]);
|
|
370
|
+
});
|
|
371
|
+
|
|
329
372
|
test("protocolCall", () => {
|
|
330
373
|
assertParse("protocolCall", "foo://bar", [[ops.scope, "foo"], "bar"]);
|
|
331
374
|
assertParse("protocolCall", "https://example.com/foo/", [
|
|
@@ -342,6 +385,14 @@ describe("Origami parser", () => {
|
|
|
342
385
|
]);
|
|
343
386
|
});
|
|
344
387
|
|
|
388
|
+
test("singleLineComment", () => {
|
|
389
|
+
assertParse("singleLineComment", "# Hello, world!", null);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("singleLineComment (JS)", () => {
|
|
393
|
+
assertParse("singleLineComment", "// Hello, world!", null);
|
|
394
|
+
});
|
|
395
|
+
|
|
345
396
|
test("scopeReference", () => {
|
|
346
397
|
assertParse("scopeReference", "x", [ops.scope, "x"]);
|
|
347
398
|
});
|
|
@@ -356,7 +407,7 @@ describe("Origami parser", () => {
|
|
|
356
407
|
});
|
|
357
408
|
|
|
358
409
|
test("templateDocument", () => {
|
|
359
|
-
assertParse("templateDocument", "hello{
|
|
410
|
+
assertParse("templateDocument", "hello${foo}world", [
|
|
360
411
|
ops.lambda,
|
|
361
412
|
null,
|
|
362
413
|
[ops.concat, "hello", [ops.scope, "foo"], "world"],
|
|
@@ -387,10 +438,33 @@ describe("Origami parser", () => {
|
|
|
387
438
|
]);
|
|
388
439
|
});
|
|
389
440
|
|
|
441
|
+
test("templateLiteral (JS)", () => {
|
|
442
|
+
assertParse("templateLiteral", "`Hello, world.`", "Hello, world.");
|
|
443
|
+
assertParse("templateLiteral", "`foo ${x} bar`", [
|
|
444
|
+
ops.concat,
|
|
445
|
+
"foo ",
|
|
446
|
+
[ops.scope, "x"],
|
|
447
|
+
" bar",
|
|
448
|
+
]);
|
|
449
|
+
assertParse("templateLiteral", "`${`nested`}`", "nested");
|
|
450
|
+
assertParse("templateLiteral", "`${map(people, =`${name}`)}`", [
|
|
451
|
+
ops.concat,
|
|
452
|
+
[
|
|
453
|
+
[ops.scope, "map"],
|
|
454
|
+
[ops.scope, "people"],
|
|
455
|
+
[ops.lambda, null, [ops.concat, [ops.scope, "name"]]],
|
|
456
|
+
],
|
|
457
|
+
]);
|
|
458
|
+
});
|
|
459
|
+
|
|
390
460
|
test("templateSubstitution", () => {
|
|
391
461
|
assertParse("templateSubstitution", "{{foo}}", [ops.scope, "foo"]);
|
|
392
462
|
});
|
|
393
463
|
|
|
464
|
+
test("templateSubtitution (JS)", () => {
|
|
465
|
+
assertParse("templateSubstitution", "${foo}", [ops.scope, "foo"]);
|
|
466
|
+
});
|
|
467
|
+
|
|
394
468
|
test("tree", () => {
|
|
395
469
|
assertParse("tree", "{}", [ops.tree]);
|
|
396
470
|
assertParse("tree", "{ a = 1, b }", [
|
|
@@ -417,6 +491,25 @@ describe("Origami parser", () => {
|
|
|
417
491
|
});
|
|
418
492
|
|
|
419
493
|
function assertParse(startRule, source, expected) {
|
|
420
|
-
|
|
494
|
+
/** @type {any} */
|
|
495
|
+
const parseResult = parse(source, { grammarSource: source, startRule });
|
|
496
|
+
const actual = stripLocations(parseResult);
|
|
421
497
|
assert.deepEqual(actual, expected);
|
|
422
498
|
}
|
|
499
|
+
|
|
500
|
+
// For comparison purposes, strip the `location` property added by the parser.
|
|
501
|
+
function stripLocations(parseResult) {
|
|
502
|
+
if (Array.isArray(parseResult)) {
|
|
503
|
+
return parseResult.map(stripLocations);
|
|
504
|
+
} else if (isPlainObject(parseResult)) {
|
|
505
|
+
const result = {};
|
|
506
|
+
for (const key in parseResult) {
|
|
507
|
+
if (key !== "location") {
|
|
508
|
+
result[key] = stripLocations(parseResult[key]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
512
|
+
} else {
|
|
513
|
+
return parseResult;
|
|
514
|
+
}
|
|
515
|
+
}
|
package/src/compiler/code.d.ts
DELETED
package/src/runtime/format.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
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
|
-
// TODO: named parameters
|
|
91
|
-
return `=${format(code[2])}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function formatScopeTraversal(code, implicitFunctionCall = false) {
|
|
95
|
-
const operands = code.slice(1);
|
|
96
|
-
const name = formatName(operands[0]);
|
|
97
|
-
if (operands.length === 1) {
|
|
98
|
-
return implicitFunctionCall ? `${name}()` : name;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const args = formatArguments(operands.slice(1));
|
|
102
|
-
return `${name}${args}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function formatSlashPath(args) {
|
|
106
|
-
return "/" + args.join("/");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function formatTemplate(code) {
|
|
110
|
-
const args = code.slice(1);
|
|
111
|
-
const formatted = args.map((arg) =>
|
|
112
|
-
typeof arg === "string" ? arg : `{{${format(arg)}}}`
|
|
113
|
-
);
|
|
114
|
-
return `\`${formatted.join("")}\``;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function formatTree(code) {
|
|
118
|
-
const [_, ...entries] = code;
|
|
119
|
-
const formatted = entries.map(([key, value]) => {
|
|
120
|
-
const rhs =
|
|
121
|
-
typeof value === "function" && value.code !== undefined
|
|
122
|
-
? value.code
|
|
123
|
-
: value;
|
|
124
|
-
return `${key} = ${format(rhs)}`;
|
|
125
|
-
});
|
|
126
|
-
return formatted ? `{ ${formatted.join(", ")} }` : "{}";
|
|
127
|
-
}
|