@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.
@@ -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
- // Markers in compiled output, will get optimized away
19
- export const markers = {
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 function calls to the given name with the
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
- // We're looking for a function call with the given name.
57
- // For `foo`, the call would be: [[reference, [ops.literal, "foo"]], undefined]
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 [[ops.scope], key] calls to be [ops.inherited, key] to avoid
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] instanceof Array &&
89
- code[0][0] === ops.scope &&
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][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 global reference to a reference.
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] === markers.reference) {
114
- return annotate([markers.reference, code[1]], code.location);
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.flat, ...spreads];
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, mode) {
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
- const op = args[0];
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] === markers.reference && !trailingSlash.has(tree[1][1])) {
225
- // Target didn't parse with a trailing slash; add one
226
- tree[1][1] = trailingSlash.add(tree[1][1]);
227
- }
228
-
229
- // Is the target an existing traversal that can be extended? It should be a
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.push(...keys);
252
- } else if (tree[0] !== ops.rootDirectory) {
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 (op === ops.templateStandard || op === ops.templateTree) {
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, mode),
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, mode), ...args];
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 = makeMerge(spreads, location);
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, mode) {
473
- const upgraded = upgradeReference(fn, mode);
474
- const result = makeCall(upgraded, [arg], mode);
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, mode) {
603
- if (
604
- mode === "shell" &&
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
- const handlers = getHandlers(this);
23
- return handleExtension(this, value, key, handlers);
14
+ return handleExtension(this, value, key);
24
15
  }
25
16
  };
26
17
  }
@@ -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.call(tree, fn, ...args); // Traverse the tree.
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
- // if (Object.isExtensible(result)) {
71
- // try {
72
- // if (code.location && !result[sourceSymbol]) {
73
- // Object.defineProperty(result, sourceSymbol, {
74
- // value: codeFragment(code.location),
75
- // enumerable: false,
76
- // });
77
- // }
78
- // if (!result[codeSymbol]) {
79
- // Object.defineProperty(result, codeSymbol, {
80
- // value: code,
81
- // enumerable: false,
82
- // });
83
- // }
84
- // if (!result[scopeSymbol]) {
85
- // Object.defineProperty(result, scopeSymbol, {
86
- // get() {
87
- // return scope(this).trees;
88
- // },
89
- // enumerable: false,
90
- // });
91
- // }
92
- // } catch (/** @type {any} */ error) {
93
- // // Ignore errors.
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
- const handlers = getHandlers(tree);
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) {
@@ -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, handlers) {
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 cases: `.ori.<ext>` extensions are Origami documents,
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 handlerName = `${extname.slice(1)}.handler`;
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);