@weborigami/language 0.3.3-jse.3 → 0.3.3
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 -3
- package/package.json +3 -3
- package/src/compiler/compile.js +2 -10
- package/src/compiler/optimize.js +93 -147
- package/src/compiler/origami.pegjs +67 -171
- package/src/compiler/parse.js +946 -1810
- package/src/compiler/parserHelpers.js +58 -159
- package/src/runtime/HandleExtensionsTransform.js +1 -10
- package/src/runtime/evaluate.js +35 -28
- package/src/runtime/expressionObject.js +4 -4
- package/src/runtime/handlers.js +54 -18
- package/src/runtime/mergeTrees.js +5 -0
- package/src/runtime/ops.js +156 -101
- package/src/runtime/symbols.js +0 -1
- package/src/runtime/{templateIndent.js → taggedTemplateIndent.js} +2 -2
- package/test/compiler/codeHelpers.js +1 -3
- package/test/compiler/compile.test.js +27 -52
- package/test/compiler/optimize.test.js +23 -92
- package/test/compiler/parse.test.js +342 -574
- package/test/runtime/evaluate.test.js +20 -4
- package/test/runtime/expressionObject.test.js +4 -5
- package/test/runtime/handlers.test.js +10 -19
- package/test/runtime/mergeTrees.test.js +5 -0
- package/test/runtime/ops.test.js +82 -103
- package/test/runtime/taggedTemplateIndent.test.js +1 -1
- package/src/runtime/getHandlers.js +0 -10
- package/src/runtime/jsGlobals.js +0 -99
- package/src/runtime/templateStandard.js +0 -13
- package/test/runtime/templateText.test.js +0 -18
|
@@ -15,12 +15,8 @@ const YAML = YAMLModule.default ?? YAMLModule.YAML;
|
|
|
15
15
|
/** @typedef {import("../../index.ts").CodeLocation} CodeLocation */
|
|
16
16
|
/** @typedef {import("../../index.ts").Code} Code */
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
export const
|
|
20
|
-
global: Symbol("global"), // Global reference
|
|
21
|
-
traverse: Symbol("traverse"), // Continuation of path traversal
|
|
22
|
-
reference: Symbol("reference"), // Reference to local, scope, or global
|
|
23
|
-
};
|
|
18
|
+
// Marker for a reference that may be a builtin or a scope reference
|
|
19
|
+
export const undetermined = Symbol("undetermined");
|
|
24
20
|
|
|
25
21
|
const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
26
22
|
|
|
@@ -41,7 +37,7 @@ export function annotate(code, location) {
|
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
/**
|
|
44
|
-
* In the given code, replace all
|
|
40
|
+
* In the given code, replace all scope refernces to the given name with the
|
|
45
41
|
* given macro code.
|
|
46
42
|
*
|
|
47
43
|
* @param {AnnotatedCode} code
|
|
@@ -53,16 +49,8 @@ export function applyMacro(code, name, macro) {
|
|
|
53
49
|
return code;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
code[0] &&
|
|
60
|
-
code[0][0] === markers.reference &&
|
|
61
|
-
code[0][1] instanceof Array &&
|
|
62
|
-
code[0][1][0] === ops.literal &&
|
|
63
|
-
code[0][1][1] === name &&
|
|
64
|
-
code[1] === undefined
|
|
65
|
-
) {
|
|
52
|
+
const [fn, ...args] = code;
|
|
53
|
+
if (fn === ops.scope && args[0] === name) {
|
|
66
54
|
return macro;
|
|
67
55
|
}
|
|
68
56
|
|
|
@@ -72,7 +60,7 @@ export function applyMacro(code, name, macro) {
|
|
|
72
60
|
|
|
73
61
|
/**
|
|
74
62
|
* The indicated code is being used to define a property named by the given key.
|
|
75
|
-
* Rewrite any [
|
|
63
|
+
* Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
|
|
76
64
|
* infinite recursion.
|
|
77
65
|
*
|
|
78
66
|
* @param {AnnotatedCode} code
|
|
@@ -85,12 +73,11 @@ function avoidRecursivePropertyCalls(code, key) {
|
|
|
85
73
|
/** @type {Code} */
|
|
86
74
|
let modified;
|
|
87
75
|
if (
|
|
88
|
-
code[0]
|
|
89
|
-
code[
|
|
90
|
-
trailingSlash.remove(code[1][1]) === trailingSlash.remove(key)
|
|
76
|
+
code[0] === ops.scope &&
|
|
77
|
+
trailingSlash.remove(code[1]) === trailingSlash.remove(key)
|
|
91
78
|
) {
|
|
92
79
|
// Rewrite to avoid recursion
|
|
93
|
-
modified = [ops.inherited, code[1]
|
|
80
|
+
modified = [ops.inherited, code[1]];
|
|
94
81
|
} else if (
|
|
95
82
|
code[0] === ops.lambda &&
|
|
96
83
|
code[1].some((param) => param[1] === key)
|
|
@@ -105,13 +92,13 @@ function avoidRecursivePropertyCalls(code, key) {
|
|
|
105
92
|
}
|
|
106
93
|
|
|
107
94
|
/**
|
|
108
|
-
* Downgrade a potential
|
|
95
|
+
* Downgrade a potential builtin reference to a scope reference.
|
|
109
96
|
*
|
|
110
97
|
* @param {AnnotatedCode} code
|
|
111
98
|
*/
|
|
112
99
|
export function downgradeReference(code) {
|
|
113
|
-
if (code && code.length === 2 && code[0] ===
|
|
114
|
-
return annotate([
|
|
100
|
+
if (code && code.length === 2 && code[0] === undetermined) {
|
|
101
|
+
return annotate([ops.scope, code[1]], code.location);
|
|
115
102
|
} else {
|
|
116
103
|
return code;
|
|
117
104
|
}
|
|
@@ -152,7 +139,7 @@ export function makeArray(entries, location) {
|
|
|
152
139
|
|
|
153
140
|
let result;
|
|
154
141
|
if (spreads.length > 1) {
|
|
155
|
-
result = [ops.
|
|
142
|
+
result = [ops.merge, ...spreads];
|
|
156
143
|
} else if (spreads.length === 1) {
|
|
157
144
|
result = spreads[0];
|
|
158
145
|
} else {
|
|
@@ -209,7 +196,7 @@ export function makeBinaryOperation(left, [operatorToken, right]) {
|
|
|
209
196
|
* @param {AnnotatedCode} target
|
|
210
197
|
* @param {any[]} args
|
|
211
198
|
*/
|
|
212
|
-
export function makeCall(target, args
|
|
199
|
+
export function makeCall(target, args) {
|
|
213
200
|
if (!(target instanceof Array)) {
|
|
214
201
|
const error = new SyntaxError(`Can't call this like a function: ${target}`);
|
|
215
202
|
/** @type {any} */ (error).location = /** @type {any} */ (target).location;
|
|
@@ -217,56 +204,38 @@ export function makeCall(target, args, mode) {
|
|
|
217
204
|
}
|
|
218
205
|
|
|
219
206
|
let fnCall;
|
|
220
|
-
|
|
221
|
-
if (op === markers.traverse || op === ops.optionalTraverse) {
|
|
207
|
+
if (args[0] === ops.traverse) {
|
|
222
208
|
let tree = target;
|
|
223
209
|
|
|
224
|
-
if (tree[0] ===
|
|
225
|
-
//
|
|
226
|
-
tree
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// reference or global where all the args are literals.
|
|
231
|
-
const extend =
|
|
232
|
-
(tree[0] === markers.reference ||
|
|
233
|
-
(tree[0] instanceof Array && tree[0][0] === markers.global)) &&
|
|
234
|
-
!tree
|
|
235
|
-
.slice(1)
|
|
236
|
-
.some((arg) => !(arg instanceof Array && arg[0] === ops.literal));
|
|
237
|
-
if (extend) {
|
|
238
|
-
fnCall = tree;
|
|
239
|
-
// If last key doesn't end with slash, add one
|
|
240
|
-
const last = tree.at(-1);
|
|
241
|
-
if (last instanceof Array && last[0] === ops.literal) {
|
|
242
|
-
last[1] = trailingSlash.add(last[1]);
|
|
210
|
+
if (tree[0] === undetermined) {
|
|
211
|
+
// In a traversal, downgrade ops.builtin references to ops.scope
|
|
212
|
+
tree = downgradeReference(tree);
|
|
213
|
+
if (tree[0] === ops.scope && !trailingSlash.has(tree[1])) {
|
|
214
|
+
// Target didn't parse with a trailing slash; add one
|
|
215
|
+
tree[1] = trailingSlash.add(tree[1]);
|
|
243
216
|
}
|
|
244
|
-
} else {
|
|
245
|
-
fnCall = [tree];
|
|
246
217
|
}
|
|
247
218
|
|
|
248
219
|
if (args.length > 1) {
|
|
249
220
|
// Regular traverse
|
|
250
221
|
const keys = args.slice(1);
|
|
251
|
-
fnCall.
|
|
252
|
-
} else
|
|
222
|
+
fnCall = [ops.traverse, tree, ...keys];
|
|
223
|
+
} else {
|
|
253
224
|
// Traverse without arguments equates to unpack
|
|
254
225
|
fnCall = [ops.unpack, tree];
|
|
255
|
-
} else {
|
|
256
|
-
fnCall = tree;
|
|
257
226
|
}
|
|
258
|
-
} else if (
|
|
227
|
+
} else if (args[0] === ops.template) {
|
|
259
228
|
// Tagged template
|
|
260
229
|
const strings = args[1];
|
|
261
230
|
const values = args.slice(2);
|
|
262
231
|
fnCall = makeTaggedTemplateCall(
|
|
263
|
-
upgradeReference(target
|
|
232
|
+
upgradeReference(target),
|
|
264
233
|
strings,
|
|
265
234
|
...values
|
|
266
235
|
);
|
|
267
236
|
} else {
|
|
268
237
|
// Function call with explicit or implicit parentheses
|
|
269
|
-
fnCall = [upgradeReference(target
|
|
238
|
+
fnCall = [upgradeReference(target), ...args];
|
|
270
239
|
}
|
|
271
240
|
|
|
272
241
|
// Create a location spanning the newly-constructed function call.
|
|
@@ -306,94 +275,6 @@ export function makeDeferredArguments(args) {
|
|
|
306
275
|
});
|
|
307
276
|
}
|
|
308
277
|
|
|
309
|
-
export function makeDocument(mode, front, body, location) {
|
|
310
|
-
// In order for template expressions to see the front matter properties,
|
|
311
|
-
// we translate the top-level front properties to object entries.
|
|
312
|
-
const entries = Object.entries(front).map(([key, value]) =>
|
|
313
|
-
annotate([key, annotate([ops.literal, value], location)], location)
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// Add an entry for the body
|
|
317
|
-
const bodyKey = mode === "jse" ? "_body" : "@text";
|
|
318
|
-
entries.push(annotate([bodyKey, body], location));
|
|
319
|
-
|
|
320
|
-
// Return the code for the document object
|
|
321
|
-
return annotate([ops.object, ...entries], location);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function makeJsPropertyAccess(expression, property) {
|
|
325
|
-
const location = {
|
|
326
|
-
source: expression.location.source,
|
|
327
|
-
start: expression.location.start,
|
|
328
|
-
end: property.location.end,
|
|
329
|
-
};
|
|
330
|
-
return annotate([expression, property], location);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* From the given spreads within an object spread, return the merge.
|
|
335
|
-
*
|
|
336
|
-
* Example:
|
|
337
|
-
*
|
|
338
|
-
* {
|
|
339
|
-
* x = { a: 1 }
|
|
340
|
-
* …x
|
|
341
|
-
* y = x
|
|
342
|
-
* }
|
|
343
|
-
*
|
|
344
|
-
* will be treated as:
|
|
345
|
-
*
|
|
346
|
-
* {
|
|
347
|
-
* x = { a: 1 }
|
|
348
|
-
* y = x
|
|
349
|
-
* _result: {
|
|
350
|
-
* x
|
|
351
|
-
* …x
|
|
352
|
-
* y
|
|
353
|
-
* }
|
|
354
|
-
* }.result
|
|
355
|
-
*
|
|
356
|
-
* @param {*} spreads
|
|
357
|
-
* @param {CodeLocation} location
|
|
358
|
-
*/
|
|
359
|
-
function makeMerge(spreads, location) {
|
|
360
|
-
const topEntries = [];
|
|
361
|
-
const resultEntries = [];
|
|
362
|
-
for (const spread of spreads) {
|
|
363
|
-
if (spread[0] === ops.object) {
|
|
364
|
-
topEntries.push(...spread.slice(1));
|
|
365
|
-
// Also add an object to the result with indirect references
|
|
366
|
-
const indirectEntries = spread.slice(1).map((entry) => {
|
|
367
|
-
const [key] = entry;
|
|
368
|
-
const context = annotate([ops.context, 1], entry.location);
|
|
369
|
-
const reference = annotate([context, key], entry.location);
|
|
370
|
-
const getter = annotate([ops.getter, reference], entry.location);
|
|
371
|
-
return annotate([key, getter], entry.location);
|
|
372
|
-
});
|
|
373
|
-
const indirectObject = annotate(
|
|
374
|
-
[ops.object, ...indirectEntries],
|
|
375
|
-
location
|
|
376
|
-
);
|
|
377
|
-
resultEntries.push(indirectObject);
|
|
378
|
-
} else {
|
|
379
|
-
resultEntries.push(spread);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Merge to create result
|
|
384
|
-
const result = annotate([ops.merge, ...resultEntries], location);
|
|
385
|
-
|
|
386
|
-
// Add the result to the top-level object as _result
|
|
387
|
-
topEntries.push(annotate(["_result", result], location));
|
|
388
|
-
|
|
389
|
-
// Construct the top-level object
|
|
390
|
-
const topObject = annotate([ops.object, ...topEntries], location);
|
|
391
|
-
|
|
392
|
-
// Get the _result property
|
|
393
|
-
const code = annotate([topObject, "_result"], location);
|
|
394
|
-
return code;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
278
|
/**
|
|
398
279
|
* Make an object.
|
|
399
280
|
*
|
|
@@ -450,7 +331,7 @@ export function makeObject(entries, location) {
|
|
|
450
331
|
let code;
|
|
451
332
|
if (spreads.length > 1) {
|
|
452
333
|
// Merge multiple spreads
|
|
453
|
-
code =
|
|
334
|
+
code = [ops.merge, ...spreads];
|
|
454
335
|
} else if (spreads.length === 1) {
|
|
455
336
|
// A single spread can just be the object
|
|
456
337
|
code = spreads[0];
|
|
@@ -467,11 +348,10 @@ export function makeObject(entries, location) {
|
|
|
467
348
|
*
|
|
468
349
|
* @param {AnnotatedCode} arg
|
|
469
350
|
* @param {AnnotatedCode} fn
|
|
470
|
-
* @param {string} mode
|
|
471
351
|
*/
|
|
472
|
-
export function makePipeline(arg, fn
|
|
473
|
-
const upgraded = upgradeReference(fn
|
|
474
|
-
const result = makeCall(upgraded, [arg]
|
|
352
|
+
export function makePipeline(arg, fn) {
|
|
353
|
+
const upgraded = upgradeReference(fn);
|
|
354
|
+
const result = makeCall(upgraded, [arg]);
|
|
475
355
|
const source = fn.location.source;
|
|
476
356
|
let start = arg.location.start;
|
|
477
357
|
let end = fn.location.end;
|
|
@@ -484,6 +364,30 @@ export function makeProperty(key, value) {
|
|
|
484
364
|
return [key, modified];
|
|
485
365
|
}
|
|
486
366
|
|
|
367
|
+
export function makeJsPropertyAccess(expression, property) {
|
|
368
|
+
const location = {
|
|
369
|
+
source: expression.location.source,
|
|
370
|
+
start: expression.location.start,
|
|
371
|
+
end: property.location.end,
|
|
372
|
+
};
|
|
373
|
+
return annotate([expression, property], location);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function makeReference(identifier) {
|
|
377
|
+
// We can't know for sure that an identifier is a builtin reference until we
|
|
378
|
+
// see whether it's being called as a function.
|
|
379
|
+
let op;
|
|
380
|
+
if (builtinRegex.test(identifier)) {
|
|
381
|
+
op = identifier.endsWith(":")
|
|
382
|
+
? // Namespace is always a builtin reference
|
|
383
|
+
ops.builtin
|
|
384
|
+
: undetermined;
|
|
385
|
+
} else {
|
|
386
|
+
op = ops.scope;
|
|
387
|
+
}
|
|
388
|
+
return [op, identifier];
|
|
389
|
+
}
|
|
390
|
+
|
|
487
391
|
/**
|
|
488
392
|
* Make a tagged template call
|
|
489
393
|
*
|
|
@@ -591,7 +495,7 @@ export function makeYamlObject(text, location) {
|
|
|
591
495
|
throw error;
|
|
592
496
|
}
|
|
593
497
|
|
|
594
|
-
return parsed;
|
|
498
|
+
return annotate([ops.literal, parsed], location);
|
|
595
499
|
}
|
|
596
500
|
|
|
597
501
|
/**
|
|
@@ -599,14 +503,9 @@ export function makeYamlObject(text, location) {
|
|
|
599
503
|
*
|
|
600
504
|
* @param {AnnotatedCode} code
|
|
601
505
|
*/
|
|
602
|
-
export function upgradeReference(code
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
code.length === 2 &&
|
|
606
|
-
code[0] === markers.reference &&
|
|
607
|
-
builtinRegex.exec(code[1][1])
|
|
608
|
-
) {
|
|
609
|
-
const result = [markers.global, code[1][1]];
|
|
506
|
+
export function upgradeReference(code) {
|
|
507
|
+
if (code.length === 2 && code[0] === undetermined) {
|
|
508
|
+
const result = [ops.builtin, code[1]];
|
|
610
509
|
return annotate(result, code.location);
|
|
611
510
|
} else {
|
|
612
511
|
return code;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import getHandlers from "./getHandlers.js";
|
|
2
1
|
import { handleExtension } from "./handlers.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -10,17 +9,9 @@ import { handleExtension } from "./handlers.js";
|
|
|
10
9
|
*/
|
|
11
10
|
export default function HandleExtensionsTransform(Base) {
|
|
12
11
|
return class FileLoaders extends Base {
|
|
13
|
-
constructor(...args) {
|
|
14
|
-
super(...args);
|
|
15
|
-
|
|
16
|
-
// Callers should set this to the set of supported extension handlers
|
|
17
|
-
this.handlers = null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
12
|
async get(key) {
|
|
21
13
|
const value = await super.get(key);
|
|
22
|
-
|
|
23
|
-
return handleExtension(this, value, key, handlers);
|
|
14
|
+
return handleExtension(this, value, key);
|
|
24
15
|
}
|
|
25
16
|
};
|
|
26
17
|
}
|
package/src/runtime/evaluate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Tree, isUnpackable } from "@weborigami/async-tree";
|
|
1
|
+
import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
|
|
2
2
|
import codeFragment from "./codeFragment.js";
|
|
3
|
+
import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Evaluate the given code and return the result.
|
|
@@ -51,7 +52,7 @@ export default async function evaluate(code) {
|
|
|
51
52
|
result =
|
|
52
53
|
fn instanceof Function
|
|
53
54
|
? await fn.call(tree, ...args) // Invoke the function
|
|
54
|
-
: await Tree.traverseOrThrow
|
|
55
|
+
: await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
|
|
55
56
|
} catch (/** @type {any} */ error) {
|
|
56
57
|
if (!error.location) {
|
|
57
58
|
// Attach the location of the code we tried to evaluate.
|
|
@@ -66,33 +67,39 @@ export default async function evaluate(code) {
|
|
|
66
67
|
throw error;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// If the result is a tree, then the default parent of the tree is the current
|
|
71
|
+
// tree.
|
|
72
|
+
if (Tree.isAsyncTree(result) && !result.parent) {
|
|
73
|
+
result.parent = tree;
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
// To aid debugging, add the code to the result.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
77
|
+
if (Object.isExtensible(result)) {
|
|
78
|
+
try {
|
|
79
|
+
if (code.location && !result[sourceSymbol]) {
|
|
80
|
+
Object.defineProperty(result, sourceSymbol, {
|
|
81
|
+
value: codeFragment(code.location),
|
|
82
|
+
enumerable: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!result[codeSymbol]) {
|
|
86
|
+
Object.defineProperty(result, codeSymbol, {
|
|
87
|
+
value: code,
|
|
88
|
+
enumerable: false,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (!result[scopeSymbol]) {
|
|
92
|
+
Object.defineProperty(result, scopeSymbol, {
|
|
93
|
+
get() {
|
|
94
|
+
return scope(this).trees;
|
|
95
|
+
},
|
|
96
|
+
enumerable: false,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} catch (/** @type {any} */ error) {
|
|
100
|
+
// Ignore errors.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
96
103
|
|
|
97
104
|
return result;
|
|
98
105
|
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
trailingSlash,
|
|
7
7
|
Tree,
|
|
8
8
|
} from "@weborigami/async-tree";
|
|
9
|
-
import getHandlers from "./getHandlers.js";
|
|
10
9
|
import { handleExtension } from "./handlers.js";
|
|
11
10
|
import { evaluate, ops } from "./internal.js";
|
|
12
11
|
|
|
@@ -33,7 +32,6 @@ export default async function expressionObject(entries, parent) {
|
|
|
33
32
|
if (parent !== null && !Tree.isAsyncTree(parent)) {
|
|
34
33
|
throw new TypeError(`Parent must be an AsyncTree or null`);
|
|
35
34
|
}
|
|
36
|
-
setParent(object, parent);
|
|
37
35
|
|
|
38
36
|
let tree;
|
|
39
37
|
const eagerProperties = [];
|
|
@@ -92,8 +90,7 @@ export default async function expressionObject(entries, parent) {
|
|
|
92
90
|
get = async () => {
|
|
93
91
|
tree ??= new ObjectTree(object);
|
|
94
92
|
const result = await evaluate.call(tree, code);
|
|
95
|
-
|
|
96
|
-
return handleExtension(tree, result, key, handlers);
|
|
93
|
+
return handleExtension(tree, result, key);
|
|
97
94
|
};
|
|
98
95
|
} else {
|
|
99
96
|
// No extension, so getter just invokes code.
|
|
@@ -119,6 +116,9 @@ export default async function expressionObject(entries, parent) {
|
|
|
119
116
|
writable: true,
|
|
120
117
|
});
|
|
121
118
|
|
|
119
|
+
// Attach the parent
|
|
120
|
+
setParent(object, parent);
|
|
121
|
+
|
|
122
122
|
// Evaluate any properties that were declared as immediate: get their value
|
|
123
123
|
// and overwrite the property getter with the actual value.
|
|
124
124
|
for (const key of eagerProperties) {
|
package/src/runtime/handlers.js
CHANGED
|
@@ -4,10 +4,60 @@ import {
|
|
|
4
4
|
isPacked,
|
|
5
5
|
isStringLike,
|
|
6
6
|
isUnpackable,
|
|
7
|
+
scope,
|
|
7
8
|
setParent,
|
|
8
9
|
trailingSlash,
|
|
9
10
|
} from "@weborigami/async-tree";
|
|
10
11
|
|
|
12
|
+
/** @typedef {import("../../index.ts").ExtensionHandler} ExtensionHandler */
|
|
13
|
+
|
|
14
|
+
// Track extensions handlers for a given containing tree.
|
|
15
|
+
const handlersForContainer = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Find an extension handler for a file in the given container.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
21
|
+
*
|
|
22
|
+
* @param {AsyncTree} parent
|
|
23
|
+
* @param {string} extension
|
|
24
|
+
*/
|
|
25
|
+
export async function getExtensionHandler(parent, extension) {
|
|
26
|
+
let handlers = handlersForContainer.get(parent);
|
|
27
|
+
if (handlers) {
|
|
28
|
+
if (handlers[extension]) {
|
|
29
|
+
return handlers[extension];
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
handlers = {};
|
|
33
|
+
handlersForContainer.set(parent, handlers);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handlerName = `${extension.slice(1)}.handler`;
|
|
37
|
+
const parentScope = scope(parent);
|
|
38
|
+
|
|
39
|
+
/** @type {Promise<ExtensionHandler>} */
|
|
40
|
+
let handlerPromise = parentScope
|
|
41
|
+
?.get(handlerName)
|
|
42
|
+
.then(async (extensionHandler) => {
|
|
43
|
+
if (isUnpackable(extensionHandler)) {
|
|
44
|
+
// The extension handler itself needs to be unpacked. E.g., if it's a
|
|
45
|
+
// buffer containing JavaScript file, we need to unpack it to get its
|
|
46
|
+
// default export.
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
extensionHandler = await extensionHandler.unpack();
|
|
49
|
+
}
|
|
50
|
+
// Update cache with actual handler
|
|
51
|
+
handlers[extension] = extensionHandler;
|
|
52
|
+
return extensionHandler;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Cache handler even if it's undefined so we don't look it up again
|
|
56
|
+
handlers[extension] = handlerPromise;
|
|
57
|
+
|
|
58
|
+
return handlerPromise;
|
|
59
|
+
}
|
|
60
|
+
|
|
11
61
|
/**
|
|
12
62
|
* If the given value is packed (e.g., buffer) and the key is a string-like path
|
|
13
63
|
* that ends in an extension, search for a handler for that extension and, if
|
|
@@ -17,34 +67,20 @@ import {
|
|
|
17
67
|
* @param {any} value
|
|
18
68
|
* @param {any} key
|
|
19
69
|
*/
|
|
20
|
-
export async function handleExtension(parent, value, key
|
|
21
|
-
if (
|
|
22
|
-
handlers &&
|
|
23
|
-
isPacked(value) &&
|
|
24
|
-
isStringLike(key) &&
|
|
25
|
-
value.unpack === undefined
|
|
26
|
-
) {
|
|
70
|
+
export async function handleExtension(parent, value, key) {
|
|
71
|
+
if (isPacked(value) && isStringLike(key) && value.unpack === undefined) {
|
|
27
72
|
const hasSlash = trailingSlash.has(key);
|
|
28
73
|
if (hasSlash) {
|
|
29
74
|
key = trailingSlash.remove(key);
|
|
30
75
|
}
|
|
31
76
|
|
|
32
|
-
// Special
|
|
33
|
-
// `.jse.<ext>` are JSE documents.
|
|
77
|
+
// Special case: `.ori.<ext>` extensions are Origami documents.
|
|
34
78
|
const extname = key.match(/\.ori\.\S+$/)
|
|
35
79
|
? ".oridocument"
|
|
36
|
-
: key.match(/\.jse\.\S+$/)
|
|
37
|
-
? ".jsedocument"
|
|
38
80
|
: extension.extname(key);
|
|
39
81
|
if (extname) {
|
|
40
|
-
const
|
|
41
|
-
let handler = await handlers.get(handlerName);
|
|
82
|
+
const handler = await getExtensionHandler(parent, extname);
|
|
42
83
|
if (handler) {
|
|
43
|
-
if (isUnpackable(handler)) {
|
|
44
|
-
// The extension handler itself needs to be unpacked
|
|
45
|
-
handler = await handler.unpack();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
84
|
if (hasSlash && handler.unpack) {
|
|
49
85
|
// Key like `data.json/` ends in slash -- unpack immediately
|
|
50
86
|
return handler.unpack(value, { key, parent });
|
|
@@ -53,6 +53,11 @@ export default async function mergeTrees(...trees) {
|
|
|
53
53
|
return result;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// If all trees are arrays, return an array.
|
|
57
|
+
if (unpacked.every((tree) => Array.isArray(tree))) {
|
|
58
|
+
return unpacked.flat();
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
// Merge the trees.
|
|
57
62
|
const result = merge(...unpacked);
|
|
58
63
|
setParent(result, this);
|