@weborigami/language 0.0.69 → 0.0.71-beta.1
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 +2 -0
- package/package.json +3 -3
- package/src/compiler/compile.js +64 -41
- package/src/compiler/origami.pegjs +40 -28
- package/src/compiler/parse.js +286 -193
- package/src/compiler/parserHelpers.js +9 -13
- package/src/runtime/WatchFilesMixin.js +1 -0
- package/src/runtime/evaluate.js +15 -15
- package/src/runtime/extensions.js +40 -14
- package/src/runtime/ops.js +64 -32
- package/src/runtime/symbols.js +3 -0
- package/src/runtime/taggedTemplate.js +9 -0
- package/test/compiler/compile.test.js +47 -0
- package/test/compiler/parse.test.js +166 -195
- package/test/compiler/stripCodeLocations.js +18 -0
- package/test/runtime/ops.test.js +16 -9
- package/test/runtime/taggedTemplate.test.js +10 -0
|
@@ -153,7 +153,7 @@ export function makeObject(entries, op) {
|
|
|
153
153
|
value instanceof Array &&
|
|
154
154
|
value[0] === ops.getter &&
|
|
155
155
|
value[1] instanceof Array &&
|
|
156
|
-
value[1][0] === ops.
|
|
156
|
+
value[1][0] === ops.literal
|
|
157
157
|
) {
|
|
158
158
|
// Simplify a getter for a primitive value to a regular property
|
|
159
159
|
value = value[1];
|
|
@@ -197,16 +197,12 @@ export function makeProperty(key, value) {
|
|
|
197
197
|
return [key, modified];
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
export function makeTemplate(
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
filtered[0][0] === ops.primitive &&
|
|
209
|
-
typeof filtered[0][1] === "string"
|
|
210
|
-
? filtered[0]
|
|
211
|
-
: [ops.concat, ...filtered];
|
|
200
|
+
export function makeTemplate(op, head, tail) {
|
|
201
|
+
const strings = [head];
|
|
202
|
+
const values = [];
|
|
203
|
+
for (const [value, string] of tail) {
|
|
204
|
+
values.push([ops.concat, value]);
|
|
205
|
+
strings.push(string);
|
|
206
|
+
}
|
|
207
|
+
return [op, [ops.literal, strings], ...values];
|
|
212
208
|
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Tree,
|
|
3
|
-
isPlainObject,
|
|
4
|
-
isUnpackable,
|
|
5
|
-
scope,
|
|
6
|
-
} from "@weborigami/async-tree";
|
|
1
|
+
import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
|
|
7
2
|
import codeFragment from "./codeFragment.js";
|
|
8
3
|
import { ops } from "./internal.js";
|
|
9
|
-
|
|
10
|
-
const codeSymbol = Symbol("code");
|
|
11
|
-
const scopeSymbol = Symbol("scope");
|
|
12
|
-
const sourceSymbol = Symbol("source");
|
|
4
|
+
import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
|
|
13
5
|
|
|
14
6
|
/**
|
|
15
7
|
* Evaluate the given code and return the result.
|
|
@@ -28,7 +20,7 @@ export default async function evaluate(code) {
|
|
|
28
20
|
}
|
|
29
21
|
|
|
30
22
|
let evaluated;
|
|
31
|
-
const unevaluatedFns = [ops.lambda, ops.object];
|
|
23
|
+
const unevaluatedFns = [ops.lambda, ops.object, ops.literal];
|
|
32
24
|
if (unevaluatedFns.includes(code[0])) {
|
|
33
25
|
// Don't evaluate instructions, use as is.
|
|
34
26
|
evaluated = code;
|
|
@@ -93,11 +85,19 @@ export default async function evaluate(code) {
|
|
|
93
85
|
}
|
|
94
86
|
|
|
95
87
|
// To aid debugging, add the code to the result.
|
|
96
|
-
if (Object.isExtensible(result) && !isPlainObject(result)) {
|
|
88
|
+
if (Object.isExtensible(result) /* && !isPlainObject(result) */) {
|
|
97
89
|
try {
|
|
98
|
-
result[
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
if (code.location && !result[sourceSymbol]) {
|
|
91
|
+
Object.defineProperty(result, sourceSymbol, {
|
|
92
|
+
value: codeFragment(code.location),
|
|
93
|
+
enumerable: false,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!result[codeSymbol]) {
|
|
97
|
+
Object.defineProperty(result, codeSymbol, {
|
|
98
|
+
value: code,
|
|
99
|
+
enumerable: false,
|
|
100
|
+
});
|
|
101
101
|
}
|
|
102
102
|
if (!result[scopeSymbol]) {
|
|
103
103
|
Object.defineProperty(result, scopeSymbol, {
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
trailingSlash,
|
|
9
9
|
} from "@weborigami/async-tree";
|
|
10
10
|
|
|
11
|
+
/** @typedef {import("../../index.ts").ExtensionHandler} ExtensionHandler */
|
|
12
|
+
|
|
13
|
+
// Track extensions handlers for a given containing tree.
|
|
14
|
+
const handlersForContainer = new Map();
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* If the given path ends in an extension, return it. Otherwise, return the
|
|
13
18
|
* empty string.
|
|
@@ -38,18 +43,39 @@ export function extname(path) {
|
|
|
38
43
|
* @param {string} extension
|
|
39
44
|
*/
|
|
40
45
|
export async function getExtensionHandler(parent, extension) {
|
|
46
|
+
let handlers = handlersForContainer.get(parent);
|
|
47
|
+
if (handlers) {
|
|
48
|
+
if (handlers[extension]) {
|
|
49
|
+
return handlers[extension];
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
handlers = {};
|
|
53
|
+
handlersForContainer.set(parent, handlers);
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
const handlerName = `${extension.slice(1)}_handler`;
|
|
42
57
|
const parentScope = scope(parent);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
/** @type {Promise<ExtensionHandler>} */
|
|
60
|
+
let handlerPromise = parentScope
|
|
61
|
+
?.get(handlerName)
|
|
62
|
+
.then(async (extensionHandler) => {
|
|
63
|
+
if (isUnpackable(extensionHandler)) {
|
|
64
|
+
// The extension handler itself needs to be unpacked. E.g., if it's a
|
|
65
|
+
// buffer containing JavaScript file, we need to unpack it to get its
|
|
66
|
+
// default export.
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
extensionHandler = await extensionHandler.unpack();
|
|
69
|
+
}
|
|
70
|
+
// Update cache with actual handler
|
|
71
|
+
handlers[extension] = extensionHandler;
|
|
72
|
+
return extensionHandler;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Cache handler even if it's undefined so we don't look it up again
|
|
76
|
+
handlers[extension] = handlerPromise;
|
|
77
|
+
|
|
78
|
+
return handlerPromise;
|
|
53
79
|
}
|
|
54
80
|
|
|
55
81
|
/**
|
|
@@ -62,7 +88,7 @@ export async function getExtensionHandler(parent, extension) {
|
|
|
62
88
|
* @param {any} key
|
|
63
89
|
*/
|
|
64
90
|
export async function handleExtension(parent, value, key) {
|
|
65
|
-
if (isPacked(value) && isStringLike(key)) {
|
|
91
|
+
if (isPacked(value) && isStringLike(key) && value.unpack === undefined) {
|
|
66
92
|
const hasSlash = trailingSlash.has(key);
|
|
67
93
|
if (hasSlash) {
|
|
68
94
|
key = trailingSlash.remove(key);
|
|
@@ -89,10 +115,10 @@ export async function handleExtension(parent, value, key) {
|
|
|
89
115
|
const unpack = handler.unpack;
|
|
90
116
|
if (unpack) {
|
|
91
117
|
// Wrap the unpack function so its only called once per value.
|
|
92
|
-
let
|
|
118
|
+
let loadPromise;
|
|
93
119
|
value.unpack = async () => {
|
|
94
|
-
|
|
95
|
-
return
|
|
120
|
+
loadPromise ??= unpack(value, { key, parent });
|
|
121
|
+
return loadPromise;
|
|
96
122
|
};
|
|
97
123
|
}
|
|
98
124
|
}
|
package/src/runtime/ops.js
CHANGED
|
@@ -20,10 +20,18 @@ import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
|
20
20
|
import { evaluate } from "./internal.js";
|
|
21
21
|
import mergeTrees from "./mergeTrees.js";
|
|
22
22
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
23
|
+
import taggedTemplate from "./taggedTemplate.js";
|
|
23
24
|
|
|
24
25
|
// For memoizing lambda functions
|
|
25
26
|
const lambdaFnMap = new Map();
|
|
26
27
|
|
|
28
|
+
function addOpLabel(op, label) {
|
|
29
|
+
Object.defineProperty(op, "toString", {
|
|
30
|
+
value: () => label,
|
|
31
|
+
enumerable: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
/**
|
|
28
36
|
* Construct an array.
|
|
29
37
|
*
|
|
@@ -33,7 +41,26 @@ const lambdaFnMap = new Map();
|
|
|
33
41
|
export async function array(...items) {
|
|
34
42
|
return Array(...items);
|
|
35
43
|
}
|
|
36
|
-
array
|
|
44
|
+
addOpLabel(array, "«ops.array»");
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Look up the given key in the scope for the current tree the first time
|
|
48
|
+
* the key is requested, holding on to the value for future requests.
|
|
49
|
+
*
|
|
50
|
+
* @this {AsyncTree|null}
|
|
51
|
+
*/
|
|
52
|
+
export async function cache(key, cache) {
|
|
53
|
+
if (key in cache) {
|
|
54
|
+
return cache[key];
|
|
55
|
+
}
|
|
56
|
+
// First save a promise for the value
|
|
57
|
+
const promise = scope.call(this, key);
|
|
58
|
+
cache[key] = promise;
|
|
59
|
+
const value = await promise;
|
|
60
|
+
// Now update with the actual value
|
|
61
|
+
cache[key] = value;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
37
64
|
|
|
38
65
|
// The assign op is a placeholder for an assignment declaration.
|
|
39
66
|
// It is only used during parsing -- it shouldn't be executed.
|
|
@@ -48,7 +75,7 @@ export const assign = "«ops.assign»";
|
|
|
48
75
|
export async function concat(...args) {
|
|
49
76
|
return treeConcat.call(this, args);
|
|
50
77
|
}
|
|
51
|
-
concat
|
|
78
|
+
addOpLabel(concat, "«ops.concat»");
|
|
52
79
|
|
|
53
80
|
/**
|
|
54
81
|
* Find the indicated constructor in scope, then return a function which invokes
|
|
@@ -71,7 +98,7 @@ export async function constructor(...keys) {
|
|
|
71
98
|
? new constructor()
|
|
72
99
|
: new constructor(...args);
|
|
73
100
|
}
|
|
74
|
-
constructor
|
|
101
|
+
addOpLabel(constructor, "«ops.constructor»");
|
|
75
102
|
|
|
76
103
|
/**
|
|
77
104
|
* Given a protocol, a host, and a list of keys, construct an href.
|
|
@@ -144,6 +171,18 @@ async function fetchResponse(href) {
|
|
|
144
171
|
*/
|
|
145
172
|
export const getter = new String("«ops.getter»");
|
|
146
173
|
|
|
174
|
+
/**
|
|
175
|
+
* A site tree with JSON Keys via HTTPS.
|
|
176
|
+
*
|
|
177
|
+
* @this {AsyncTree|null}
|
|
178
|
+
* @param {string} host
|
|
179
|
+
* @param {...string} keys
|
|
180
|
+
*/
|
|
181
|
+
export function explorableSite(host, ...keys) {
|
|
182
|
+
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
183
|
+
}
|
|
184
|
+
addOpLabel(explorableSite, "«ops.explorableSite»");
|
|
185
|
+
|
|
147
186
|
/**
|
|
148
187
|
* Construct a files tree for the filesystem root.
|
|
149
188
|
*
|
|
@@ -171,7 +210,7 @@ export async function http(host, ...keys) {
|
|
|
171
210
|
const href = constructHref("http:", host, ...keys);
|
|
172
211
|
return fetchResponse.call(this, href);
|
|
173
212
|
}
|
|
174
|
-
http
|
|
213
|
+
addOpLabel(http, "«ops.http»");
|
|
175
214
|
|
|
176
215
|
/**
|
|
177
216
|
* Retrieve a web resource via HTTPS.
|
|
@@ -184,7 +223,7 @@ export function https(host, ...keys) {
|
|
|
184
223
|
const href = constructHref("https:", host, ...keys);
|
|
185
224
|
return fetchResponse.call(this, href);
|
|
186
225
|
}
|
|
187
|
-
https
|
|
226
|
+
addOpLabel(https, "«ops.https»");
|
|
188
227
|
|
|
189
228
|
/**
|
|
190
229
|
* Search the parent's scope -- i.e., exclude the current tree -- for the given
|
|
@@ -200,7 +239,7 @@ export async function inherited(key) {
|
|
|
200
239
|
const parentScope = scopeFn(this.parent);
|
|
201
240
|
return parentScope.get(key);
|
|
202
241
|
}
|
|
203
|
-
inherited
|
|
242
|
+
addOpLabel(inherited, "«ops.inherited»");
|
|
204
243
|
|
|
205
244
|
/**
|
|
206
245
|
* Return a function that will invoke the given code.
|
|
@@ -216,9 +255,6 @@ export function lambda(parameters, code) {
|
|
|
216
255
|
return lambdaFnMap.get(code);
|
|
217
256
|
}
|
|
218
257
|
|
|
219
|
-
// By default, the first input argument is named `_`.
|
|
220
|
-
parameters ??= ["_"];
|
|
221
|
-
|
|
222
258
|
/** @this {AsyncTree|null} */
|
|
223
259
|
async function invoke(...args) {
|
|
224
260
|
// Add arguments and @recurse to scope.
|
|
@@ -258,7 +294,15 @@ export function lambda(parameters, code) {
|
|
|
258
294
|
lambdaFnMap.set(code, invoke);
|
|
259
295
|
return invoke;
|
|
260
296
|
}
|
|
261
|
-
lambda
|
|
297
|
+
addOpLabel(lambda, "«ops.lambda");
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Return a primitive value
|
|
301
|
+
*/
|
|
302
|
+
export async function literal(value) {
|
|
303
|
+
return value;
|
|
304
|
+
}
|
|
305
|
+
addOpLabel(literal, "«ops.literal»");
|
|
262
306
|
|
|
263
307
|
/**
|
|
264
308
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
@@ -269,7 +313,7 @@ lambda.toString = () => "«ops.lambda";
|
|
|
269
313
|
export async function merge(...trees) {
|
|
270
314
|
return mergeTrees.call(this, ...trees);
|
|
271
315
|
}
|
|
272
|
-
merge
|
|
316
|
+
addOpLabel(merge, "«ops.merge»");
|
|
273
317
|
|
|
274
318
|
/**
|
|
275
319
|
* Construct an object. The keys will be the same as the given `obj`
|
|
@@ -282,19 +326,7 @@ merge.toString = () => "«ops.merge»";
|
|
|
282
326
|
export async function object(...entries) {
|
|
283
327
|
return expressionObject(entries, this);
|
|
284
328
|
}
|
|
285
|
-
object
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* A site tree with JSON Keys via HTTPS.
|
|
289
|
-
*
|
|
290
|
-
* @this {AsyncTree|null}
|
|
291
|
-
* @param {string} host
|
|
292
|
-
* @param {...string} keys
|
|
293
|
-
*/
|
|
294
|
-
export function explorableSite(host, ...keys) {
|
|
295
|
-
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
296
|
-
}
|
|
297
|
-
explorableSite.toString = () => "«ops.explorableSite»";
|
|
329
|
+
addOpLabel(object, "«ops.object»");
|
|
298
330
|
|
|
299
331
|
/**
|
|
300
332
|
* Look up the given key in the scope for the current tree.
|
|
@@ -308,7 +340,7 @@ export async function scope(key) {
|
|
|
308
340
|
const scope = scopeFn(this);
|
|
309
341
|
return scope.get(key);
|
|
310
342
|
}
|
|
311
|
-
scope
|
|
343
|
+
addOpLabel(scope, "«ops.scope»");
|
|
312
344
|
|
|
313
345
|
/**
|
|
314
346
|
* The spread operator is a placeholder during parsing. It should be replaced
|
|
@@ -319,15 +351,15 @@ export function spread(...args) {
|
|
|
319
351
|
"A compile-time spread operator wasn't converted to an object merge."
|
|
320
352
|
);
|
|
321
353
|
}
|
|
322
|
-
spread
|
|
354
|
+
addOpLabel(spread, "«ops.spread»");
|
|
323
355
|
|
|
324
356
|
/**
|
|
325
|
-
*
|
|
357
|
+
* Apply the default tagged template function.
|
|
326
358
|
*/
|
|
327
|
-
export
|
|
328
|
-
return
|
|
359
|
+
export function template(strings, ...values) {
|
|
360
|
+
return taggedTemplate(strings, values);
|
|
329
361
|
}
|
|
330
|
-
|
|
362
|
+
addOpLabel(template, "«ops.template»");
|
|
331
363
|
|
|
332
364
|
/**
|
|
333
365
|
* Traverse a path of keys through a tree.
|
|
@@ -344,7 +376,7 @@ export const traverse = Tree.traverseOrThrow;
|
|
|
344
376
|
export function treeHttp(host, ...keys) {
|
|
345
377
|
return constructSiteTree("http:", SiteTree, this, host, ...keys);
|
|
346
378
|
}
|
|
347
|
-
treeHttp
|
|
379
|
+
addOpLabel(treeHttp, "«ops.treeHttp»");
|
|
348
380
|
|
|
349
381
|
/**
|
|
350
382
|
* A website tree via HTTPS.
|
|
@@ -356,7 +388,7 @@ treeHttp.toString = () => "«ops.treeHttp»";
|
|
|
356
388
|
export function treeHttps(host, ...keys) {
|
|
357
389
|
return constructSiteTree("https:", SiteTree, this, host, ...keys);
|
|
358
390
|
}
|
|
359
|
-
treeHttps
|
|
391
|
+
addOpLabel(treeHttps, "«ops.treeHttps»");
|
|
360
392
|
|
|
361
393
|
/**
|
|
362
394
|
* If the value is packed but has an unpack method, call it and return that as
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Default JavaScript tagged template function splices strings and values
|
|
2
|
+
// together.
|
|
3
|
+
export default function defaultTemplateJoin(strings, values) {
|
|
4
|
+
let result = strings[0];
|
|
5
|
+
for (let i = 0; i < values.length; i++) {
|
|
6
|
+
result += values[i] + strings[i + 1];
|
|
7
|
+
}
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
@@ -2,6 +2,8 @@ import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
|
|
|
2
2
|
import assert from "node:assert";
|
|
3
3
|
import { describe, test } from "node:test";
|
|
4
4
|
import * as compile from "../../src/compiler/compile.js";
|
|
5
|
+
import { ops } from "../../src/runtime/internal.js";
|
|
6
|
+
import { stripCodeLocations } from "./stripCodeLocations.js";
|
|
5
7
|
|
|
6
8
|
const shared = new ObjectTree({
|
|
7
9
|
greet: (name) => `Hello, ${name}!`,
|
|
@@ -61,6 +63,51 @@ describe("compile", () => {
|
|
|
61
63
|
"escape characters with `backslash`"
|
|
62
64
|
);
|
|
63
65
|
});
|
|
66
|
+
|
|
67
|
+
test("tagged template string array is identical across calls", async () => {
|
|
68
|
+
let saved;
|
|
69
|
+
const scope = new ObjectTree({
|
|
70
|
+
tag: (strings, ...values) => {
|
|
71
|
+
assert.deepEqual(strings, ["Hello, ", "!"]);
|
|
72
|
+
if (saved) {
|
|
73
|
+
assert.equal(strings, saved);
|
|
74
|
+
} else {
|
|
75
|
+
saved = strings;
|
|
76
|
+
}
|
|
77
|
+
return strings[0] + values[0] + strings[1];
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const fn = compile.expression("=tag`Hello, ${_}!`");
|
|
81
|
+
const templateFn = await fn.call(null);
|
|
82
|
+
const alice = await templateFn.call(scope, "Alice");
|
|
83
|
+
assert.equal(alice, "Hello, Alice!");
|
|
84
|
+
const bob = await templateFn.call(scope, "Bob");
|
|
85
|
+
assert.equal(bob, "Hello, Bob!");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("converts non-local ops.scope calls to ops.cache", async () => {
|
|
89
|
+
const expression = `
|
|
90
|
+
(name) => {
|
|
91
|
+
a: 1
|
|
92
|
+
b: a // local, should be left as ops.scope
|
|
93
|
+
c: nonLocal // non-local, should be converted to ops.cache
|
|
94
|
+
d: name // local, should be left as ops.scope
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
const fn = compile.expression(expression);
|
|
98
|
+
const code = fn.code;
|
|
99
|
+
assert.deepEqual(stripCodeLocations(code), [
|
|
100
|
+
ops.lambda,
|
|
101
|
+
["name"],
|
|
102
|
+
[
|
|
103
|
+
ops.object,
|
|
104
|
+
["a", [ops.literal, 1]],
|
|
105
|
+
["b", [ops.scope, "a"]],
|
|
106
|
+
["c", [ops.cache, "nonLocal", {}]],
|
|
107
|
+
["d", [ops.scope, "name"]],
|
|
108
|
+
],
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
64
111
|
});
|
|
65
112
|
|
|
66
113
|
async function assertCompile(text, expected) {
|