@weborigami/language 0.0.68 → 0.0.70
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 +1 -40
- package/src/compiler/origami.pegjs +41 -28
- package/src/compiler/parse.js +287 -193
- package/src/compiler/parserHelpers.js +51 -28
- package/src/runtime/WatchFilesMixin.js +1 -0
- package/src/runtime/evaluate.js +15 -15
- package/src/runtime/ops.js +54 -38
- package/src/runtime/symbols.js +3 -0
- package/src/runtime/taggedTemplate.js +9 -0
- package/test/compiler/compile.test.js +21 -0
- package/test/compiler/parse.test.js +160 -171
- package/test/runtime/taggedTemplate.test.js +10 -0
|
@@ -13,8 +13,28 @@ export function annotate(parseResult, location) {
|
|
|
13
13
|
return parseResult;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// The indicated code is being used to define a property named by the given key.
|
|
17
|
+
// Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
18
|
+
// infinite recursion.
|
|
19
|
+
function avoidRecursivePropertyCalls(code, key) {
|
|
20
|
+
if (!(code instanceof Array)) {
|
|
21
|
+
return code;
|
|
22
|
+
}
|
|
23
|
+
let modified;
|
|
24
|
+
if (code[0] === ops.scope && code[1] === key) {
|
|
25
|
+
// Rewrite to avoid recursion
|
|
26
|
+
modified = [ops.inherited, key];
|
|
27
|
+
} else {
|
|
28
|
+
// Process any nested code
|
|
29
|
+
modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
|
|
30
|
+
}
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
modified.location = code.location;
|
|
33
|
+
return modified;
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
// Return true if the code will generate an async object.
|
|
17
|
-
function
|
|
37
|
+
function isCodeForAsyncObject(code) {
|
|
18
38
|
if (!(code instanceof Array)) {
|
|
19
39
|
return false;
|
|
20
40
|
}
|
|
@@ -120,28 +140,29 @@ export function makeObject(entries, op) {
|
|
|
120
140
|
|
|
121
141
|
for (let [key, value] of entries) {
|
|
122
142
|
if (key === ops.spread) {
|
|
123
|
-
//
|
|
143
|
+
// Spread entry; accumulate
|
|
124
144
|
if (currentEntries.length > 0) {
|
|
125
145
|
spreads.push([op, ...currentEntries]);
|
|
126
146
|
currentEntries = [];
|
|
127
147
|
}
|
|
128
148
|
spreads.push(value);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
value instanceof Array &&
|
|
132
|
-
value[0] === ops.getter &&
|
|
133
|
-
value[1] instanceof Array &&
|
|
134
|
-
value[1][0] === ops.primitive
|
|
135
|
-
) {
|
|
136
|
-
// Simplify a getter for a primitive value to a regular property
|
|
137
|
-
value = value[1];
|
|
138
|
-
} else if (isAsyncObject(value)) {
|
|
139
|
-
// Add a trailing slash to key if value is an async object
|
|
140
|
-
key = key + "/";
|
|
141
|
-
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
if (
|
|
153
|
+
value instanceof Array &&
|
|
154
|
+
value[0] === ops.getter &&
|
|
155
|
+
value[1] instanceof Array &&
|
|
156
|
+
value[1][0] === ops.literal
|
|
157
|
+
) {
|
|
158
|
+
// Simplify a getter for a primitive value to a regular property
|
|
159
|
+
value = value[1];
|
|
160
|
+
} else if (isCodeForAsyncObject(value)) {
|
|
161
|
+
// Add a trailing slash to key to indicate value is a subtree
|
|
162
|
+
key = key + "/";
|
|
144
163
|
}
|
|
164
|
+
|
|
165
|
+
currentEntries.push([key, value]);
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
// Finish any current entries.
|
|
@@ -170,16 +191,18 @@ export function makePipeline(steps) {
|
|
|
170
191
|
return value;
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
// Define a property on an object.
|
|
195
|
+
export function makeProperty(key, value) {
|
|
196
|
+
const modified = avoidRecursivePropertyCalls(value, key);
|
|
197
|
+
return [key, modified];
|
|
198
|
+
}
|
|
199
|
+
|
|
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];
|
|
185
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, {
|
package/src/runtime/ops.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SiteTree,
|
|
10
10
|
Tree,
|
|
11
11
|
isUnpackable,
|
|
12
|
+
pathFromKeys,
|
|
12
13
|
scope as scopeFn,
|
|
13
14
|
trailingSlash,
|
|
14
15
|
concat as treeConcat,
|
|
@@ -19,10 +20,18 @@ import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
|
|
|
19
20
|
import { evaluate } from "./internal.js";
|
|
20
21
|
import mergeTrees from "./mergeTrees.js";
|
|
21
22
|
import OrigamiFiles from "./OrigamiFiles.js";
|
|
23
|
+
import taggedTemplate from "./taggedTemplate.js";
|
|
22
24
|
|
|
23
25
|
// For memoizing lambda functions
|
|
24
26
|
const lambdaFnMap = new Map();
|
|
25
27
|
|
|
28
|
+
function addOpLabel(op, label) {
|
|
29
|
+
Object.defineProperty(op, "toString", {
|
|
30
|
+
value: () => label,
|
|
31
|
+
enumerable: false,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Construct an array.
|
|
28
37
|
*
|
|
@@ -32,7 +41,7 @@ const lambdaFnMap = new Map();
|
|
|
32
41
|
export async function array(...items) {
|
|
33
42
|
return Array(...items);
|
|
34
43
|
}
|
|
35
|
-
array
|
|
44
|
+
addOpLabel(array, "«ops.array»");
|
|
36
45
|
|
|
37
46
|
// The assign op is a placeholder for an assignment declaration.
|
|
38
47
|
// It is only used during parsing -- it shouldn't be executed.
|
|
@@ -47,7 +56,7 @@ export const assign = "«ops.assign»";
|
|
|
47
56
|
export async function concat(...args) {
|
|
48
57
|
return treeConcat.call(this, args);
|
|
49
58
|
}
|
|
50
|
-
concat
|
|
59
|
+
addOpLabel(concat, "«ops.concat»");
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
62
|
* Find the indicated constructor in scope, then return a function which invokes
|
|
@@ -70,19 +79,18 @@ export async function constructor(...keys) {
|
|
|
70
79
|
? new constructor()
|
|
71
80
|
: new constructor(...args);
|
|
72
81
|
}
|
|
73
|
-
constructor
|
|
82
|
+
addOpLabel(constructor, "«ops.constructor»");
|
|
74
83
|
|
|
75
84
|
/**
|
|
76
85
|
* Given a protocol, a host, and a list of keys, construct an href.
|
|
77
86
|
*
|
|
78
87
|
* @param {string} protocol
|
|
79
88
|
* @param {string} host
|
|
80
|
-
* @param {
|
|
89
|
+
* @param {string[]} keys
|
|
81
90
|
*/
|
|
82
91
|
function constructHref(protocol, host, ...keys) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let href = [host, ...baseKeys].join("/");
|
|
92
|
+
const path = pathFromKeys(keys);
|
|
93
|
+
let href = [host, path].join("/");
|
|
86
94
|
if (!href.startsWith(protocol)) {
|
|
87
95
|
if (!href.startsWith("//")) {
|
|
88
96
|
href = `//${href}`;
|
|
@@ -99,7 +107,7 @@ function constructHref(protocol, host, ...keys) {
|
|
|
99
107
|
* @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
|
|
100
108
|
* @param {AsyncTree|null} parent
|
|
101
109
|
* @param {string} host
|
|
102
|
-
* @param {
|
|
110
|
+
* @param {string[]} keys
|
|
103
111
|
*/
|
|
104
112
|
async function constructSiteTree(protocol, treeClass, parent, host, ...keys) {
|
|
105
113
|
// If the last key doesn't end in a slash, remove it for now.
|
|
@@ -144,6 +152,18 @@ async function fetchResponse(href) {
|
|
|
144
152
|
*/
|
|
145
153
|
export const getter = new String("«ops.getter»");
|
|
146
154
|
|
|
155
|
+
/**
|
|
156
|
+
* A site tree with JSON Keys via HTTPS.
|
|
157
|
+
*
|
|
158
|
+
* @this {AsyncTree|null}
|
|
159
|
+
* @param {string} host
|
|
160
|
+
* @param {...string} keys
|
|
161
|
+
*/
|
|
162
|
+
export function explorableSite(host, ...keys) {
|
|
163
|
+
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
164
|
+
}
|
|
165
|
+
addOpLabel(explorableSite, "«ops.explorableSite»");
|
|
166
|
+
|
|
147
167
|
/**
|
|
148
168
|
* Construct a files tree for the filesystem root.
|
|
149
169
|
*
|
|
@@ -165,26 +185,26 @@ export async function filesRoot() {
|
|
|
165
185
|
*
|
|
166
186
|
* @this {AsyncTree|null}
|
|
167
187
|
* @param {string} host
|
|
168
|
-
* @param {...string
|
|
188
|
+
* @param {...string} keys
|
|
169
189
|
*/
|
|
170
190
|
export async function http(host, ...keys) {
|
|
171
191
|
const href = constructHref("http:", host, ...keys);
|
|
172
192
|
return fetchResponse.call(this, href);
|
|
173
193
|
}
|
|
174
|
-
http
|
|
194
|
+
addOpLabel(http, "«ops.http»");
|
|
175
195
|
|
|
176
196
|
/**
|
|
177
197
|
* Retrieve a web resource via HTTPS.
|
|
178
198
|
*
|
|
179
199
|
* @this {AsyncTree|null}
|
|
180
200
|
* @param {string} host
|
|
181
|
-
* @param {...string
|
|
201
|
+
* @param {...string} keys
|
|
182
202
|
*/
|
|
183
203
|
export function https(host, ...keys) {
|
|
184
204
|
const href = constructHref("https:", host, ...keys);
|
|
185
205
|
return fetchResponse.call(this, href);
|
|
186
206
|
}
|
|
187
|
-
https
|
|
207
|
+
addOpLabel(https, "«ops.https»");
|
|
188
208
|
|
|
189
209
|
/**
|
|
190
210
|
* Search the parent's scope -- i.e., exclude the current tree -- for the given
|
|
@@ -200,7 +220,7 @@ export async function inherited(key) {
|
|
|
200
220
|
const parentScope = scopeFn(this.parent);
|
|
201
221
|
return parentScope.get(key);
|
|
202
222
|
}
|
|
203
|
-
inherited
|
|
223
|
+
addOpLabel(inherited, "«ops.inherited»");
|
|
204
224
|
|
|
205
225
|
/**
|
|
206
226
|
* Return a function that will invoke the given code.
|
|
@@ -258,7 +278,15 @@ export function lambda(parameters, code) {
|
|
|
258
278
|
lambdaFnMap.set(code, invoke);
|
|
259
279
|
return invoke;
|
|
260
280
|
}
|
|
261
|
-
lambda
|
|
281
|
+
addOpLabel(lambda, "«ops.lambda");
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Return a primitive value
|
|
285
|
+
*/
|
|
286
|
+
export async function literal(value) {
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
addOpLabel(literal, "«ops.literal»");
|
|
262
290
|
|
|
263
291
|
/**
|
|
264
292
|
* Merge the given trees. If they are all plain objects, return a plain object.
|
|
@@ -269,7 +297,7 @@ lambda.toString = () => "«ops.lambda";
|
|
|
269
297
|
export async function merge(...trees) {
|
|
270
298
|
return mergeTrees.call(this, ...trees);
|
|
271
299
|
}
|
|
272
|
-
merge
|
|
300
|
+
addOpLabel(merge, "«ops.merge»");
|
|
273
301
|
|
|
274
302
|
/**
|
|
275
303
|
* Construct an object. The keys will be the same as the given `obj`
|
|
@@ -282,19 +310,7 @@ merge.toString = () => "«ops.merge»";
|
|
|
282
310
|
export async function object(...entries) {
|
|
283
311
|
return expressionObject(entries, this);
|
|
284
312
|
}
|
|
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|Symbol} keys
|
|
293
|
-
*/
|
|
294
|
-
export function explorableSite(host, ...keys) {
|
|
295
|
-
return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
|
|
296
|
-
}
|
|
297
|
-
explorableSite.toString = () => "«ops.explorableSite»";
|
|
313
|
+
addOpLabel(object, "«ops.object»");
|
|
298
314
|
|
|
299
315
|
/**
|
|
300
316
|
* Look up the given key in the scope for the current tree.
|
|
@@ -308,7 +324,7 @@ export async function scope(key) {
|
|
|
308
324
|
const scope = scopeFn(this);
|
|
309
325
|
return scope.get(key);
|
|
310
326
|
}
|
|
311
|
-
scope
|
|
327
|
+
addOpLabel(scope, "«ops.scope»");
|
|
312
328
|
|
|
313
329
|
/**
|
|
314
330
|
* The spread operator is a placeholder during parsing. It should be replaced
|
|
@@ -319,15 +335,15 @@ export function spread(...args) {
|
|
|
319
335
|
"A compile-time spread operator wasn't converted to an object merge."
|
|
320
336
|
);
|
|
321
337
|
}
|
|
322
|
-
spread
|
|
338
|
+
addOpLabel(spread, "«ops.spread»");
|
|
323
339
|
|
|
324
340
|
/**
|
|
325
|
-
*
|
|
341
|
+
* Apply the default tagged template function.
|
|
326
342
|
*/
|
|
327
|
-
export
|
|
328
|
-
return
|
|
343
|
+
export function template(strings, ...values) {
|
|
344
|
+
return taggedTemplate(strings, values);
|
|
329
345
|
}
|
|
330
|
-
|
|
346
|
+
addOpLabel(template, "«ops.template»");
|
|
331
347
|
|
|
332
348
|
/**
|
|
333
349
|
* Traverse a path of keys through a tree.
|
|
@@ -339,24 +355,24 @@ export const traverse = Tree.traverseOrThrow;
|
|
|
339
355
|
*
|
|
340
356
|
* @this {AsyncTree|null}
|
|
341
357
|
* @param {string} host
|
|
342
|
-
* @param {...string
|
|
358
|
+
* @param {...string} keys
|
|
343
359
|
*/
|
|
344
360
|
export function treeHttp(host, ...keys) {
|
|
345
361
|
return constructSiteTree("http:", SiteTree, this, host, ...keys);
|
|
346
362
|
}
|
|
347
|
-
treeHttp
|
|
363
|
+
addOpLabel(treeHttp, "«ops.treeHttp»");
|
|
348
364
|
|
|
349
365
|
/**
|
|
350
366
|
* A website tree via HTTPS.
|
|
351
367
|
*
|
|
352
368
|
* @this {AsyncTree|null}
|
|
353
369
|
* @param {string} host
|
|
354
|
-
* @param {...string
|
|
370
|
+
* @param {...string} keys
|
|
355
371
|
*/
|
|
356
372
|
export function treeHttps(host, ...keys) {
|
|
357
373
|
return constructSiteTree("https:", SiteTree, this, host, ...keys);
|
|
358
374
|
}
|
|
359
|
-
treeHttps
|
|
375
|
+
addOpLabel(treeHttps, "«ops.treeHttps»");
|
|
360
376
|
|
|
361
377
|
/**
|
|
362
378
|
* 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
|
+
}
|
|
@@ -61,6 +61,27 @@ describe("compile", () => {
|
|
|
61
61
|
"escape characters with `backslash`"
|
|
62
62
|
);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
test("tagged template string array is identical across calls", async () => {
|
|
66
|
+
let saved;
|
|
67
|
+
const scope = new ObjectTree({
|
|
68
|
+
tag: (strings, ...values) => {
|
|
69
|
+
assert.deepEqual(strings, ["Hello, ", "!"]);
|
|
70
|
+
if (saved) {
|
|
71
|
+
assert.equal(strings, saved);
|
|
72
|
+
} else {
|
|
73
|
+
saved = strings;
|
|
74
|
+
}
|
|
75
|
+
return strings[0] + values[0] + strings[1];
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const fn = compile.expression("=tag`Hello, ${_}!`");
|
|
79
|
+
const templateFn = await fn.call(null);
|
|
80
|
+
const alice = await templateFn.call(scope, "Alice");
|
|
81
|
+
assert.equal(alice, "Hello, Alice!");
|
|
82
|
+
const bob = await templateFn.call(scope, "Bob");
|
|
83
|
+
assert.equal(bob, "Hello, Bob!");
|
|
84
|
+
});
|
|
64
85
|
});
|
|
65
86
|
|
|
66
87
|
async function assertCompile(text, expected) {
|