@weborigami/language 0.2.10 → 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 +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 +81 -0
- package/src/runtime/errors.js +19 -2
- package/src/runtime/evaluate.js +1 -9
- package/src/runtime/ops.js +29 -0
- package/test/compiler/codeHelpers.js +1 -1
- package/test/compiler/parse.test.js +105 -18
- package/test/runtime/ops.test.js +23 -0
|
@@ -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
|
|
@@ -385,6 +413,59 @@ export function makeUnaryOperation(operator, value, location) {
|
|
|
385
413
|
return annotate([operators[operator], value], location);
|
|
386
414
|
}
|
|
387
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
|
+
|
|
388
469
|
/**
|
|
389
470
|
* Upgrade a potential builtin reference to an actual builtin reference.
|
|
390
471
|
*
|
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
|
|
@@ -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;
|
|
@@ -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.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
|
+
|
|
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
|
[
|
|
@@ -1007,8 +1030,8 @@ describe("Origami parser", () => {
|
|
|
1007
1030
|
]);
|
|
1008
1031
|
});
|
|
1009
1032
|
|
|
1010
|
-
test("
|
|
1011
|
-
assertParse("
|
|
1033
|
+
test("templateBody", () => {
|
|
1034
|
+
assertParse("templateBody", "hello${foo}world", [
|
|
1012
1035
|
ops.lambda,
|
|
1013
1036
|
[[ops.literal, "_"]],
|
|
1014
1037
|
[
|
|
@@ -1017,7 +1040,7 @@ describe("Origami parser", () => {
|
|
|
1017
1040
|
[ops.concat, [ops.scope, "foo"]],
|
|
1018
1041
|
],
|
|
1019
1042
|
]);
|
|
1020
|
-
assertParse("
|
|
1043
|
+
assertParse("templateBody", "Documents can contain ` backticks", [
|
|
1021
1044
|
ops.lambda,
|
|
1022
1045
|
[[ops.literal, "_"]],
|
|
1023
1046
|
[
|
|
@@ -1027,6 +1050,66 @@ describe("Origami parser", () => {
|
|
|
1027
1050
|
]);
|
|
1028
1051
|
});
|
|
1029
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
|
+
|
|
1030
1113
|
test("templateLiteral", () => {
|
|
1031
1114
|
assertParse("templateLiteral", "`Hello, world.`", [
|
|
1032
1115
|
ops.template,
|
|
@@ -1106,7 +1189,7 @@ function assertParse(startRule, source, expected, checkLocation = true) {
|
|
|
1106
1189
|
code.location.start.offset,
|
|
1107
1190
|
code.location.end.offset
|
|
1108
1191
|
);
|
|
1109
|
-
assert.equal(resultSource, source
|
|
1192
|
+
assert.equal(resultSource, source);
|
|
1110
1193
|
}
|
|
1111
1194
|
|
|
1112
1195
|
assertCodeEqual(code, expected);
|
|
@@ -1121,7 +1204,7 @@ function assertCodeLocations(code) {
|
|
|
1121
1204
|
}
|
|
1122
1205
|
}
|
|
1123
1206
|
|
|
1124
|
-
function assertThrows(startRule, source, message) {
|
|
1207
|
+
function assertThrows(startRule, source, message, position) {
|
|
1125
1208
|
try {
|
|
1126
1209
|
parse(source, {
|
|
1127
1210
|
grammarSource: { text: source },
|
|
@@ -1132,6 +1215,10 @@ function assertThrows(startRule, source, message) {
|
|
|
1132
1215
|
error.message.includes(message),
|
|
1133
1216
|
`Error message incorrect:\n expected: "${message}"\n actual: "${error.message}"`
|
|
1134
1217
|
);
|
|
1218
|
+
if (position) {
|
|
1219
|
+
assert.equal(error.location.start.line, position.line);
|
|
1220
|
+
assert.equal(error.location.start.column, position.column);
|
|
1221
|
+
}
|
|
1135
1222
|
return;
|
|
1136
1223
|
}
|
|
1137
1224
|
assert.fail(`Expected error: ${message}`);
|
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);
|