@weborigami/language 0.2.9 → 0.2.11
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/main.js +1 -0
- package/package.json +7 -7
- package/src/compiler/isOrigamiFrontMatter.js +26 -0
- package/src/compiler/optimize.js +9 -7
- package/src/compiler/origami.pegjs +146 -56
- package/src/compiler/parse.js +1224 -772
- package/src/compiler/parserHelpers.js +85 -1
- package/src/runtime/EventTargetMixin.d.ts +1 -5
- package/src/runtime/errors.js +19 -2
- package/src/runtime/evaluate.js +1 -9
- package/src/runtime/ops.js +31 -1
- package/test/compiler/codeHelpers.js +1 -1
- package/test/compiler/optimize.test.js +1 -1
- package/test/compiler/parse.test.js +152 -30
- package/test/runtime/ops.test.js +27 -1
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { trailingSlash } from "@weborigami/async-tree";
|
|
2
|
+
import * as YAMLModule from "yaml";
|
|
2
3
|
import codeFragment from "../runtime/codeFragment.js";
|
|
3
4
|
import * as ops from "../runtime/ops.js";
|
|
4
5
|
|
|
6
|
+
// The "yaml" package doesn't seem to provide a default export that the browser can
|
|
7
|
+
// recognize, so we have to handle two ways to accommodate Node and the browser.
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
const YAML = YAMLModule.default ?? YAMLModule.YAML;
|
|
10
|
+
|
|
5
11
|
// Parser helpers
|
|
6
12
|
|
|
7
13
|
/** @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode */
|
|
@@ -30,6 +36,28 @@ export function annotate(code, location) {
|
|
|
30
36
|
return annotated;
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
/**
|
|
40
|
+
* In the given code, replace all scope refernces to the given name with the
|
|
41
|
+
* given macro code.
|
|
42
|
+
*
|
|
43
|
+
* @param {AnnotatedCode} code
|
|
44
|
+
* @param {string} name
|
|
45
|
+
* @param {AnnotatedCode} macro
|
|
46
|
+
*/
|
|
47
|
+
export function applyMacro(code, name, macro) {
|
|
48
|
+
if (!(code instanceof Array)) {
|
|
49
|
+
return code;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [fn, ...args] = code;
|
|
53
|
+
if (fn === ops.scope && args[0] === name) {
|
|
54
|
+
return macro;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const applied = code.map((child) => applyMacro(child, name, macro));
|
|
58
|
+
return annotate(applied, code.location);
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
/**
|
|
34
62
|
* The indicated code is being used to define a property named by the given key.
|
|
35
63
|
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
@@ -50,7 +78,10 @@ function avoidRecursivePropertyCalls(code, key) {
|
|
|
50
78
|
) {
|
|
51
79
|
// Rewrite to avoid recursion
|
|
52
80
|
modified = [ops.inherited, code[1]];
|
|
53
|
-
} else if (
|
|
81
|
+
} else if (
|
|
82
|
+
code[0] === ops.lambda &&
|
|
83
|
+
code[1].some((param) => param[1] === key)
|
|
84
|
+
) {
|
|
54
85
|
// Lambda that defines the key; don't rewrite
|
|
55
86
|
return code;
|
|
56
87
|
} else {
|
|
@@ -382,6 +413,59 @@ export function makeUnaryOperation(operator, value, location) {
|
|
|
382
413
|
return annotate([operators[operator], value], location);
|
|
383
414
|
}
|
|
384
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Make an object from YAML front matter
|
|
418
|
+
*
|
|
419
|
+
* @param {string} text
|
|
420
|
+
* @param {CodeLocation} location
|
|
421
|
+
*/
|
|
422
|
+
export function makeYamlObject(text, location) {
|
|
423
|
+
// Account for the "---" delimiter at the beginning of the YAML front matter
|
|
424
|
+
const yamlLineDelta = 1;
|
|
425
|
+
const yamlOffsetDelta = 4; // 3 dashes + 1 newline
|
|
426
|
+
|
|
427
|
+
let parsed;
|
|
428
|
+
try {
|
|
429
|
+
parsed = YAML.parse(text);
|
|
430
|
+
} catch (/** @type {any} */ yamlError) {
|
|
431
|
+
// Convert YAML error to a SyntaxError
|
|
432
|
+
|
|
433
|
+
let { message } = yamlError;
|
|
434
|
+
// Remove the line number and column if present
|
|
435
|
+
const lineNumberRegex = /( at line )(\d+)(,)/;
|
|
436
|
+
const lineNumberMatch = message.match(lineNumberRegex);
|
|
437
|
+
if (lineNumberMatch) {
|
|
438
|
+
message = message.slice(0, lineNumberMatch.index);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** @type {any} */
|
|
442
|
+
const error = new SyntaxError(message);
|
|
443
|
+
error.location = {
|
|
444
|
+
end: {
|
|
445
|
+
column: yamlError.linePos[1].col,
|
|
446
|
+
line: yamlError.linePos[1].line + yamlLineDelta,
|
|
447
|
+
offset: yamlError.pos[1] + yamlOffsetDelta,
|
|
448
|
+
},
|
|
449
|
+
source: location.source,
|
|
450
|
+
start: {
|
|
451
|
+
column: yamlError.linePos[0].col,
|
|
452
|
+
line: yamlError.linePos[0].line + yamlLineDelta,
|
|
453
|
+
offset: yamlError.pos[0] + yamlOffsetDelta,
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!(parsed instanceof Object)) {
|
|
460
|
+
/** @type {any} */
|
|
461
|
+
const error = new SyntaxError("YAML front matter must be an object.");
|
|
462
|
+
error.location = location;
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return annotate([ops.literal, parsed], location);
|
|
467
|
+
}
|
|
468
|
+
|
|
385
469
|
/**
|
|
386
470
|
* Upgrade a potential builtin reference to an actual builtin reference.
|
|
387
471
|
*
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { Mixin } from "../../index.ts";
|
|
2
2
|
|
|
3
|
-
declare const EventTargetMixin: Mixin<
|
|
4
|
-
addEventListener(type: string, listener: EventListener): void;
|
|
5
|
-
dispatchEvent(event: Event): boolean;
|
|
6
|
-
removeEventListener(type: string, listener: EventListener): void;
|
|
7
|
-
}>;
|
|
3
|
+
declare const EventTargetMixin: Mixin<EventTarget>;
|
|
8
4
|
|
|
9
5
|
export default EventTargetMixin;
|
package/src/runtime/errors.js
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
trailingSlash,
|
|
6
6
|
TraverseError,
|
|
7
7
|
} from "@weborigami/async-tree";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
import codeFragment from "./codeFragment.js";
|
|
9
11
|
import { typos } from "./typos.js";
|
|
10
12
|
|
|
@@ -85,9 +87,24 @@ export function formatError(error) {
|
|
|
85
87
|
if (!fragmentInMessage) {
|
|
86
88
|
message += `\nevaluating: ${fragment}`;
|
|
87
89
|
}
|
|
90
|
+
|
|
88
91
|
if (typeof source === "object" && source.url) {
|
|
89
|
-
|
|
92
|
+
const { url } = source;
|
|
93
|
+
let fileRef;
|
|
94
|
+
// If URL is a file: URL, change to a relative path
|
|
95
|
+
if (url.protocol === "file:") {
|
|
96
|
+
fileRef = fileURLToPath(url);
|
|
97
|
+
const relativePath = path.relative(process.cwd(), fileRef);
|
|
98
|
+
if (!relativePath.startsWith("..")) {
|
|
99
|
+
fileRef = relativePath;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// Not a file: URL, use as is
|
|
103
|
+
fileRef = url.href;
|
|
104
|
+
}
|
|
105
|
+
message += `\n at ${fileRef}:${line}:${start.column}`;
|
|
90
106
|
} else if (source.text.includes("\n")) {
|
|
107
|
+
// Don't know the URL, but has multiple lines so add line number
|
|
91
108
|
message += `\n at line ${line}, column ${start.column}`;
|
|
92
109
|
}
|
|
93
110
|
}
|
|
@@ -113,7 +130,7 @@ export function maybeOrigamiSourceCode(text) {
|
|
|
113
130
|
|
|
114
131
|
export async function scopeReferenceError(scope, key) {
|
|
115
132
|
const messages = [
|
|
116
|
-
`"${key}" is not in scope.`,
|
|
133
|
+
`"${key}" is not in scope or is undefined.`,
|
|
117
134
|
await formatScopeTypos(scope, key),
|
|
118
135
|
];
|
|
119
136
|
const message = messages.join(" ");
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
|
|
2
2
|
import codeFragment from "./codeFragment.js";
|
|
3
|
-
import { ops } from "./internal.js";
|
|
4
3
|
import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -20,14 +19,7 @@ export default async function evaluate(code) {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
let evaluated;
|
|
23
|
-
|
|
24
|
-
ops.external,
|
|
25
|
-
ops.lambda,
|
|
26
|
-
ops.merge,
|
|
27
|
-
ops.object,
|
|
28
|
-
ops.literal,
|
|
29
|
-
];
|
|
30
|
-
if (unevaluatedFns.includes(code[0])) {
|
|
22
|
+
if (code[0]?.unevaluatedArgs) {
|
|
31
23
|
// Don't evaluate instructions, use as is.
|
|
32
24
|
evaluated = code;
|
|
33
25
|
} else {
|
package/src/runtime/ops.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Tree,
|
|
11
11
|
isUnpackable,
|
|
12
12
|
scope as scopeFn,
|
|
13
|
+
symbols,
|
|
13
14
|
concat as treeConcat,
|
|
14
15
|
} from "@weborigami/async-tree";
|
|
15
16
|
import os from "node:os";
|
|
@@ -113,6 +114,29 @@ export async function conditional(condition, truthy, falsy) {
|
|
|
113
114
|
return value instanceof Function ? await value() : value;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Construct a document object by invoking the body code (a lambda) and adding
|
|
119
|
+
* the resulting text to the front data.
|
|
120
|
+
*
|
|
121
|
+
* @this {AsyncTree|null}
|
|
122
|
+
* @param {any} frontData
|
|
123
|
+
* @param {AnnotatedCode} bodyCode
|
|
124
|
+
*/
|
|
125
|
+
export async function document(frontData, bodyCode) {
|
|
126
|
+
const context = new ObjectTree(frontData);
|
|
127
|
+
context.parent = this;
|
|
128
|
+
const bodyFn = await evaluate.call(context, bodyCode);
|
|
129
|
+
const body = await bodyFn();
|
|
130
|
+
const object = {
|
|
131
|
+
...frontData,
|
|
132
|
+
"@text": body,
|
|
133
|
+
};
|
|
134
|
+
object[symbols.parent] = this;
|
|
135
|
+
return object;
|
|
136
|
+
}
|
|
137
|
+
addOpLabel(document, "«ops.document");
|
|
138
|
+
document.unevaluatedArgs = true;
|
|
139
|
+
|
|
116
140
|
export function division(a, b) {
|
|
117
141
|
return a / b;
|
|
118
142
|
}
|
|
@@ -158,6 +182,7 @@ export async function external(path, code, cache) {
|
|
|
158
182
|
return value;
|
|
159
183
|
}
|
|
160
184
|
addOpLabel(external, "«ops.external»");
|
|
185
|
+
external.unevaluatedArgs = true;
|
|
161
186
|
|
|
162
187
|
/**
|
|
163
188
|
* This op is only used during parsing. It signals to ops.object that the
|
|
@@ -223,7 +248,8 @@ export function lambda(parameters, code) {
|
|
|
223
248
|
// Add arguments to scope.
|
|
224
249
|
const ambients = {};
|
|
225
250
|
for (const parameter of parameters) {
|
|
226
|
-
|
|
251
|
+
const parameterName = parameter[1];
|
|
252
|
+
ambients[parameterName] = args.shift();
|
|
227
253
|
}
|
|
228
254
|
Object.defineProperty(ambients, codeSymbol, {
|
|
229
255
|
value: code,
|
|
@@ -262,6 +288,7 @@ export function lambda(parameters, code) {
|
|
|
262
288
|
return invoke;
|
|
263
289
|
}
|
|
264
290
|
addOpLabel(lambda, "«ops.lambda");
|
|
291
|
+
lambda.unevaluatedArgs = true;
|
|
265
292
|
|
|
266
293
|
export function lessThan(a, b) {
|
|
267
294
|
return a < b;
|
|
@@ -283,6 +310,7 @@ export async function literal(value) {
|
|
|
283
310
|
return value;
|
|
284
311
|
}
|
|
285
312
|
addOpLabel(literal, "«ops.literal»");
|
|
313
|
+
literal.unevaluatedArgs = true;
|
|
286
314
|
|
|
287
315
|
/**
|
|
288
316
|
* Logical AND operator
|
|
@@ -376,6 +404,7 @@ export async function merge(...codes) {
|
|
|
376
404
|
return mergeTrees.call(this, ...trees);
|
|
377
405
|
}
|
|
378
406
|
addOpLabel(merge, "«ops.merge»");
|
|
407
|
+
merge.unevaluatedArgs = true;
|
|
379
408
|
|
|
380
409
|
export function multiplication(a, b) {
|
|
381
410
|
return a * b;
|
|
@@ -424,6 +453,7 @@ export async function object(...entries) {
|
|
|
424
453
|
return expressionObject(entries, this);
|
|
425
454
|
}
|
|
426
455
|
addOpLabel(object, "«ops.object»");
|
|
456
|
+
object.unevaluatedArgs = true;
|
|
427
457
|
|
|
428
458
|
export function remainder(a, b) {
|
|
429
459
|
return a % b;
|
|
@@ -4,7 +4,7 @@ import assert from "node:assert";
|
|
|
4
4
|
export function assertCodeEqual(actual, expected) {
|
|
5
5
|
const actualStripped = stripCodeLocations(actual);
|
|
6
6
|
const expectedStripped = stripCodeLocations(expected);
|
|
7
|
-
assert.
|
|
7
|
+
assert.deepStrictEqual(actualStripped, expectedStripped);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -64,12 +64,16 @@ describe("Origami parser", () => {
|
|
|
64
64
|
]);
|
|
65
65
|
assertParse("arrowFunction", "x => y", [
|
|
66
66
|
ops.lambda,
|
|
67
|
-
["x"],
|
|
67
|
+
[[ops.literal, "x"]],
|
|
68
68
|
[ops.scope, "y"],
|
|
69
69
|
]);
|
|
70
70
|
assertParse("arrowFunction", "(a, b, c) ⇒ fn(a, b, c)", [
|
|
71
71
|
ops.lambda,
|
|
72
|
-
[
|
|
72
|
+
[
|
|
73
|
+
[ops.literal, "a"],
|
|
74
|
+
[ops.literal, "b"],
|
|
75
|
+
[ops.literal, "c"],
|
|
76
|
+
],
|
|
73
77
|
[
|
|
74
78
|
[ops.builtin, "fn"],
|
|
75
79
|
[ops.scope, "a"],
|
|
@@ -79,10 +83,10 @@ describe("Origami parser", () => {
|
|
|
79
83
|
]);
|
|
80
84
|
assertParse("arrowFunction", "a => b => fn(a, b)", [
|
|
81
85
|
ops.lambda,
|
|
82
|
-
["a"],
|
|
86
|
+
[[ops.literal, "a"]],
|
|
83
87
|
[
|
|
84
88
|
ops.lambda,
|
|
85
|
-
["b"],
|
|
89
|
+
[[ops.literal, "b"]],
|
|
86
90
|
[
|
|
87
91
|
[ops.builtin, "fn"],
|
|
88
92
|
[ops.scope, "a"],
|
|
@@ -247,20 +251,20 @@ describe("Origami parser", () => {
|
|
|
247
251
|
assertParse("conditionalExpression", "true ? 1 : 0", [
|
|
248
252
|
ops.conditional,
|
|
249
253
|
[ops.scope, "true"],
|
|
250
|
-
[ops.literal,
|
|
251
|
-
[ops.literal,
|
|
254
|
+
[ops.literal, 1],
|
|
255
|
+
[ops.literal, 0],
|
|
252
256
|
]);
|
|
253
257
|
assertParse("conditionalExpression", "false ? () => 1 : 0", [
|
|
254
258
|
ops.conditional,
|
|
255
259
|
[ops.scope, "false"],
|
|
256
|
-
[ops.lambda, [], [ops.lambda, [], [ops.literal,
|
|
257
|
-
[ops.literal,
|
|
260
|
+
[ops.lambda, [], [ops.lambda, [], [ops.literal, 1]]],
|
|
261
|
+
[ops.literal, 0],
|
|
258
262
|
]);
|
|
259
263
|
assertParse("conditionalExpression", "false ? =1 : 0", [
|
|
260
264
|
ops.conditional,
|
|
261
265
|
[ops.scope, "false"],
|
|
262
|
-
[ops.lambda, [], [ops.lambda, ["_"], [ops.literal,
|
|
263
|
-
[ops.literal,
|
|
266
|
+
[ops.lambda, [], [ops.lambda, [[ops.literal, "_"]], [ops.literal, 1]]],
|
|
267
|
+
[ops.literal, 0],
|
|
264
268
|
]);
|
|
265
269
|
});
|
|
266
270
|
|
|
@@ -282,6 +286,43 @@ describe("Origami parser", () => {
|
|
|
282
286
|
]);
|
|
283
287
|
});
|
|
284
288
|
|
|
289
|
+
test("error thrown for missing token", () => {
|
|
290
|
+
assertThrows("arrowFunction", "(a) => ", "Expected an expression");
|
|
291
|
+
assertThrows("arrowFunction", "a ⇒ ", "Expected an expression");
|
|
292
|
+
assertThrows("callExpression", "fn(a", "Expected right parenthesis");
|
|
293
|
+
assertThrows("doubleQuoteString", '"foo', "Expected closing quote");
|
|
294
|
+
assertThrows("guillemetString", "«foo", "Expected closing guillemet");
|
|
295
|
+
assertThrows("objectGetter", "a =", "Expected an expression");
|
|
296
|
+
assertThrows("objectProperty", "a:", "Expected an expression");
|
|
297
|
+
assertThrows("singleQuoteString", "'foo", "Expected closing quote");
|
|
298
|
+
assertThrows("templateLiteral", "`foo", "Expected closing backtick");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test.only("error thrown for invalid Origami front matter expression", () => {
|
|
302
|
+
assertThrows(
|
|
303
|
+
"templateDocument",
|
|
304
|
+
`---
|
|
305
|
+
(name) => foo)
|
|
306
|
+
---
|
|
307
|
+
Body`,
|
|
308
|
+
'Expected "---"',
|
|
309
|
+
{ line: 2, column: 14 }
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("error thrown for invalid YAML front matter", () => {
|
|
314
|
+
assertThrows(
|
|
315
|
+
"templateDocument",
|
|
316
|
+
`---
|
|
317
|
+
a : 1
|
|
318
|
+
}
|
|
319
|
+
---
|
|
320
|
+
Body`,
|
|
321
|
+
"Unexpected flow-map-end token",
|
|
322
|
+
{ line: 3, column: 1 }
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
285
326
|
test("exponentiationExpression", () => {
|
|
286
327
|
assertParse("exponentiationExpression", "2 ** 2 ** 3", [
|
|
287
328
|
ops.exponentiation,
|
|
@@ -293,12 +334,10 @@ describe("Origami parser", () => {
|
|
|
293
334
|
test("expression", () => {
|
|
294
335
|
assertParse(
|
|
295
336
|
"expression",
|
|
296
|
-
`
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
`,
|
|
337
|
+
`{
|
|
338
|
+
index.html = index.ori(teamData.yaml)
|
|
339
|
+
thumbnails = map(images, { value: thumbnail.js })
|
|
340
|
+
}`,
|
|
302
341
|
[
|
|
303
342
|
ops.object,
|
|
304
343
|
[
|
|
@@ -389,7 +428,7 @@ describe("Origami parser", () => {
|
|
|
389
428
|
]);
|
|
390
429
|
assertParse("expression", "fn =`x`", [
|
|
391
430
|
[ops.builtin, "fn"],
|
|
392
|
-
[ops.lambda, ["_"], [ops.template, [ops.literal, ["x"]]]],
|
|
431
|
+
[ops.lambda, [[ops.literal, "_"]], [ops.template, [ops.literal, ["x"]]]],
|
|
393
432
|
]);
|
|
394
433
|
assertParse("expression", "copy app.js(formulas), files:snapshot", [
|
|
395
434
|
[ops.builtin, "copy"],
|
|
@@ -406,7 +445,7 @@ describe("Origami parser", () => {
|
|
|
406
445
|
[ops.builtin, "map"],
|
|
407
446
|
[
|
|
408
447
|
ops.lambda,
|
|
409
|
-
["_"],
|
|
448
|
+
[[ops.literal, "_"]],
|
|
410
449
|
[
|
|
411
450
|
ops.template,
|
|
412
451
|
[ops.literal, ["<li>", "</li>"]],
|
|
@@ -426,7 +465,10 @@ describe("Origami parser", () => {
|
|
|
426
465
|
]);
|
|
427
466
|
assertParse("expression", "(post, slug) => fn.js(post, slug)", [
|
|
428
467
|
ops.lambda,
|
|
429
|
-
[
|
|
468
|
+
[
|
|
469
|
+
[ops.literal, "post"],
|
|
470
|
+
[ops.literal, "slug"],
|
|
471
|
+
],
|
|
430
472
|
[
|
|
431
473
|
[ops.scope, "fn.js"],
|
|
432
474
|
[ops.scope, "post"],
|
|
@@ -718,7 +760,7 @@ describe("Origami parser", () => {
|
|
|
718
760
|
assertParse("objectEntry", "a: a", ["a", [ops.inherited, "a"]]);
|
|
719
761
|
assertParse("objectEntry", "a: (a) => a", [
|
|
720
762
|
"a",
|
|
721
|
-
[ops.lambda, ["a"], [ops.scope, "a"]],
|
|
763
|
+
[ops.lambda, [[ops.literal, "a"]], [ops.scope, "a"]],
|
|
722
764
|
]);
|
|
723
765
|
assertParse("objectEntry", "posts/: map(posts, post.ori)", [
|
|
724
766
|
"posts/",
|
|
@@ -943,12 +985,12 @@ describe("Origami parser", () => {
|
|
|
943
985
|
test("shorthandFunction", () => {
|
|
944
986
|
assertParse("shorthandFunction", "=message", [
|
|
945
987
|
ops.lambda,
|
|
946
|
-
["_"],
|
|
988
|
+
[[ops.literal, "_"]],
|
|
947
989
|
[undetermined, "message"],
|
|
948
990
|
]);
|
|
949
991
|
assertParse("shorthandFunction", "=`Hello, ${name}.`", [
|
|
950
992
|
ops.lambda,
|
|
951
|
-
["_"],
|
|
993
|
+
[[ops.literal, "_"]],
|
|
952
994
|
[
|
|
953
995
|
ops.template,
|
|
954
996
|
[ops.literal, ["Hello, ", "."]],
|
|
@@ -957,7 +999,7 @@ describe("Origami parser", () => {
|
|
|
957
999
|
]);
|
|
958
1000
|
assertParse("shorthandFunction", "=indent`hello`", [
|
|
959
1001
|
ops.lambda,
|
|
960
|
-
["_"],
|
|
1002
|
+
[[ops.literal, "_"]],
|
|
961
1003
|
[
|
|
962
1004
|
[ops.builtin, "indent"],
|
|
963
1005
|
[ops.literal, ["hello"]],
|
|
@@ -988,19 +1030,19 @@ describe("Origami parser", () => {
|
|
|
988
1030
|
]);
|
|
989
1031
|
});
|
|
990
1032
|
|
|
991
|
-
test("
|
|
992
|
-
assertParse("
|
|
1033
|
+
test("templateBody", () => {
|
|
1034
|
+
assertParse("templateBody", "hello${foo}world", [
|
|
993
1035
|
ops.lambda,
|
|
994
|
-
["_"],
|
|
1036
|
+
[[ops.literal, "_"]],
|
|
995
1037
|
[
|
|
996
1038
|
ops.templateIndent,
|
|
997
1039
|
[ops.literal, ["hello", "world"]],
|
|
998
1040
|
[ops.concat, [ops.scope, "foo"]],
|
|
999
1041
|
],
|
|
1000
1042
|
]);
|
|
1001
|
-
assertParse("
|
|
1043
|
+
assertParse("templateBody", "Documents can contain ` backticks", [
|
|
1002
1044
|
ops.lambda,
|
|
1003
|
-
["_"],
|
|
1045
|
+
[[ops.literal, "_"]],
|
|
1004
1046
|
[
|
|
1005
1047
|
ops.templateIndent,
|
|
1006
1048
|
[ops.literal, ["Documents can contain ` backticks"]],
|
|
@@ -1008,6 +1050,66 @@ describe("Origami parser", () => {
|
|
|
1008
1050
|
]);
|
|
1009
1051
|
});
|
|
1010
1052
|
|
|
1053
|
+
test("templateDocument with no front matter", () => {
|
|
1054
|
+
assertParse("templateDocument", "Hello, world!", [
|
|
1055
|
+
ops.lambda,
|
|
1056
|
+
[[ops.literal, "_"]],
|
|
1057
|
+
[ops.templateIndent, [ops.literal, ["Hello, world!"]]],
|
|
1058
|
+
]);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("templateDocument with YAML front matter", () => {
|
|
1062
|
+
assertParse(
|
|
1063
|
+
"templateDocument",
|
|
1064
|
+
`---
|
|
1065
|
+
title: Title goes here
|
|
1066
|
+
---
|
|
1067
|
+
Body text`,
|
|
1068
|
+
[
|
|
1069
|
+
ops.document,
|
|
1070
|
+
[ops.literal, { title: "Title goes here" }],
|
|
1071
|
+
[
|
|
1072
|
+
ops.lambda,
|
|
1073
|
+
[[ops.literal, "_"]],
|
|
1074
|
+
[ops.templateIndent, [ops.literal, ["Body text"]]],
|
|
1075
|
+
],
|
|
1076
|
+
]
|
|
1077
|
+
);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
test("templateDocument with Origami front matter", () => {
|
|
1081
|
+
assertParse(
|
|
1082
|
+
"templateDocument",
|
|
1083
|
+
`---
|
|
1084
|
+
{
|
|
1085
|
+
title: "Title"
|
|
1086
|
+
@text: @template()
|
|
1087
|
+
}
|
|
1088
|
+
---
|
|
1089
|
+
<h1>\${ title }</h1>
|
|
1090
|
+
`,
|
|
1091
|
+
[
|
|
1092
|
+
ops.object,
|
|
1093
|
+
["title", [ops.literal, "Title"]],
|
|
1094
|
+
[
|
|
1095
|
+
"@text",
|
|
1096
|
+
[
|
|
1097
|
+
[
|
|
1098
|
+
ops.lambda,
|
|
1099
|
+
[[ops.literal, "_"]],
|
|
1100
|
+
[
|
|
1101
|
+
ops.templateIndent,
|
|
1102
|
+
[ops.literal, ["<h1>", "</h1>\n"]],
|
|
1103
|
+
[ops.concat, [ops.scope, "title"]],
|
|
1104
|
+
],
|
|
1105
|
+
],
|
|
1106
|
+
undefined,
|
|
1107
|
+
],
|
|
1108
|
+
],
|
|
1109
|
+
]
|
|
1110
|
+
);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1011
1113
|
test("templateLiteral", () => {
|
|
1012
1114
|
assertParse("templateLiteral", "`Hello, world.`", [
|
|
1013
1115
|
ops.template,
|
|
@@ -1033,7 +1135,7 @@ describe("Origami parser", () => {
|
|
|
1033
1135
|
[ops.scope, "people"],
|
|
1034
1136
|
[
|
|
1035
1137
|
ops.lambda,
|
|
1036
|
-
["_"],
|
|
1138
|
+
[[ops.literal, "_"]],
|
|
1037
1139
|
[
|
|
1038
1140
|
ops.template,
|
|
1039
1141
|
[ops.literal, ["", ""]],
|
|
@@ -1087,7 +1189,7 @@ function assertParse(startRule, source, expected, checkLocation = true) {
|
|
|
1087
1189
|
code.location.start.offset,
|
|
1088
1190
|
code.location.end.offset
|
|
1089
1191
|
);
|
|
1090
|
-
assert.equal(resultSource, source
|
|
1192
|
+
assert.equal(resultSource, source);
|
|
1091
1193
|
}
|
|
1092
1194
|
|
|
1093
1195
|
assertCodeEqual(code, expected);
|
|
@@ -1101,3 +1203,23 @@ function assertCodeLocations(code) {
|
|
|
1101
1203
|
}
|
|
1102
1204
|
}
|
|
1103
1205
|
}
|
|
1206
|
+
|
|
1207
|
+
function assertThrows(startRule, source, message, position) {
|
|
1208
|
+
try {
|
|
1209
|
+
parse(source, {
|
|
1210
|
+
grammarSource: { text: source },
|
|
1211
|
+
startRule,
|
|
1212
|
+
});
|
|
1213
|
+
} catch (/** @type {any} */ error) {
|
|
1214
|
+
assert(
|
|
1215
|
+
error.message.includes(message),
|
|
1216
|
+
`Error message incorrect:\n expected: "${message}"\n actual: "${error.message}"`
|
|
1217
|
+
);
|
|
1218
|
+
if (position) {
|
|
1219
|
+
assert.equal(error.location.start.line, position.line);
|
|
1220
|
+
assert.equal(error.location.start.column, position.column);
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
assert.fail(`Expected error: ${message}`);
|
|
1225
|
+
}
|
package/test/runtime/ops.test.js
CHANGED
|
@@ -80,6 +80,29 @@ describe("ops", () => {
|
|
|
80
80
|
assert.strictEqual(await ops.conditional(false, errorFn, trueFn), true);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
test("ops.documentFunction", async () => {
|
|
84
|
+
const code = createCode([
|
|
85
|
+
ops.document,
|
|
86
|
+
{
|
|
87
|
+
a: 1,
|
|
88
|
+
},
|
|
89
|
+
[
|
|
90
|
+
ops.lambda,
|
|
91
|
+
[["_"]],
|
|
92
|
+
[
|
|
93
|
+
ops.template,
|
|
94
|
+
[ops.literal, ["a = ", ""]],
|
|
95
|
+
[ops.concat, [ops.scope, "a"]],
|
|
96
|
+
],
|
|
97
|
+
],
|
|
98
|
+
]);
|
|
99
|
+
const result = await evaluate.call(null, code);
|
|
100
|
+
assert.deepEqual(result, {
|
|
101
|
+
a: 1,
|
|
102
|
+
"@text": "a = 1",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
83
106
|
test("ops.division divides two numbers", async () => {
|
|
84
107
|
assert.strictEqual(ops.division(12, 2), 6);
|
|
85
108
|
assert.strictEqual(ops.division(3, 2), 1.5);
|
|
@@ -169,7 +192,10 @@ describe("ops", () => {
|
|
|
169
192
|
test("ops.lambda adds input parameters to scope", async () => {
|
|
170
193
|
const code = createCode([
|
|
171
194
|
ops.lambda,
|
|
172
|
-
[
|
|
195
|
+
[
|
|
196
|
+
[ops.literal, "a"],
|
|
197
|
+
[ops.literal, "b"],
|
|
198
|
+
],
|
|
173
199
|
[ops.concat, [ops.scope, "b"], [ops.scope, "a"]],
|
|
174
200
|
]);
|
|
175
201
|
const fn = await evaluate.call(null, code);
|