@weborigami/language 0.3.3 → 0.3.4-jse.4

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.
@@ -1,5 +1,5 @@
1
- import { trailingSlash } from "@weborigami/async-tree";
2
1
  import * as YAMLModule from "yaml";
2
+ import trailingSlash from "../../../origami/src/origami/slash.js";
3
3
  import codeFragment from "../runtime/codeFragment.js";
4
4
  import * as ops from "../runtime/ops.js";
5
5
 
@@ -15,10 +15,14 @@ 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
- // Marker for a reference that may be a builtin or a scope reference
19
- export const undetermined = Symbol("undetermined");
20
-
21
- const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
18
+ // Markers in compiled output, will get optimized away
19
+ export const markers = {
20
+ global: Symbol("global"), // Global reference
21
+ external: Symbol("external"), // External reference
22
+ property: Symbol("property"), // Property access
23
+ reference: Symbol("reference"), // Reference to local, scope, or global
24
+ traverse: Symbol("traverse"), // Path traversal
25
+ };
22
26
 
23
27
  /**
24
28
  * If a parse result is an object that will be evaluated at runtime, attach the
@@ -37,7 +41,7 @@ export function annotate(code, location) {
37
41
  }
38
42
 
39
43
  /**
40
- * In the given code, replace all scope refernces to the given name with the
44
+ * In the given code, replace all function calls to the given name with the
41
45
  * given macro code.
42
46
  *
43
47
  * @param {AnnotatedCode} code
@@ -49,59 +53,20 @@ export function applyMacro(code, name, macro) {
49
53
  return code;
50
54
  }
51
55
 
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
-
61
- /**
62
- * The indicated code is being used to define a property named by the given key.
63
- * Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
64
- * infinite recursion.
65
- *
66
- * @param {AnnotatedCode} code
67
- * @param {string} key
68
- */
69
- function avoidRecursivePropertyCalls(code, key) {
70
- if (!(code instanceof Array)) {
71
- return code;
72
- }
73
- /** @type {Code} */
74
- let modified;
56
+ // We're looking for a function call with the given name.
57
+ // For `foo`, the call would be: [[markers.traverse, [markers.reference, "foo"]], undefined]
75
58
  if (
76
- code[0] === ops.scope &&
77
- trailingSlash.remove(code[1]) === trailingSlash.remove(key)
59
+ code[0] instanceof Array &&
60
+ code[0][0] === markers.traverse &&
61
+ code[0][1][0] === markers.reference &&
62
+ code[0][1][1] === name
78
63
  ) {
79
- // Rewrite to avoid recursion
80
- modified = [ops.inherited, code[1]];
81
- } else if (
82
- code[0] === ops.lambda &&
83
- code[1].some((param) => param[1] === key)
84
- ) {
85
- // Lambda that defines the key; don't rewrite
86
- return code;
87
- } else {
88
- // Process any nested code
89
- modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
64
+ // Replace the call with the macro
65
+ return annotate(macro, code.location);
90
66
  }
91
- return annotate(modified, code.location);
92
- }
93
67
 
94
- /**
95
- * Downgrade a potential builtin reference to a scope reference.
96
- *
97
- * @param {AnnotatedCode} code
98
- */
99
- export function downgradeReference(code) {
100
- if (code && code.length === 2 && code[0] === undetermined) {
101
- return annotate([ops.scope, code[1]], code.location);
102
- } else {
103
- return code;
104
- }
68
+ const applied = code.map((child) => applyMacro(child, name, macro));
69
+ return annotate(applied, code.location);
105
70
  }
106
71
 
107
72
  /**
@@ -139,7 +104,7 @@ export function makeArray(entries, location) {
139
104
 
140
105
  let result;
141
106
  if (spreads.length > 1) {
142
- result = [ops.merge, ...spreads];
107
+ result = [ops.flat, ...spreads];
143
108
  } else if (spreads.length === 1) {
144
109
  result = spreads[0];
145
110
  } else {
@@ -196,7 +161,7 @@ export function makeBinaryOperation(left, [operatorToken, right]) {
196
161
  * @param {AnnotatedCode} target
197
162
  * @param {any[]} args
198
163
  */
199
- export function makeCall(target, args) {
164
+ export function makeCall(target, args, location) {
200
165
  if (!(target instanceof Array)) {
201
166
  const error = new SyntaxError(`Can't call this like a function: ${target}`);
202
167
  /** @type {any} */ (error).location = /** @type {any} */ (target).location;
@@ -204,53 +169,23 @@ export function makeCall(target, args) {
204
169
  }
205
170
 
206
171
  let fnCall;
207
- if (args[0] === ops.traverse) {
208
- let tree = target;
209
-
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]);
216
- }
217
- }
218
-
219
- if (args.length > 1) {
220
- // Regular traverse
221
- const keys = args.slice(1);
222
- fnCall = [ops.traverse, tree, ...keys];
223
- } else {
224
- // Traverse without arguments equates to unpack
225
- fnCall = [ops.unpack, tree];
226
- }
227
- } else if (args[0] === ops.template) {
172
+ const op = args[0];
173
+ if (op === markers.traverse || op === ops.optionalTraverse) {
174
+ // Traverse
175
+ const keys = args.slice(1);
176
+ fnCall = [target, ...keys];
177
+ } else if (op === markers.property) {
178
+ // Property access
179
+ const property = args[1];
180
+ fnCall = [target, property];
181
+ } else if (op === ops.templateTree) {
228
182
  // Tagged template
229
183
  const strings = args[1];
230
184
  const values = args.slice(2);
231
- fnCall = makeTaggedTemplateCall(
232
- upgradeReference(target),
233
- strings,
234
- ...values
235
- );
185
+ fnCall = makeTaggedTemplateCall(target, strings, ...values);
236
186
  } else {
237
187
  // Function call with explicit or implicit parentheses
238
- fnCall = [upgradeReference(target), ...args];
239
- }
240
-
241
- // Create a location spanning the newly-constructed function call.
242
- const location = { ...target.location };
243
- if (args instanceof Array) {
244
- let end;
245
- if ("location" in args) {
246
- end = /** @type {any} */ (args).location.end;
247
- } else if ("location" in args.at(-1)) {
248
- end = args.at(-1).location.end;
249
- }
250
- if (end === undefined) {
251
- throw "Internal parser error: no location for function call argument";
252
- }
253
- location.end = end;
188
+ fnCall = [target, ...args];
254
189
  }
255
190
 
256
191
  return annotate(fnCall, location);
@@ -275,6 +210,86 @@ export function makeDeferredArguments(args) {
275
210
  });
276
211
  }
277
212
 
213
+ export function makeDocument(front, body, location) {
214
+ // In order for template expressions to see the front matter properties,
215
+ // we translate the top-level front properties to object entries.
216
+ const entries = Object.entries(front).map(([key, value]) =>
217
+ annotate([key, annotate([ops.literal, value], location)], location)
218
+ );
219
+
220
+ // Add an entry for the body
221
+ // TODO: Deprecate @text
222
+ entries.push(annotate(["(@text)", body], location));
223
+ entries.push(annotate(["_body", body], location));
224
+
225
+ // Return the code for the document object
226
+ return annotate([ops.object, ...entries], location);
227
+ }
228
+
229
+ /**
230
+ * From the given spreads within an object spread, return the merge.
231
+ *
232
+ * Example:
233
+ *
234
+ * {
235
+ * x = { a: 1 }
236
+ * …x
237
+ * y = x
238
+ * }
239
+ *
240
+ * will be treated as:
241
+ *
242
+ * {
243
+ * x = { a: 1 }
244
+ * y = x
245
+ * _result: {
246
+ * x
247
+ * …x
248
+ * y
249
+ * }
250
+ * }.result
251
+ *
252
+ * @param {*} spreads
253
+ * @param {CodeLocation} location
254
+ */
255
+ function makeMerge(spreads, location) {
256
+ const topEntries = [];
257
+ const resultEntries = [];
258
+ for (const spread of spreads) {
259
+ if (spread[0] === ops.object) {
260
+ topEntries.push(...spread.slice(1));
261
+ // Also add an object to the result with indirect references
262
+ const indirectEntries = spread.slice(1).map((entry) => {
263
+ const [key] = entry;
264
+ const context = annotate([ops.context, 1], entry.location);
265
+ const reference = annotate([context, key], entry.location);
266
+ const getter = annotate([ops.getter, reference], entry.location);
267
+ return annotate([key, getter], entry.location);
268
+ });
269
+ const indirectObject = annotate(
270
+ [ops.object, ...indirectEntries],
271
+ location
272
+ );
273
+ resultEntries.push(indirectObject);
274
+ } else {
275
+ resultEntries.push(spread);
276
+ }
277
+ }
278
+
279
+ // Merge to create result
280
+ const result = annotate([ops.merge, ...resultEntries], location);
281
+
282
+ // Add the result to the top-level object as _result
283
+ topEntries.push(annotate(["_result", result], location));
284
+
285
+ // Construct the top-level object
286
+ const topObject = annotate([ops.object, ...topEntries], location);
287
+
288
+ // Get the _result property
289
+ const code = annotate([topObject, "_result"], location);
290
+ return code;
291
+ }
292
+
278
293
  /**
279
294
  * Make an object.
280
295
  *
@@ -331,7 +346,7 @@ export function makeObject(entries, location) {
331
346
  let code;
332
347
  if (spreads.length > 1) {
333
348
  // Merge multiple spreads
334
- code = [ops.merge, ...spreads];
349
+ code = makeMerge(spreads, location);
335
350
  } else if (spreads.length === 1) {
336
351
  // A single spread can just be the object
337
352
  code = spreads[0];
@@ -343,51 +358,47 @@ export function makeObject(entries, location) {
343
358
  return annotate(code, location);
344
359
  }
345
360
 
361
+ /**
362
+ * Handle a path with one or more segments separated by slashes.
363
+ *
364
+ * @param {AnnotatedCode} keys
365
+ */
366
+ export function makePath(keys) {
367
+ // Remove empty segments
368
+ const args = keys.filter(
369
+ (key, index) => index === 0 || (key[1] !== "" && key[1] !== "/")
370
+ );
371
+
372
+ // Upgrade head to a reference
373
+ const [head, ...tail] = args;
374
+ const headKey = head[1];
375
+ const reference = annotate([markers.reference, headKey], head.location);
376
+
377
+ let code = [markers.traverse, reference, ...tail];
378
+ code.location = spanLocations(code);
379
+
380
+ // Last key has trailing slash implies unpack operation
381
+ if (trailingSlash.has(args.at(-1)[1])) {
382
+ code = annotate([ops.unpack, code], code.location);
383
+ }
384
+
385
+ return code;
386
+ }
387
+
346
388
  /**
347
389
  * Make a pipline: similar to a function call, but the order is reversed.
348
390
  *
349
391
  * @param {AnnotatedCode} arg
350
392
  * @param {AnnotatedCode} fn
351
393
  */
352
- export function makePipeline(arg, fn) {
353
- const upgraded = upgradeReference(fn);
354
- const result = makeCall(upgraded, [arg]);
394
+ export function makePipeline(arg, fn, location) {
395
+ const result = makeCall(fn, [arg], location);
355
396
  const source = fn.location.source;
356
397
  let start = arg.location.start;
357
398
  let end = fn.location.end;
358
399
  return annotate(result, { start, source, end });
359
400
  }
360
401
 
361
- // Define a property on an object.
362
- export function makeProperty(key, value) {
363
- const modified = avoidRecursivePropertyCalls(value, key);
364
- return [key, modified];
365
- }
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
-
391
402
  /**
392
403
  * Make a tagged template call
393
404
  *
@@ -495,19 +506,17 @@ export function makeYamlObject(text, location) {
495
506
  throw error;
496
507
  }
497
508
 
498
- return annotate([ops.literal, parsed], location);
509
+ return parsed;
499
510
  }
500
511
 
501
- /**
502
- * Upgrade a potential builtin reference to an actual builtin reference.
503
- *
504
- * @param {AnnotatedCode} code
505
- */
506
- export function upgradeReference(code) {
507
- if (code.length === 2 && code[0] === undetermined) {
508
- const result = [ops.builtin, code[1]];
509
- return annotate(result, code.location);
510
- } else {
511
- return code;
512
- }
512
+ // Create a locations that spans those in the array. This assumes the locations
513
+ // are in order and non-overlapping.
514
+ function spanLocations(code) {
515
+ const first = code.find((item) => item.location).location;
516
+ const last = code[code.findLastIndex((item) => item.location)].location;
517
+ return {
518
+ source: first.source,
519
+ start: first.start,
520
+ end: last.end,
521
+ };
513
522
  }
@@ -1,3 +1,4 @@
1
+ import getHandlers from "./getHandlers.js";
1
2
  import { handleExtension } from "./handlers.js";
2
3
 
3
4
  /**
@@ -9,9 +10,17 @@ import { handleExtension } from "./handlers.js";
9
10
  */
10
11
  export default function HandleExtensionsTransform(Base) {
11
12
  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
+
12
20
  async get(key) {
13
21
  const value = await super.get(key);
14
- return handleExtension(this, value, key);
22
+ const handlers = getHandlers(this);
23
+ return handleExtension(this, value, key, handlers);
15
24
  }
16
25
  };
17
26
  }
@@ -1,6 +1,5 @@
1
- import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
1
+ import { Tree, isUnpackable } from "@weborigami/async-tree";
2
2
  import codeFragment from "./codeFragment.js";
3
- import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
4
3
 
5
4
  /**
6
5
  * Evaluate the given code and return the result.
@@ -52,7 +51,7 @@ export default async function evaluate(code) {
52
51
  result =
53
52
  fn instanceof Function
54
53
  ? await fn.call(tree, ...args) // Invoke the function
55
- : await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
54
+ : await Tree.traverseOrThrow.call(tree, fn, ...args); // Traverse the tree.
56
55
  } catch (/** @type {any} */ error) {
57
56
  if (!error.location) {
58
57
  // Attach the location of the code we tried to evaluate.
@@ -67,39 +66,33 @@ export default async function evaluate(code) {
67
66
  throw error;
68
67
  }
69
68
 
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
-
76
69
  // To aid debugging, add the code to the result.
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
- }
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
+ // }
103
96
 
104
97
  return result;
105
98
  }
@@ -6,6 +6,7 @@ import {
6
6
  trailingSlash,
7
7
  Tree,
8
8
  } from "@weborigami/async-tree";
9
+ import getHandlers from "./getHandlers.js";
9
10
  import { handleExtension } from "./handlers.js";
10
11
  import { evaluate, ops } from "./internal.js";
11
12
 
@@ -32,6 +33,7 @@ export default async function expressionObject(entries, parent) {
32
33
  if (parent !== null && !Tree.isAsyncTree(parent)) {
33
34
  throw new TypeError(`Parent must be an AsyncTree or null`);
34
35
  }
36
+ setParent(object, parent);
35
37
 
36
38
  let tree;
37
39
  const eagerProperties = [];
@@ -90,7 +92,8 @@ export default async function expressionObject(entries, parent) {
90
92
  get = async () => {
91
93
  tree ??= new ObjectTree(object);
92
94
  const result = await evaluate.call(tree, code);
93
- return handleExtension(tree, result, key);
95
+ const handlers = getHandlers(tree);
96
+ return handleExtension(tree, result, key, handlers);
94
97
  };
95
98
  } else {
96
99
  // No extension, so getter just invokes code.
@@ -116,9 +119,6 @@ export default async function expressionObject(entries, parent) {
116
119
  writable: true,
117
120
  });
118
121
 
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) {
@@ -136,17 +136,23 @@ export default async function expressionObject(entries, parent) {
136
136
  return object;
137
137
  }
138
138
 
139
- function entryKey(object, eagerProperties, entry) {
140
- const [key, value] = entry;
139
+ export function entryKey(entry, object = null, eagerProperties = []) {
140
+ let [key, value] = entry;
141
+
142
+ if (key[0] === "(" && key[key.length - 1] === ")") {
143
+ // Non-enumerable property, remove parentheses. This doesn't come up in the
144
+ // constructor, but can happen in situations encountered by the compiler's
145
+ // optimizer.
146
+ key = key.slice(1, -1);
147
+ }
141
148
 
142
- const hasExplicitSlash = trailingSlash.has(key);
143
- if (hasExplicitSlash) {
144
- // Return key as is
149
+ if (trailingSlash.has(key)) {
150
+ // Explicit trailing slash, return as is
145
151
  return key;
146
152
  }
147
153
 
148
154
  // If eager property value is treelike, add slash to the key
149
- if (eagerProperties.includes(key) && Tree.isTreelike(object[key])) {
155
+ if (eagerProperties.includes(key) && Tree.isTreelike(object?.[key])) {
150
156
  return trailingSlash.add(key);
151
157
  }
152
158
 
@@ -163,5 +169,5 @@ function entryKey(object, eagerProperties, entry) {
163
169
  function keys(object, eagerProperties, propertyIsEnumerable, entries) {
164
170
  return entries
165
171
  .filter(([key]) => propertyIsEnumerable[key])
166
- .map((entry) => entryKey(object, eagerProperties, entry));
172
+ .map((entry) => entryKey(entry, object, eagerProperties));
167
173
  }
@@ -0,0 +1,10 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+
3
+ // Return the extension handlers for the given tree
4
+ export default function getHandlers(tree) {
5
+ if (!tree) {
6
+ return null;
7
+ }
8
+ const root = Tree.root(tree);
9
+ return root.handlers;
10
+ }
@@ -4,60 +4,10 @@ import {
4
4
  isPacked,
5
5
  isStringLike,
6
6
  isUnpackable,
7
- scope,
8
7
  setParent,
9
8
  trailingSlash,
10
9
  } from "@weborigami/async-tree";
11
10
 
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
-
61
11
  /**
62
12
  * If the given value is packed (e.g., buffer) and the key is a string-like path
63
13
  * that ends in an extension, search for a handler for that extension and, if
@@ -67,20 +17,34 @@ export async function getExtensionHandler(parent, extension) {
67
17
  * @param {any} value
68
18
  * @param {any} key
69
19
  */
70
- export async function handleExtension(parent, value, key) {
71
- if (isPacked(value) && isStringLike(key) && value.unpack === undefined) {
20
+ export async function handleExtension(parent, value, key, handlers) {
21
+ if (
22
+ handlers &&
23
+ isPacked(value) &&
24
+ isStringLike(key) &&
25
+ value.unpack === undefined
26
+ ) {
72
27
  const hasSlash = trailingSlash.has(key);
73
28
  if (hasSlash) {
74
29
  key = trailingSlash.remove(key);
75
30
  }
76
31
 
77
- // Special case: `.ori.<ext>` extensions are Origami documents.
32
+ // Special cases: `.ori.<ext>` extensions are Origami documents,
33
+ // `.jse.<ext>` are JSE documents.
78
34
  const extname = key.match(/\.ori\.\S+$/)
79
35
  ? ".oridocument"
36
+ : key.match(/\.jse\.\S+$/)
37
+ ? ".jsedocument"
80
38
  : extension.extname(key);
81
39
  if (extname) {
82
- const handler = await getExtensionHandler(parent, extname);
40
+ const handlerName = `${extname.slice(1)}.handler`;
41
+ let handler = await handlers[handlerName];
83
42
  if (handler) {
43
+ if (isUnpackable(handler)) {
44
+ // The extension handler itself needs to be unpacked
45
+ handler = await handler.unpack();
46
+ }
47
+
84
48
  if (hasSlash && handler.unpack) {
85
49
  // Key like `data.json/` ends in slash -- unpack immediately
86
50
  return handler.unpack(value, { key, parent });