@weborigami/language 0.2.10 → 0.2.12
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 -1
- package/package.json +6 -6
- package/src/compiler/isOrigamiFrontMatter.js +26 -0
- package/src/compiler/origami.pegjs +56 -10
- package/src/compiler/parse.js +623 -349
- package/src/compiler/parserHelpers.js +107 -3
- package/src/runtime/errors.js +19 -2
- package/src/runtime/evaluate.js +1 -9
- package/src/runtime/ops.js +33 -4
- package/src/runtime/taggedTemplateIndent.js +10 -5
- package/test/compiler/codeHelpers.js +1 -1
- package/test/compiler/parse.test.js +115 -43
- package/test/runtime/expressionObject.test.js +2 -2
- package/test/runtime/ops.test.js +23 -0
- package/test/runtime/taggedTemplateIndent.test.js +6 -6
- package/src/runtime/taggedTemplate.js +0 -9
- package/test/runtime/taggedTemplate.test.js +0 -10
|
@@ -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
|
|
@@ -198,7 +226,13 @@ export function makeCall(target, args) {
|
|
|
198
226
|
}
|
|
199
227
|
} else if (args[0] === ops.template) {
|
|
200
228
|
// Tagged template
|
|
201
|
-
|
|
229
|
+
const strings = args[1];
|
|
230
|
+
const values = args.slice(2);
|
|
231
|
+
fnCall = makeTaggedTemplateCall(
|
|
232
|
+
upgradeReference(target),
|
|
233
|
+
strings,
|
|
234
|
+
...values
|
|
235
|
+
);
|
|
202
236
|
} else {
|
|
203
237
|
// Function call with explicit or implicit parentheses
|
|
204
238
|
fnCall = [upgradeReference(target), ...args];
|
|
@@ -345,6 +379,24 @@ export function makeReference(identifier) {
|
|
|
345
379
|
return [op, identifier];
|
|
346
380
|
}
|
|
347
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Make a tagged template call
|
|
384
|
+
*
|
|
385
|
+
* Because the tagged template function may not be an Origami function, we wrap
|
|
386
|
+
* each argument in a ops.concat call to convert it to a string.
|
|
387
|
+
*
|
|
388
|
+
* @param {AnnotatedCode} fn
|
|
389
|
+
* @param {AnnotatedCode} strings
|
|
390
|
+
* @param {AnnotatedCode[]} values
|
|
391
|
+
*/
|
|
392
|
+
function makeTaggedTemplateCall(fn, strings, ...values) {
|
|
393
|
+
const args = values.map((value) =>
|
|
394
|
+
// @ts-ignore
|
|
395
|
+
annotate([ops.concat, value], value.location)
|
|
396
|
+
);
|
|
397
|
+
return annotate([fn, strings, ...args], strings.location);
|
|
398
|
+
}
|
|
399
|
+
|
|
348
400
|
/**
|
|
349
401
|
* Make a template
|
|
350
402
|
*
|
|
@@ -357,8 +409,7 @@ export function makeTemplate(op, head, tail, location) {
|
|
|
357
409
|
const strings = [head[1]];
|
|
358
410
|
const values = [];
|
|
359
411
|
for (const [value, literal] of tail) {
|
|
360
|
-
|
|
361
|
-
values.push(concat);
|
|
412
|
+
values.push(value);
|
|
362
413
|
strings.push(literal[1]);
|
|
363
414
|
}
|
|
364
415
|
const stringsCode = annotate(strings, location);
|
|
@@ -385,6 +436,59 @@ export function makeUnaryOperation(operator, value, location) {
|
|
|
385
436
|
return annotate([operators[operator], value], location);
|
|
386
437
|
}
|
|
387
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Make an object from YAML front matter
|
|
441
|
+
*
|
|
442
|
+
* @param {string} text
|
|
443
|
+
* @param {CodeLocation} location
|
|
444
|
+
*/
|
|
445
|
+
export function makeYamlObject(text, location) {
|
|
446
|
+
// Account for the "---" delimiter at the beginning of the YAML front matter
|
|
447
|
+
const yamlLineDelta = 1;
|
|
448
|
+
const yamlOffsetDelta = 4; // 3 dashes + 1 newline
|
|
449
|
+
|
|
450
|
+
let parsed;
|
|
451
|
+
try {
|
|
452
|
+
parsed = YAML.parse(text);
|
|
453
|
+
} catch (/** @type {any} */ yamlError) {
|
|
454
|
+
// Convert YAML error to a SyntaxError
|
|
455
|
+
|
|
456
|
+
let { message } = yamlError;
|
|
457
|
+
// Remove the line number and column if present
|
|
458
|
+
const lineNumberRegex = /( at line )(\d+)(,)/;
|
|
459
|
+
const lineNumberMatch = message.match(lineNumberRegex);
|
|
460
|
+
if (lineNumberMatch) {
|
|
461
|
+
message = message.slice(0, lineNumberMatch.index);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** @type {any} */
|
|
465
|
+
const error = new SyntaxError(message);
|
|
466
|
+
error.location = {
|
|
467
|
+
end: {
|
|
468
|
+
column: yamlError.linePos[1].col,
|
|
469
|
+
line: yamlError.linePos[1].line + yamlLineDelta,
|
|
470
|
+
offset: yamlError.pos[1] + yamlOffsetDelta,
|
|
471
|
+
},
|
|
472
|
+
source: location.source,
|
|
473
|
+
start: {
|
|
474
|
+
column: yamlError.linePos[0].col,
|
|
475
|
+
line: yamlError.linePos[0].line + yamlLineDelta,
|
|
476
|
+
offset: yamlError.pos[0] + yamlOffsetDelta,
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!(parsed instanceof Object)) {
|
|
483
|
+
/** @type {any} */
|
|
484
|
+
const error = new SyntaxError("YAML front matter must be an object.");
|
|
485
|
+
error.location = location;
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return annotate([ops.literal, parsed], location);
|
|
490
|
+
}
|
|
491
|
+
|
|
388
492
|
/**
|
|
389
493
|
* Upgrade a potential builtin reference to an actual builtin reference.
|
|
390
494
|
*
|
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
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import {
|
|
9
9
|
ObjectTree,
|
|
10
10
|
Tree,
|
|
11
|
+
concatTrees,
|
|
11
12
|
isUnpackable,
|
|
12
13
|
scope as scopeFn,
|
|
14
|
+
symbols,
|
|
13
15
|
concat as treeConcat,
|
|
14
16
|
} from "@weborigami/async-tree";
|
|
15
17
|
import os from "node:os";
|
|
@@ -20,7 +22,6 @@ import { evaluate } from "./internal.js";
|
|
|
20
22
|
import mergeTrees from "./mergeTrees.js";
|
|
21
23
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
22
24
|
import { codeSymbol } from "./symbols.js";
|
|
23
|
-
import taggedTemplate from "./taggedTemplate.js";
|
|
24
25
|
|
|
25
26
|
function addOpLabel(op, label) {
|
|
26
27
|
Object.defineProperty(op, "toString", {
|
|
@@ -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
|
|
@@ -263,6 +288,7 @@ export function lambda(parameters, code) {
|
|
|
263
288
|
return invoke;
|
|
264
289
|
}
|
|
265
290
|
addOpLabel(lambda, "«ops.lambda");
|
|
291
|
+
lambda.unevaluatedArgs = true;
|
|
266
292
|
|
|
267
293
|
export function lessThan(a, b) {
|
|
268
294
|
return a < b;
|
|
@@ -284,6 +310,7 @@ export async function literal(value) {
|
|
|
284
310
|
return value;
|
|
285
311
|
}
|
|
286
312
|
addOpLabel(literal, "«ops.literal»");
|
|
313
|
+
literal.unevaluatedArgs = true;
|
|
287
314
|
|
|
288
315
|
/**
|
|
289
316
|
* Logical AND operator
|
|
@@ -377,6 +404,7 @@ export async function merge(...codes) {
|
|
|
377
404
|
return mergeTrees.call(this, ...trees);
|
|
378
405
|
}
|
|
379
406
|
addOpLabel(merge, "«ops.merge»");
|
|
407
|
+
merge.unevaluatedArgs = true;
|
|
380
408
|
|
|
381
409
|
export function multiplication(a, b) {
|
|
382
410
|
return a * b;
|
|
@@ -425,6 +453,7 @@ export async function object(...entries) {
|
|
|
425
453
|
return expressionObject(entries, this);
|
|
426
454
|
}
|
|
427
455
|
addOpLabel(object, "«ops.object»");
|
|
456
|
+
object.unevaluatedArgs = true;
|
|
428
457
|
|
|
429
458
|
export function remainder(a, b) {
|
|
430
459
|
return a % b;
|
|
@@ -503,15 +532,15 @@ addOpLabel(subtraction, "«ops.subtraction»");
|
|
|
503
532
|
/**
|
|
504
533
|
* Apply the default tagged template function.
|
|
505
534
|
*/
|
|
506
|
-
export function template(strings, ...values) {
|
|
507
|
-
return
|
|
535
|
+
export async function template(strings, ...values) {
|
|
536
|
+
return concatTrees(strings, ...values);
|
|
508
537
|
}
|
|
509
538
|
addOpLabel(template, "«ops.template»");
|
|
510
539
|
|
|
511
540
|
/**
|
|
512
541
|
* Apply the tagged template indent function.
|
|
513
542
|
*/
|
|
514
|
-
export function templateIndent(strings, ...values) {
|
|
543
|
+
export async function templateIndent(strings, ...values) {
|
|
515
544
|
return taggedTemplateIndent(strings, ...values);
|
|
516
545
|
}
|
|
517
546
|
addOpLabel(templateIndent, "«ops.templateIndent");
|
|
@@ -1,22 +1,27 @@
|
|
|
1
|
+
import { concat, toString, Tree } from "@weborigami/async-tree";
|
|
2
|
+
|
|
1
3
|
const lastLineWhitespaceRegex = /\n(?<indent>[ \t]*)$/;
|
|
2
4
|
|
|
3
5
|
const mapStringsToModifications = new Map();
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
* Normalize indentation in a tagged template string
|
|
8
|
+
* Normalize indentation in a tagged template string
|
|
7
9
|
*
|
|
8
10
|
* @param {TemplateStringsArray} strings
|
|
9
11
|
* @param {...any} values
|
|
10
|
-
* @returns {string}
|
|
12
|
+
* @returns {Promise<string>}
|
|
11
13
|
*/
|
|
12
|
-
export default function indent(strings, ...values) {
|
|
14
|
+
export default async function indent(strings, ...values) {
|
|
13
15
|
let modified = mapStringsToModifications.get(strings);
|
|
14
16
|
if (!modified) {
|
|
15
17
|
modified = modifyStrings(strings);
|
|
16
18
|
mapStringsToModifications.set(strings, modified);
|
|
17
19
|
}
|
|
18
20
|
const { blockIndentations, strings: modifiedStrings } = modified;
|
|
19
|
-
|
|
21
|
+
const valueTexts = await Promise.all(
|
|
22
|
+
values.map((value) => (Tree.isTreelike(value) ? concat(value) : value))
|
|
23
|
+
);
|
|
24
|
+
return joinBlocks(modifiedStrings, valueTexts, blockIndentations);
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
// Join strings and values, applying the given block indentation to the lines of
|
|
@@ -24,7 +29,7 @@ export default function indent(strings, ...values) {
|
|
|
24
29
|
function joinBlocks(strings, values, blockIndentations) {
|
|
25
30
|
let result = strings[0];
|
|
26
31
|
for (let i = 0; i < values.length; i++) {
|
|
27
|
-
let text = values[i];
|
|
32
|
+
let text = toString(values[i]);
|
|
28
33
|
if (text) {
|
|
29
34
|
const blockIndentation = blockIndentations[i];
|
|
30
35
|
if (blockIndentation) {
|
|
@@ -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
|
/**
|
|
@@ -251,20 +251,20 @@ describe("Origami parser", () => {
|
|
|
251
251
|
assertParse("conditionalExpression", "true ? 1 : 0", [
|
|
252
252
|
ops.conditional,
|
|
253
253
|
[ops.scope, "true"],
|
|
254
|
-
[ops.literal,
|
|
255
|
-
[ops.literal,
|
|
254
|
+
[ops.literal, 1],
|
|
255
|
+
[ops.literal, 0],
|
|
256
256
|
]);
|
|
257
257
|
assertParse("conditionalExpression", "false ? () => 1 : 0", [
|
|
258
258
|
ops.conditional,
|
|
259
259
|
[ops.scope, "false"],
|
|
260
|
-
[ops.lambda, [], [ops.lambda, [], [ops.literal,
|
|
261
|
-
[ops.literal,
|
|
260
|
+
[ops.lambda, [], [ops.lambda, [], [ops.literal, 1]]],
|
|
261
|
+
[ops.literal, 0],
|
|
262
262
|
]);
|
|
263
263
|
assertParse("conditionalExpression", "false ? =1 : 0", [
|
|
264
264
|
ops.conditional,
|
|
265
265
|
[ops.scope, "false"],
|
|
266
|
-
[ops.lambda, [], [ops.lambda, [[ops.literal, "_"]], [ops.literal,
|
|
267
|
-
[ops.literal,
|
|
266
|
+
[ops.lambda, [], [ops.lambda, [[ops.literal, "_"]], [ops.literal, 1]]],
|
|
267
|
+
[ops.literal, 0],
|
|
268
268
|
]);
|
|
269
269
|
});
|
|
270
270
|
|
|
@@ -286,7 +286,7 @@ describe("Origami parser", () => {
|
|
|
286
286
|
]);
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
-
test("
|
|
289
|
+
test("error thrown for missing token", () => {
|
|
290
290
|
assertThrows("arrowFunction", "(a) => ", "Expected an expression");
|
|
291
291
|
assertThrows("arrowFunction", "a ⇒ ", "Expected an expression");
|
|
292
292
|
assertThrows("callExpression", "fn(a", "Expected right parenthesis");
|
|
@@ -298,6 +298,31 @@ describe("Origami parser", () => {
|
|
|
298
298
|
assertThrows("templateLiteral", "`foo", "Expected closing backtick");
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
+
test("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
|
+
|
|
301
326
|
test("exponentiationExpression", () => {
|
|
302
327
|
assertParse("exponentiationExpression", "2 ** 2 ** 3", [
|
|
303
328
|
ops.exponentiation,
|
|
@@ -309,12 +334,10 @@ describe("Origami parser", () => {
|
|
|
309
334
|
test("expression", () => {
|
|
310
335
|
assertParse(
|
|
311
336
|
"expression",
|
|
312
|
-
`
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
`,
|
|
337
|
+
`{
|
|
338
|
+
index.html = index.ori(teamData.yaml)
|
|
339
|
+
thumbnails = map(images, { value: thumbnail.js })
|
|
340
|
+
}`,
|
|
318
341
|
[
|
|
319
342
|
ops.object,
|
|
320
343
|
[
|
|
@@ -423,11 +446,7 @@ describe("Origami parser", () => {
|
|
|
423
446
|
[
|
|
424
447
|
ops.lambda,
|
|
425
448
|
[[ops.literal, "_"]],
|
|
426
|
-
[
|
|
427
|
-
ops.template,
|
|
428
|
-
[ops.literal, ["<li>", "</li>"]],
|
|
429
|
-
[ops.concat, [ops.scope, "_"]],
|
|
430
|
-
],
|
|
449
|
+
[ops.template, [ops.literal, ["<li>", "</li>"]], [ops.scope, "_"]],
|
|
431
450
|
],
|
|
432
451
|
]);
|
|
433
452
|
assertParse("expression", `https://example.com/about/`, [
|
|
@@ -968,11 +987,7 @@ describe("Origami parser", () => {
|
|
|
968
987
|
assertParse("shorthandFunction", "=`Hello, ${name}.`", [
|
|
969
988
|
ops.lambda,
|
|
970
989
|
[[ops.literal, "_"]],
|
|
971
|
-
[
|
|
972
|
-
ops.template,
|
|
973
|
-
[ops.literal, ["Hello, ", "."]],
|
|
974
|
-
[ops.concat, [ops.scope, "name"]],
|
|
975
|
-
],
|
|
990
|
+
[ops.template, [ops.literal, ["Hello, ", "."]], [ops.scope, "name"]],
|
|
976
991
|
]);
|
|
977
992
|
assertParse("shorthandFunction", "=indent`hello`", [
|
|
978
993
|
ops.lambda,
|
|
@@ -1007,17 +1022,17 @@ describe("Origami parser", () => {
|
|
|
1007
1022
|
]);
|
|
1008
1023
|
});
|
|
1009
1024
|
|
|
1010
|
-
test("
|
|
1011
|
-
assertParse("
|
|
1025
|
+
test("templateBody", () => {
|
|
1026
|
+
assertParse("templateBody", "hello${foo}world", [
|
|
1012
1027
|
ops.lambda,
|
|
1013
1028
|
[[ops.literal, "_"]],
|
|
1014
1029
|
[
|
|
1015
1030
|
ops.templateIndent,
|
|
1016
1031
|
[ops.literal, ["hello", "world"]],
|
|
1017
|
-
[ops.
|
|
1032
|
+
[ops.scope, "foo"],
|
|
1018
1033
|
],
|
|
1019
1034
|
]);
|
|
1020
|
-
assertParse("
|
|
1035
|
+
assertParse("templateBody", "Documents can contain ` backticks", [
|
|
1021
1036
|
ops.lambda,
|
|
1022
1037
|
[[ops.literal, "_"]],
|
|
1023
1038
|
[
|
|
@@ -1027,6 +1042,66 @@ describe("Origami parser", () => {
|
|
|
1027
1042
|
]);
|
|
1028
1043
|
});
|
|
1029
1044
|
|
|
1045
|
+
test("templateDocument with no front matter", () => {
|
|
1046
|
+
assertParse("templateDocument", "Hello, world!", [
|
|
1047
|
+
ops.lambda,
|
|
1048
|
+
[[ops.literal, "_"]],
|
|
1049
|
+
[ops.templateIndent, [ops.literal, ["Hello, world!"]]],
|
|
1050
|
+
]);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test("templateDocument with YAML front matter", () => {
|
|
1054
|
+
assertParse(
|
|
1055
|
+
"templateDocument",
|
|
1056
|
+
`---
|
|
1057
|
+
title: Title goes here
|
|
1058
|
+
---
|
|
1059
|
+
Body text`,
|
|
1060
|
+
[
|
|
1061
|
+
ops.document,
|
|
1062
|
+
[ops.literal, { title: "Title goes here" }],
|
|
1063
|
+
[
|
|
1064
|
+
ops.lambda,
|
|
1065
|
+
[[ops.literal, "_"]],
|
|
1066
|
+
[ops.templateIndent, [ops.literal, ["Body text"]]],
|
|
1067
|
+
],
|
|
1068
|
+
]
|
|
1069
|
+
);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
test("templateDocument with Origami front matter", () => {
|
|
1073
|
+
assertParse(
|
|
1074
|
+
"templateDocument",
|
|
1075
|
+
`---
|
|
1076
|
+
{
|
|
1077
|
+
title: "Title"
|
|
1078
|
+
@text: @template()
|
|
1079
|
+
}
|
|
1080
|
+
---
|
|
1081
|
+
<h1>\${ title }</h1>
|
|
1082
|
+
`,
|
|
1083
|
+
[
|
|
1084
|
+
ops.object,
|
|
1085
|
+
["title", [ops.literal, "Title"]],
|
|
1086
|
+
[
|
|
1087
|
+
"@text",
|
|
1088
|
+
[
|
|
1089
|
+
[
|
|
1090
|
+
ops.lambda,
|
|
1091
|
+
[[ops.literal, "_"]],
|
|
1092
|
+
[
|
|
1093
|
+
ops.templateIndent,
|
|
1094
|
+
[ops.literal, ["<h1>", "</h1>\n"]],
|
|
1095
|
+
[ops.scope, "title"],
|
|
1096
|
+
],
|
|
1097
|
+
],
|
|
1098
|
+
undefined,
|
|
1099
|
+
],
|
|
1100
|
+
],
|
|
1101
|
+
]
|
|
1102
|
+
);
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1030
1105
|
test("templateLiteral", () => {
|
|
1031
1106
|
assertParse("templateLiteral", "`Hello, world.`", [
|
|
1032
1107
|
ops.template,
|
|
@@ -1035,30 +1110,23 @@ describe("Origami parser", () => {
|
|
|
1035
1110
|
assertParse("templateLiteral", "`foo ${x} bar`", [
|
|
1036
1111
|
ops.template,
|
|
1037
1112
|
[ops.literal, ["foo ", " bar"]],
|
|
1038
|
-
[ops.
|
|
1113
|
+
[ops.scope, "x"],
|
|
1039
1114
|
]);
|
|
1040
1115
|
assertParse("templateLiteral", "`${`nested`}`", [
|
|
1041
1116
|
ops.template,
|
|
1042
1117
|
[ops.literal, ["", ""]],
|
|
1043
|
-
[ops.
|
|
1118
|
+
[ops.template, [ops.literal, ["nested"]]],
|
|
1044
1119
|
]);
|
|
1045
1120
|
assertParse("templateLiteral", "`${ map:(people, =`${name}`) }`", [
|
|
1046
1121
|
ops.template,
|
|
1047
1122
|
[ops.literal, ["", ""]],
|
|
1048
1123
|
[
|
|
1049
|
-
ops.
|
|
1124
|
+
[ops.builtin, "map:"],
|
|
1125
|
+
[ops.scope, "people"],
|
|
1050
1126
|
[
|
|
1051
|
-
|
|
1052
|
-
[ops.
|
|
1053
|
-
[
|
|
1054
|
-
ops.lambda,
|
|
1055
|
-
[[ops.literal, "_"]],
|
|
1056
|
-
[
|
|
1057
|
-
ops.template,
|
|
1058
|
-
[ops.literal, ["", ""]],
|
|
1059
|
-
[ops.concat, [ops.scope, "name"]],
|
|
1060
|
-
],
|
|
1061
|
-
],
|
|
1127
|
+
ops.lambda,
|
|
1128
|
+
[[ops.literal, "_"]],
|
|
1129
|
+
[ops.template, [ops.literal, ["", ""]], [ops.scope, "name"]],
|
|
1062
1130
|
],
|
|
1063
1131
|
],
|
|
1064
1132
|
]);
|
|
@@ -1106,7 +1174,7 @@ function assertParse(startRule, source, expected, checkLocation = true) {
|
|
|
1106
1174
|
code.location.start.offset,
|
|
1107
1175
|
code.location.end.offset
|
|
1108
1176
|
);
|
|
1109
|
-
assert.equal(resultSource, source
|
|
1177
|
+
assert.equal(resultSource, source);
|
|
1110
1178
|
}
|
|
1111
1179
|
|
|
1112
1180
|
assertCodeEqual(code, expected);
|
|
@@ -1121,7 +1189,7 @@ function assertCodeLocations(code) {
|
|
|
1121
1189
|
}
|
|
1122
1190
|
}
|
|
1123
1191
|
|
|
1124
|
-
function assertThrows(startRule, source, message) {
|
|
1192
|
+
function assertThrows(startRule, source, message, position) {
|
|
1125
1193
|
try {
|
|
1126
1194
|
parse(source, {
|
|
1127
1195
|
grammarSource: { text: source },
|
|
@@ -1132,6 +1200,10 @@ function assertThrows(startRule, source, message) {
|
|
|
1132
1200
|
error.message.includes(message),
|
|
1133
1201
|
`Error message incorrect:\n expected: "${message}"\n actual: "${error.message}"`
|
|
1134
1202
|
);
|
|
1203
|
+
if (position) {
|
|
1204
|
+
assert.equal(error.location.start.line, position.line);
|
|
1205
|
+
assert.equal(error.location.start.column, position.column);
|
|
1206
|
+
}
|
|
1135
1207
|
return;
|
|
1136
1208
|
}
|
|
1137
1209
|
assert.fail(`Expected error: ${message}`);
|
|
@@ -5,7 +5,7 @@ import { describe, test } from "node:test";
|
|
|
5
5
|
import expressionObject from "../../src/runtime/expressionObject.js";
|
|
6
6
|
import { ops } from "../../src/runtime/internal.js";
|
|
7
7
|
|
|
8
|
-
describe
|
|
8
|
+
describe("expressionObject", () => {
|
|
9
9
|
test("can instantiate an object", async () => {
|
|
10
10
|
const scope = new ObjectTree({
|
|
11
11
|
upper: (s) => s.toUpperCase(),
|
|
@@ -74,7 +74,7 @@ describe.only("expressionObject", () => {
|
|
|
74
74
|
assert.equal(object["hidden"], "shh");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
test
|
|
77
|
+
test("provides a symbols.keys method", async () => {
|
|
78
78
|
const entries = [
|
|
79
79
|
// Will return a tree, should have a slash
|
|
80
80
|
["getter", [ops.getter, [ops.object, ["b", [ops.literal, 2]]]]],
|
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);
|