@weborigami/language 0.6.17 → 0.7.0-beta.2

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.
Files changed (90) hide show
  1. package/index.ts +1 -0
  2. package/main.js +7 -1
  3. package/package.json +7 -6
  4. package/src/compiler/compile.js +10 -3
  5. package/src/compiler/optimize.js +71 -40
  6. package/src/compiler/parse.js +1 -1
  7. package/src/compiler/parserHelpers.js +5 -3
  8. package/src/handlers/addExtensionKeyFn.js +18 -0
  9. package/src/handlers/epub_handler.js +54 -0
  10. package/src/handlers/getSource.js +11 -0
  11. package/src/handlers/handlers.js +2 -0
  12. package/src/handlers/htm_handler.js +1 -1
  13. package/src/handlers/js_handler.js +13 -4
  14. package/src/handlers/mediaTypeExtensions.json +15 -0
  15. package/src/handlers/ori_handler.js +8 -7
  16. package/src/handlers/oridocument_handler.js +19 -28
  17. package/src/handlers/processOriExport.js +17 -0
  18. package/src/handlers/tsv_handler.js +1 -1
  19. package/src/handlers/txt_handler.js +4 -2
  20. package/src/handlers/xhtml_handler.js +1 -1
  21. package/src/handlers/yaml_handler.js +6 -3
  22. package/src/handlers/zip_handler.js +112 -0
  23. package/src/project/activeProjectRoot.js +9 -0
  24. package/src/project/getGlobalsForTree.js +5 -0
  25. package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
  26. package/src/project/jsGlobals.js +1 -0
  27. package/src/project/projectConfig.js +2 -2
  28. package/src/project/projectRootFromPath.js +2 -0
  29. package/src/protocols/constructHref.js +3 -3
  30. package/src/protocols/constructSiteTree.js +11 -2
  31. package/src/protocols/explore.js +1 -1
  32. package/src/protocols/explorehttp.js +1 -1
  33. package/src/protocols/fetchAndHandleExtension.js +23 -11
  34. package/src/protocols/files.js +1 -0
  35. package/src/protocols/http.js +4 -1
  36. package/src/protocols/https.js +4 -1
  37. package/src/protocols/httpstree.js +1 -1
  38. package/src/protocols/httptree.js +1 -1
  39. package/src/protocols/package.js +15 -3
  40. package/src/runtime/AsyncCacheTransform.d.ts +5 -0
  41. package/src/runtime/AsyncCacheTransform.js +134 -0
  42. package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
  43. package/src/runtime/HandleExtensionsTransform.js +18 -2
  44. package/src/runtime/OrigamiFileMap.d.ts +5 -2
  45. package/src/runtime/OrigamiFileMap.js +27 -4
  46. package/src/runtime/ScopeMap.js +72 -0
  47. package/src/runtime/SyncCacheTransform.d.ts +8 -0
  48. package/src/runtime/SyncCacheTransform.js +133 -0
  49. package/src/runtime/SystemCacheMap.js +259 -0
  50. package/src/runtime/WatchFilesMixin.js +52 -19
  51. package/src/runtime/enableValueCaching.js +192 -0
  52. package/src/runtime/execute.js +2 -2
  53. package/src/runtime/executionContext.js +7 -0
  54. package/src/runtime/explainReferenceError.js +7 -2
  55. package/src/runtime/expressionObject.js +54 -46
  56. package/src/runtime/handleExtension.js +65 -34
  57. package/src/runtime/interop.js +2 -2
  58. package/src/runtime/mergeTrees.js +1 -1
  59. package/src/runtime/ops.js +28 -33
  60. package/src/runtime/symbols.js +3 -0
  61. package/src/runtime/systemCache.js +3 -0
  62. package/src/runtime/volatile.js +14 -0
  63. package/test/compiler/codeHelpers.js +2 -1
  64. package/test/compiler/optimize.test.js +62 -54
  65. package/test/handlers/epub_handler.test.js +27 -0
  66. package/test/handlers/fixtures/test.zip +0 -0
  67. package/test/handlers/ori_handler.test.js +22 -3
  68. package/test/handlers/oridocument_handler.test.js +1 -1
  69. package/test/handlers/zip_handler.test.js +45 -0
  70. package/test/protocols/https.test.js +19 -0
  71. package/test/protocols/package.test.js +7 -2
  72. package/test/runtime/AsyncCacheTransform.test.js +91 -0
  73. package/test/runtime/OrigamiFileMap.test.js +26 -23
  74. package/test/runtime/ScopeMap.test.js +49 -0
  75. package/test/runtime/SyncCacheTransform.test.js +93 -0
  76. package/test/runtime/SystemCacheMap.test.js +239 -0
  77. package/test/runtime/asyncCalcs.js +28 -0
  78. package/test/runtime/enableValueCaching.test.js +55 -0
  79. package/test/runtime/errors.test.js +53 -30
  80. package/test/runtime/evaluate.test.js +9 -4
  81. package/test/runtime/execute.test.js +6 -1
  82. package/test/runtime/expressionObject.test.js +55 -15
  83. package/test/runtime/fetchAndHandleExtension.test.js +24 -0
  84. package/test/runtime/fixtures/unpack/hello.json +1 -0
  85. package/test/runtime/handleExtension.test.js +12 -1
  86. package/test/runtime/ops.test.js +70 -65
  87. package/test/runtime/syncCalcs.js +27 -0
  88. package/test/runtime/systemCache.test.js +66 -0
  89. package/src/runtime/assignPropertyDescriptors.js +0 -23
  90. package/src/runtime/asyncStorage.js +0 -7
package/index.ts CHANGED
@@ -90,6 +90,7 @@ export type RuntimeState = {
90
90
  * Source code representation used by the parser.
91
91
  */
92
92
  export type Source = {
93
+ cachePath?: string;
93
94
  name?: string;
94
95
  text: string;
95
96
  url?: URL;
package/main.js CHANGED
@@ -5,12 +5,15 @@ export { default as isOrigamiFrontMatter } from "./src/compiler/isOrigamiFrontMa
5
5
  export { parse } from "./src/compiler/parse.js";
6
6
  export * from "./src/compiler/parserHelpers.js";
7
7
  export * as Handlers from "./src/handlers/handlers.js";
8
+ export * as activeProjectRoot from "./src/project/activeProjectRoot.js";
8
9
  export { default as coreGlobals } from "./src/project/coreGlobals.js";
10
+ export { default as getGlobalsForTree } from "./src/project/getGlobalsForTree.js";
11
+ export { default as initializeGlobalsForTree } from "./src/project/initializeGlobalsForTree.js";
9
12
  export { default as jsGlobals } from "./src/project/jsGlobals.js";
10
13
  export { default as projectConfig } from "./src/project/projectConfig.js";
11
- export { default as projectGlobals } from "./src/project/projectGlobals.js";
12
14
  export { default as projectRoot } from "./src/project/projectRoot.js";
13
15
  export { default as projectRootFromPath } from "./src/project/projectRootFromPath.js";
16
+ export { default as fetchAndHandleExtension } from "./src/protocols/fetchAndHandleExtension.js";
14
17
  export * as Protocols from "./src/protocols/protocols.js";
15
18
  export { formatError, highlightError, lineInfo } from "./src/runtime/errors.js";
16
19
  export { default as evaluate } from "./src/runtime/evaluate.js";
@@ -25,5 +28,8 @@ export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.
25
28
  export * as moduleCache from "./src/runtime/moduleCache.js";
26
29
  export { default as OrigamiFileMap } from "./src/runtime/OrigamiFileMap.js";
27
30
  export * as symbols from "./src/runtime/symbols.js";
31
+ export { default as systemCache } from "./src/runtime/systemCache.js";
32
+ export { default as SystemCacheMap } from "./src/runtime/SystemCacheMap.js";
28
33
  export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
34
+ export { default as volatile } from "./src/runtime/volatile.js";
29
35
  export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.6.17",
3
+ "version": "0.7.0-beta.2",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
7
7
  "types": "./index.ts",
8
8
  "devDependencies": {
9
- "@types/node": "25.3.2",
10
- "peggy": "5.0.6",
11
- "typescript": "5.9.3"
9
+ "@types/node": "25.9.1",
10
+ "peggy": "5.1.0",
11
+ "typescript": "6.0.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.6.17",
14
+ "@weborigami/async-tree": "0.7.0-beta.2",
15
+ "adm-zip": "0.5.17",
15
16
  "exif-parser": "0.1.12",
16
17
  "watcher": "2.3.1",
17
- "yaml": "2.8.2"
18
+ "yaml": "2.9.0"
18
19
  },
19
20
  "scripts": {
20
21
  "build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
@@ -1,7 +1,11 @@
1
+ import { trailingSlash } from "@weborigami/async-tree";
1
2
  import { createExpressionFunction } from "../runtime/expressionFunction.js";
2
3
  import optimize from "./optimize.js";
3
4
  import { parse } from "./parse.js";
4
5
 
6
+ // For caching while evaluating expressions with no identified source file
7
+ let count = 0;
8
+
5
9
  /**
6
10
  * Compile the given Origami source code into a JavaScript function.
7
11
  *
@@ -27,12 +31,15 @@ function compile(source, options) {
27
31
  startRule,
28
32
  });
29
33
 
34
+ // Select a path the code will use for caching
35
+ const cachePath = source.cachePath ?? `_compile${count++}`;
36
+ const objectCachePath = cachePath ? trailingSlash.add(cachePath) : null;
37
+
30
38
  // Optimize the code
31
- const cache = mode === "program" ? {} : null;
32
39
  const optimized = optimize(code, {
33
- cache,
40
+ cachePath,
34
41
  globals,
35
- parent,
42
+ objectCachePath,
36
43
  });
37
44
 
38
45
  // Create a function that executes the optimized code.
@@ -6,6 +6,7 @@ import {
6
6
  propertyInfo,
7
7
  } from "../runtime/expressionObject.js";
8
8
  import { ops } from "../runtime/internal.js";
9
+ import SystemCacheMap from "../runtime/SystemCacheMap.js";
9
10
  import { annotate, markers, spanLocations } from "./parserHelpers.js";
10
11
 
11
12
  export const REFERENCE_PARAM = 1;
@@ -30,9 +31,9 @@ export const REFERENCE_EXTERNAL = 4;
30
31
  * @returns {AnnotatedCode}
31
32
  */
32
33
  export default function optimize(code, options = {}) {
34
+ // Cache path for this source file
35
+ const cachePath = options.cachePath ?? null;
33
36
  const globals = options.globals ?? jsGlobals;
34
- const cache = options.cache === undefined ? {} : options.cache;
35
- const parent = options.parent ?? null;
36
37
 
37
38
  // The locals is an array, one item for each function or object context that
38
39
  // has been entered. The array grows to the right. Array items are objects
@@ -48,7 +49,7 @@ export default function optimize(code, options = {}) {
48
49
  return globals[args[0]];
49
50
 
50
51
  case markers.traverse:
51
- return resolvePath(code, globals, parent, locals, cache);
52
+ return resolvePath(code, globals, locals, cachePath);
52
53
 
53
54
  case ops.lambda:
54
55
  const parameters = args[1];
@@ -65,8 +66,7 @@ export default function optimize(code, options = {}) {
65
66
  return inlineLiteral(code);
66
67
 
67
68
  case ops.object:
68
- const entries = args;
69
- const propertyNames = getPropertyNames(entries);
69
+ const propertyNames = getPropertyNames(args);
70
70
  locals.push({
71
71
  type: REFERENCE_INHERITED,
72
72
  names: propertyNames,
@@ -75,37 +75,48 @@ export default function optimize(code, options = {}) {
75
75
  }
76
76
 
77
77
  // Optimize children
78
- const optimized = annotate(
78
+ let optimized = annotate(
79
79
  code.map((child, index) => {
80
- if (op === ops.object && index > 0) {
81
- const [key, value] = child;
82
- const adjustedLocals = avoidLocalRecursion(locals, key);
83
- const optimizedKey =
84
- typeof key === "string"
80
+ if (op === ops.object) {
81
+ if (index === 0) {
82
+ return child; // return op as is
83
+ } else {
84
+ // Object entry
85
+ const [key, value] = child;
86
+ const adjustedLocals = avoidLocalRecursion(locals, key);
87
+ const isStringKey = typeof key === "string";
88
+ const childOptions = {
89
+ ...options,
90
+ locals: adjustedLocals,
91
+ };
92
+ const optimizedKey = isStringKey
85
93
  ? key
86
- : optimize(/** @type {AnnotatedCode} */ (key), {
87
- ...options,
88
- locals: adjustedLocals,
89
- });
90
- const optimizedValue = optimize(/** @type {AnnotatedCode} */ (value), {
91
- ...options,
92
- locals: adjustedLocals,
93
- });
94
- return annotate([optimizedKey, optimizedValue], child.location);
94
+ : optimize(/** @type {AnnotatedCode} */ (key), childOptions);
95
+ const optimizedValue = optimize(
96
+ /** @type {AnnotatedCode} */ (value),
97
+ childOptions,
98
+ );
99
+ return annotate([optimizedKey, optimizedValue], child.location);
100
+ }
95
101
  } else if (Array.isArray(child) && "location" in child) {
96
102
  // Review: Aside from ops.object (above), what non-instruction arrays
97
103
  // does this descend into?
98
- return optimize(/** @type {AnnotatedCode} */ (child), {
99
- ...options,
100
- locals,
101
- });
104
+ const childOptions = { ...options, locals };
105
+ // Objects below here are detached from top-level object; not cached
106
+ delete childOptions.objectCachePath;
107
+ return optimize(/** @type {AnnotatedCode} */ (child), childOptions);
102
108
  } else {
103
109
  return child;
104
110
  }
105
111
  }),
106
- code.location
112
+ code.location,
107
113
  );
108
114
 
115
+ if (optimized[0] instanceof Array && optimized[0][0] === ops.cache) {
116
+ // An external reference in function position: add implied unpack
117
+ optimized = impliedUnpack(optimized);
118
+ }
119
+
109
120
  return annotate(optimized, code.location);
110
121
  }
111
122
 
@@ -129,7 +140,7 @@ function avoidLocalRecursion(locals, key) {
129
140
  const matchingKeyIndex = locals[currentFrameIndex].names.findIndex(
130
141
  (name) =>
131
142
  // Ignore trailing slashes when comparing keys
132
- trailingSlash.remove(name) === trailingSlash.remove(key)
143
+ trailingSlash.remove(name) === trailingSlash.remove(key),
133
144
  );
134
145
 
135
146
  if (matchingKeyIndex >= 0) {
@@ -147,10 +158,11 @@ function avoidLocalRecursion(locals, key) {
147
158
  }
148
159
  }
149
160
 
150
- function cachePath(code, cache) {
161
+ function cacheExternalPath(code, cachePath) {
151
162
  const keys = code.map(keyFromCode).filter((key) => key !== null);
152
- const path = pathFromKeys(keys);
153
- return annotate([ops.cache, cache, path, code], code.location);
163
+ const keysPath = pathFromKeys(keys);
164
+ const refCachePath = SystemCacheMap.joinPath(cachePath, "_refs", keysPath);
165
+ return annotate([ops.cache, refCachePath, code], code.location);
154
166
  }
155
167
 
156
168
  // A reference with periods like x.y.z
@@ -180,8 +192,8 @@ function compoundReference(key, globals, locals, location) {
180
192
  return { type: headReference.type, result };
181
193
  }
182
194
 
183
- function externalReference(key, parent, location) {
184
- const scope = annotate([ops.scope, parent], location);
195
+ function externalReference(key, location) {
196
+ const scope = annotate([ops.scope], location);
185
197
  const literal = annotate([ops.literal, key], location);
186
198
  return annotate([scope, literal], location);
187
199
  }
@@ -193,7 +205,7 @@ function findLocalDetails(key, locals) {
193
205
  for (let i = locals.length - 1; i >= 0; i--) {
194
206
  const { type, names } = locals[i];
195
207
  const local = names.find(
196
- (name) => trailingSlash.remove(name) === normalized
208
+ (name) => trailingSlash.remove(name) === normalized,
197
209
  );
198
210
  if (local) {
199
211
  const depth = type === REFERENCE_PARAM ? paramDepth : inheritedDepth;
@@ -221,6 +233,25 @@ function globalReference(key, globals) {
221
233
  return globals[normalized];
222
234
  }
223
235
 
236
+ // The code starts with a cached external reference in function position. If the
237
+ // cache path doesn't end in a trailing slash, wrap the external reference in an
238
+ // unpack operation.
239
+ function impliedUnpack(code) {
240
+ const [opCache, cachePath, refCode] = code[0];
241
+ if (cachePath.endsWith("/")) {
242
+ // Will already be unpacked
243
+ return code;
244
+ }
245
+ const modifiedPath = cachePath + "/";
246
+ const modifiedRefCode = annotate([ops.unpack, refCode], code[0].location);
247
+ const unpacked = annotate(
248
+ [opCache, modifiedPath, modifiedRefCode],
249
+ code[0].location,
250
+ );
251
+ const modified = annotate([unpacked, ...code.slice(1)], code.location);
252
+ return modified;
253
+ }
254
+
224
255
  function inheritedReference(key, depth, location) {
225
256
  const literal = annotate([ops.literal, key], location);
226
257
  const inherited = annotate([ops.inherited, depth], location);
@@ -283,7 +314,7 @@ function paramReference(key, depth, location) {
283
314
  return annotate([params, literal], location);
284
315
  }
285
316
 
286
- function reference(code, globals, parent, locals) {
317
+ function reference(code, globals, locals) {
287
318
  const key = keyFromCode(code);
288
319
  const normalized = trailingSlash.remove(key);
289
320
  const location = code.location;
@@ -306,7 +337,7 @@ function reference(code, globals, parent, locals) {
306
337
  // Explicit external reference
307
338
  return {
308
339
  type: REFERENCE_EXTERNAL,
309
- result: externalReference(key, parent, location),
340
+ result: externalReference(key, location),
310
341
  };
311
342
  }
312
343
 
@@ -325,15 +356,15 @@ function reference(code, globals, parent, locals) {
325
356
  // Must be external
326
357
  return {
327
358
  type: REFERENCE_EXTERNAL,
328
- result: externalReference(key, parent, location),
359
+ result: externalReference(key, location),
329
360
  };
330
361
  }
331
362
 
332
- function resolvePath(code, globals, parent, locals, cache) {
363
+ function resolvePath(code, globals, locals, cachePath) {
333
364
  const args = code.slice(1);
334
365
  const [head, ...tail] = args;
335
366
 
336
- let { type, result } = reference(head, globals, parent, locals);
367
+ let { type, result } = reference(head, globals, locals);
337
368
 
338
369
  if (tail.length > 0) {
339
370
  // If the result is a traversal, we can safely extend it
@@ -352,9 +383,9 @@ function resolvePath(code, globals, parent, locals, cache) {
352
383
  }
353
384
  }
354
385
 
355
- if (type === REFERENCE_EXTERNAL && cache !== null) {
356
- // Cache external path
357
- return cachePath(result, cache);
386
+ if (type === REFERENCE_EXTERNAL && cachePath) {
387
+ // External references should be cached
388
+ return cacheExternalPath(result, cachePath);
358
389
  }
359
390
 
360
391
  return result;
@@ -1,4 +1,4 @@
1
- // @generated by Peggy 5.0.6.
1
+ // @generated by Peggy 5.1.0.
2
2
  //
3
3
  // https://peggyjs.org/
4
4
 
@@ -341,9 +341,10 @@ function makeMerge(spreads, location) {
341
341
 
342
342
  for (const spread of spreads) {
343
343
  if (spread[0] === ops.object) {
344
- topEntries.push(...spread.slice(1));
344
+ const spreadEntries = spread.slice(1);
345
+ topEntries.push(...spreadEntries);
345
346
  // Also add an object to the result with indirect references
346
- const indirectEntries = spread.slice(1).map((entry) => {
347
+ const indirectEntries = spreadEntries.map((entry) => {
347
348
  const [key] = entry;
348
349
  const parent = annotate([ops.inherited, 1], entry.location);
349
350
  const reference = annotate([parent, key], entry.location);
@@ -389,7 +390,8 @@ export function makeObject(entries, location) {
389
390
  if (key === markers.spread) {
390
391
  if (value[0] === ops.object) {
391
392
  // Spread of an object; fold into current object
392
- currentEntries.push(...value.slice(1));
393
+ const spreadEntries = value.slice(1);
394
+ currentEntries.push(...spreadEntries);
393
395
  } else {
394
396
  // Spread of a tree; accumulate
395
397
  if (currentEntries.length > 0) {
@@ -0,0 +1,18 @@
1
+ import { extension, trailingSlash } from "@weborigami/async-tree";
2
+
3
+ // Return a function that adds the given extension
4
+ export default function addExtensionKeyFn(resultExtension) {
5
+ const keyFn = (sourceValue, sourceKey) => {
6
+ if (sourceKey === undefined) {
7
+ return undefined;
8
+ }
9
+ const normalizedKey = trailingSlash.remove(sourceKey);
10
+ const sourceExtension = extension.extname(normalizedKey);
11
+ const resultKey = sourceExtension
12
+ ? extension.replace(normalizedKey, sourceExtension, resultExtension)
13
+ : normalizedKey + resultExtension;
14
+ return resultKey;
15
+ };
16
+ keyFn.needsSourceValue = false;
17
+ return keyFn;
18
+ }
@@ -0,0 +1,54 @@
1
+ import { AsyncMap, isUnpackable, Tree } from "@weborigami/async-tree";
2
+ import addExtensionKeyFn from "./addExtensionKeyFn.js";
3
+ import zip_handler from "./zip_handler.js";
4
+
5
+ /**
6
+ * Handler for EPUB files
7
+ */
8
+ const epub_handler = {
9
+ mediaType: "application/epub+zip",
10
+
11
+ /**
12
+ * Package a tree of files as an EPUB file in Buffer form.
13
+ *
14
+ * This calls the pack() method for ZIP files, but ensures the `mimetype` file
15
+ * is the first file in the package -- a requirement for EPUB files.
16
+ *
17
+ * @param {import("@weborigami/async-tree").Maplike} maplike
18
+ */
19
+ async pack(maplike) {
20
+ if (isUnpackable(maplike)) {
21
+ maplike = await maplike.unpack();
22
+ }
23
+ const tree = Tree.from(maplike, { deep: true });
24
+ return zip_handler.pack(mimetypeFirst(tree));
25
+ },
26
+
27
+ async unpack(buffer, options) {
28
+ return zip_handler.unpack(buffer, options);
29
+ },
30
+ };
31
+
32
+ /** @type {any} */ (epub_handler.pack).key = addExtensionKeyFn(".epub");
33
+
34
+ export default epub_handler;
35
+
36
+ // A tree with its `mimetype` file first
37
+ function mimetypeFirst(tree) {
38
+ return Object.assign(new AsyncMap(), {
39
+ async get(key) {
40
+ return tree.get(key);
41
+ },
42
+
43
+ async *keys() {
44
+ const keys = await Tree.keys(tree);
45
+ // Move `mimetype` (if present) to the front of the list.
46
+ const index = keys.indexOf("mimetype");
47
+ if (index >= 0) {
48
+ keys.splice(index, 1);
49
+ keys.unshift("mimetype");
50
+ }
51
+ yield* keys;
52
+ },
53
+ });
54
+ }
@@ -1,4 +1,5 @@
1
1
  import { getParent, toString } from "@weborigami/async-tree";
2
+ import SystemCacheMap from "../runtime/SystemCacheMap.js";
2
3
 
3
4
  /**
4
5
  * Given packed source text and a handler's options, return a source
@@ -9,6 +10,7 @@ export default function getSource(packed, options = {}) {
9
10
 
10
11
  // Try to determine a URL for error messages
11
12
  const sourceName = options.key;
13
+ let cachePath;
12
14
  let url;
13
15
  if (sourceName) {
14
16
  if (/** @type {any} */ (parent)?.url) {
@@ -24,6 +26,11 @@ export default function getSource(packed, options = {}) {
24
26
  parentHref += "/";
25
27
  }
26
28
  url = new URL(sourceName, parentHref);
29
+
30
+ const parentPath = /** @type {any} */ (parent).path;
31
+ cachePath = SystemCacheMap.joinPath(parentPath, sourceName);
32
+ } else {
33
+ cachePath = sourceName;
27
34
  }
28
35
  }
29
36
 
@@ -32,5 +39,9 @@ export default function getSource(packed, options = {}) {
32
39
  name: options.key,
33
40
  url,
34
41
  };
42
+ if (cachePath) {
43
+ source.cachePath = cachePath;
44
+ }
45
+
35
46
  return source;
36
47
  }
@@ -20,6 +20,7 @@ export { default as txt_handler } from "./txt_handler.js";
20
20
 
21
21
  export { default as css_handler } from "./css_handler.js";
22
22
  export { default as csv_handler } from "./csv_handler.js";
23
+ export { default as epub_handler } from "./epub_handler.js";
23
24
  export { default as htm_handler } from "./htm_handler.js";
24
25
  export { default as html_handler } from "./html_handler.js";
25
26
  export { default as jpeg_handler } from "./jpeg_handler.js";
@@ -33,3 +34,4 @@ export { default as wasm_handler } from "./wasm_handler.js";
33
34
  export { default as xhtml_handler } from "./xhtml_handler.js";
34
35
  export { default as yaml_handler } from "./yaml_handler.js";
35
36
  export { default as yml_handler } from "./yml_handler.js";
37
+ export { default as zip_handler } from "./zip_handler.js";
@@ -1,2 +1,2 @@
1
1
  // .htm is a synonynm for .html
2
- export { html_handler as default } from "./handlers.js";
2
+ export { default } from "./html_handler.js";
@@ -10,23 +10,32 @@ export default {
10
10
  /** @type {import("@weborigami/async-tree").UnpackFunction} */
11
11
  async unpack(packed, options = {}) {
12
12
  const { key, parent } = options;
13
- if (!(parent && "import" in parent)) {
13
+ let importTarget = parent;
14
+ while (importTarget.source && !importTarget.import) {
15
+ importTarget = importTarget.source;
16
+ }
17
+
18
+ if (!importTarget) {
14
19
  throw new TypeError(
15
20
  "The parent tree must support importing modules to unpack JavaScript files.",
16
21
  );
17
22
  }
18
23
 
19
- const object = await /** @type {any} */ (parent).import?.(key);
24
+ const object = await importTarget.import?.(key);
20
25
 
21
26
  let bound;
27
+ let bindTarget = parent;
28
+ while (bindTarget.result) {
29
+ bindTarget = bindTarget.result;
30
+ }
22
31
  if ("default" in object) {
23
32
  // Module with a default export; return that.
24
- bound = bindToParent(object.default, parent);
33
+ bound = bindToParent(object.default, bindTarget);
25
34
  } else {
26
35
  // Module with multiple named exports.
27
36
  bound = {};
28
37
  for (const [name, value] of Object.entries(object)) {
29
- bound[name] = bindToParent(value, parent);
38
+ bound[name] = bindToParent(value, bindTarget);
30
39
  }
31
40
  }
32
41
 
@@ -0,0 +1,15 @@
1
+ {
2
+ "application/json": ".json",
3
+ "application/wasm": ".wasm",
4
+ "application/xhtml+xml": ".xhtml",
5
+ "application/xml": ".xml",
6
+ "application/yaml": ".yaml",
7
+ "image/jpeg": ".jpg",
8
+ "image/jpg": ".jpg",
9
+ "text/css": ".css",
10
+ "text/csv": ".csv",
11
+ "text/html": ".html",
12
+ "text/plain": ".txt",
13
+ "text/tsv": ".tsv",
14
+ "text/xml": ".xml"
15
+ }
@@ -1,7 +1,9 @@
1
- import { getParent, setParent } from "@weborigami/async-tree";
1
+ import { getParent } from "@weborigami/async-tree";
2
2
  import * as compile from "../compiler/compile.js";
3
- import projectGlobals from "../project/projectGlobals.js";
3
+ import coreGlobals from "../project/coreGlobals.js";
4
+ import getGlobalsForTree from "../project/getGlobalsForTree.js";
4
5
  import getSource from "./getSource.js";
6
+ import processOriExport from "./processOriExport.js";
5
7
 
6
8
  /**
7
9
  * An Origami expression file
@@ -18,7 +20,8 @@ export default {
18
20
 
19
21
  // Compile the source code as an Origami program
20
22
  const compiler = options.compiler ?? compile.program;
21
- const globals = options.globals ?? (await projectGlobals(parent));
23
+ const globals =
24
+ options.globals ?? getGlobalsForTree(parent) ?? (await coreGlobals());
22
25
  const fn = compiler(source, {
23
26
  globals,
24
27
  mode: "program",
@@ -26,11 +29,9 @@ export default {
26
29
  });
27
30
 
28
31
  // Evaluate the program
29
- const result = await fn();
32
+ let result = await fn();
30
33
 
31
- if (parent) {
32
- setParent(result, parent);
33
- }
34
+ result = processOriExport(result, source, parent);
34
35
 
35
36
  return result;
36
37
  },
@@ -1,7 +1,10 @@
1
- import { extension, getParent, trailingSlash } from "@weborigami/async-tree";
1
+ import { extension, getParent } from "@weborigami/async-tree";
2
2
  import * as compile from "../compiler/compile.js";
3
- import projectGlobals from "../project/projectGlobals.js";
3
+ import coreGlobals from "../project/coreGlobals.js";
4
+ import getGlobalsForTree from "../project/getGlobalsForTree.js";
5
+ import addExtensionKeyFn from "./addExtensionKeyFn.js";
4
6
  import getSource from "./getSource.js";
7
+ import processOriExport from "./processOriExport.js";
5
8
 
6
9
  /**
7
10
  * An Origami template document: a plain text file that contains Origami
@@ -16,41 +19,29 @@ export default {
16
19
  const source = getSource(packed, options);
17
20
 
18
21
  // Compile the source code as an Origami template document
19
- const globals = options.globals ?? (await projectGlobals(parent));
20
- const defineFn = compile.templateDocument(source, {
22
+ const globals =
23
+ options.globals ?? getGlobalsForTree(parent) ?? (await coreGlobals());
24
+ const fn = compile.templateDocument(source, {
21
25
  front: options.front,
22
26
  globals,
23
27
  mode: "program",
24
28
  parent,
25
29
  });
26
30
 
27
- // Invoke the definition to get back the template function
28
- const result = await defineFn();
31
+ // Invoke the definition to get back the template function or object
32
+ let result = await fn();
29
33
 
30
- const key = options.key;
31
- const resultExtension = key ? extension.extname(key) : null;
32
- if (resultExtension && Object.isExtensible(result)) {
33
- // Add sidecar function so this template can be used in a map.
34
- result.key = addExtension(resultExtension);
34
+ result = processOriExport(result, source, parent);
35
+
36
+ if (result instanceof Function) {
37
+ const key = options.key;
38
+ const resultExtension = key ? extension.extname(key) : null;
39
+ if (resultExtension && Object.isExtensible(result)) {
40
+ // Add sidecar function so this template can be used in a map.
41
+ result.key = addExtensionKeyFn(resultExtension);
42
+ }
35
43
  }
36
44
 
37
45
  return result;
38
46
  },
39
47
  };
40
-
41
- // Return a function that adds the given extension
42
- function addExtension(resultExtension) {
43
- const keyFn = (sourceValue, sourceKey) => {
44
- if (sourceKey === undefined) {
45
- return undefined;
46
- }
47
- const normalizedKey = trailingSlash.remove(sourceKey);
48
- const sourceExtension = extension.extname(normalizedKey);
49
- const resultKey = sourceExtension
50
- ? extension.replace(normalizedKey, sourceExtension, resultExtension)
51
- : normalizedKey + resultExtension;
52
- return resultKey;
53
- };
54
- keyFn.needsSourceValue = false;
55
- return keyFn;
56
- }
@@ -0,0 +1,17 @@
1
+ import { setParent, trailingSlash } from "@weborigami/async-tree";
2
+ import enableValueCaching from "../runtime/enableValueCaching.js";
3
+
4
+ /**
5
+ * Given an object that's the top-level result of an Origami file, perform any
6
+ * necessary processing.
7
+ */
8
+ export default function processOriExport(value, source, parent) {
9
+ setParent(value, parent);
10
+
11
+ if (source.cachePath) {
12
+ const cachePath = trailingSlash.add(source.cachePath);
13
+ value = enableValueCaching(value, cachePath);
14
+ }
15
+
16
+ return value;
17
+ }
@@ -1,7 +1,7 @@
1
1
  import { symbols, toString } from "@weborigami/async-tree";
2
2
 
3
3
  export default {
4
- mediaType: "text/csv",
4
+ mediaType: "text/tsv",
5
5
 
6
6
  unpack(packed, options = {}) {
7
7
  const parent = options.parent ?? null;