@weborigami/language 0.7.0-beta.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.7.0-beta.1",
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",
@@ -11,7 +11,8 @@
11
11
  "typescript": "6.0.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.7.0-beta.1",
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
18
  "yaml": "2.9.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
+ }
@@ -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,7 +1,8 @@
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
3
  import coreGlobals from "../project/coreGlobals.js";
4
4
  import getGlobalsForTree from "../project/getGlobalsForTree.js";
5
+ import addExtensionKeyFn from "./addExtensionKeyFn.js";
5
6
  import getSource from "./getSource.js";
6
7
  import processOriExport from "./processOriExport.js";
7
8
 
@@ -37,27 +38,10 @@ export default {
37
38
  const resultExtension = key ? extension.extname(key) : null;
38
39
  if (resultExtension && Object.isExtensible(result)) {
39
40
  // Add sidecar function so this template can be used in a map.
40
- result.key = addExtension(resultExtension);
41
+ result.key = addExtensionKeyFn(resultExtension);
41
42
  }
42
43
  }
43
44
 
44
45
  return result;
45
46
  },
46
47
  };
47
-
48
- // Return a function that adds the given extension
49
- function addExtension(resultExtension) {
50
- const keyFn = (sourceValue, sourceKey) => {
51
- if (sourceKey === undefined) {
52
- return undefined;
53
- }
54
- const normalizedKey = trailingSlash.remove(sourceKey);
55
- const sourceExtension = extension.extname(normalizedKey);
56
- const resultKey = sourceExtension
57
- ? extension.replace(normalizedKey, sourceExtension, resultExtension)
58
- : normalizedKey + resultExtension;
59
- return resultKey;
60
- };
61
- keyFn.needsSourceValue = false;
62
- return keyFn;
63
- }
@@ -0,0 +1,112 @@
1
+ import { isUnpackable, SyncMap, Tree } from "@weborigami/async-tree";
2
+ import {
3
+ getGlobalsForTree,
4
+ HandleExtensionsTransform,
5
+ } from "@weborigami/language";
6
+ import Zip from "adm-zip";
7
+ import addExtensionKeyFn from "./addExtensionKeyFn.js";
8
+
9
+ /**
10
+ * Handler for ZIP files
11
+ */
12
+ const zip_handler = {
13
+ mediaType: "application/zip",
14
+
15
+ /**
16
+ * Pack a tree of files as a ZIP file in Buffer form.
17
+ *
18
+ * @param {import("@weborigami/async-tree").Maplike} maplike
19
+ */
20
+ async pack(maplike) {
21
+ // The ZIP file should leave the files in tree order.
22
+ const zip = new Zip({ noSort: true });
23
+
24
+ if (isUnpackable(maplike)) {
25
+ maplike = await maplike.unpack();
26
+ }
27
+ const tree = Tree.from(maplike, { deep: true });
28
+ const deflated = await Tree.deflatePaths(tree);
29
+ for (let [path, value] of deflated) {
30
+ if (typeof value === "function") {
31
+ value = value();
32
+ }
33
+ if (value instanceof Promise) {
34
+ value = await value;
35
+ } else if (value instanceof String) {
36
+ value = value.toString(); // adm-zip wants simple strings
37
+ }
38
+ zip.addFile(path, value);
39
+
40
+ // Special case for EPUB files, where `mimetype` must be uncompressed.
41
+ if (path === "mimetype") {
42
+ const entry = zip.getEntry(path);
43
+ entry.header.method = 0; // STORE (not DEFLATE)
44
+ }
45
+ }
46
+ const buffer = zip.toBuffer();
47
+ return buffer;
48
+ },
49
+
50
+ /**
51
+ * Unpack a ZIP file
52
+ */
53
+ async unpack(buffer, options) {
54
+ // Origami generally prefers keeping things as an Uint8Array or ArrayBuffer,
55
+ // but adm-zip only accepts a Buffer.
56
+ if (buffer instanceof Uint8Array || buffer instanceof ArrayBuffer) {
57
+ // @ts-ignore
58
+ buffer = Buffer.from(buffer);
59
+ }
60
+
61
+ const zip = new Zip(buffer);
62
+
63
+ const entries = zip.getEntries();
64
+ const filtered = entries.filter(
65
+ (entry) =>
66
+ !entry.entryName.startsWith("__MACOSX/") &&
67
+ !entry.entryName.endsWith("/"),
68
+ );
69
+ const deflated = Object.fromEntries(
70
+ filtered.map((entry) => [entry.entryName, () => entry.getData()]),
71
+ );
72
+
73
+ // The final tree will include extension handlers and have functions invoked
74
+ // to retrieve data from the ZIP file. While the base map is a SyncMap, the
75
+ // final tree will be async.
76
+ const classFn = HandleExtensionsTransform(
77
+ InvokeFunctionsTransform(SyncMap),
78
+ );
79
+ const result = await Tree.inflatePaths(deflated, { classFn });
80
+
81
+ const parent = options?.parent;
82
+ const globals = parent ? getGlobalsForTree(parent) : null;
83
+ if (globals) {
84
+ result.globals = globals;
85
+ }
86
+
87
+ return result;
88
+ },
89
+ };
90
+
91
+ /** @type {any} */ (zip_handler.pack).key = addExtensionKeyFn(".zip");
92
+
93
+ export default zip_handler;
94
+
95
+ function InvokeFunctionsTransform(Base) {
96
+ return class InvokeFunctions extends Base {
97
+ delete(key) {
98
+ return super.delete(key);
99
+ }
100
+
101
+ get(key) {
102
+ const value = super.get(key);
103
+ return typeof value === "function" ? value() : value;
104
+ }
105
+
106
+ set(key, value) {
107
+ return super.set(key, value);
108
+ }
109
+
110
+ trailingSlashKeys = true;
111
+ };
112
+ }
@@ -0,0 +1,27 @@
1
+ import Zip from "adm-zip";
2
+ import assert from "node:assert";
3
+ import { describe, test } from "node:test";
4
+ import epub_handler from "../../src/handlers/epub_handler.js";
5
+
6
+ describe("EPUB handler", () => {
7
+ test("ensures mimetype file comes first", async () => {
8
+ const tree = {
9
+ EPUB: {
10
+ "index.xhtml": "This is where the book content goes",
11
+ },
12
+ "META-INF": {
13
+ "container.xml": "This is where the metadata goes",
14
+ },
15
+ mimetype: "application/epub+zip",
16
+ };
17
+ const buffer = await epub_handler.pack(tree);
18
+ const unzipped = new Zip(buffer);
19
+ const entries = unzipped.getEntries();
20
+ const entryNames = entries.map((entry) => entry.entryName);
21
+ assert.deepEqual(entryNames, [
22
+ "mimetype",
23
+ "EPUB/index.xhtml",
24
+ "META-INF/container.xml",
25
+ ]);
26
+ });
27
+ });
Binary file
@@ -0,0 +1,45 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+ import Zip from "adm-zip";
3
+ import assert from "node:assert";
4
+ import fs from "node:fs/promises";
5
+ import { describe, test } from "node:test";
6
+ import zip_handler from "../../src/handlers/zip_handler.js";
7
+
8
+ describe("ZIP handler", () => {
9
+ test("creates a ZIP file as Buffer", async () => {
10
+ const tree = {
11
+ "ReadMe.md": "This is a ReadMe file.",
12
+ sub: {
13
+ "file.txt": "This is a text file in a subfolder.",
14
+ },
15
+ };
16
+ const buffer = await zip_handler.pack(tree);
17
+ const unzipped = new Zip(buffer);
18
+ const entries = unzipped.getEntries();
19
+ assert.equal(entries.length, 2);
20
+ assert.equal(entries[0].entryName, "ReadMe.md");
21
+ assert.equal(
22
+ entries[0].getData().toString("utf8"),
23
+ "This is a ReadMe file.",
24
+ );
25
+ assert.equal(entries[1].entryName, "sub/file.txt");
26
+ assert.equal(
27
+ entries[1].getData().toString("utf8"),
28
+ "This is a text file in a subfolder.",
29
+ );
30
+ });
31
+
32
+ test("reads a ZIP file", async () => {
33
+ const fixturePath = new URL("fixtures/test.zip", import.meta.url);
34
+ const buffer = await fs.readFile(fixturePath);
35
+ const tree = await zip_handler.unpack(buffer);
36
+ const plain = await Tree.plain(tree);
37
+ assert.deepEqual(Object.keys(plain), ["ReadMe.md", "sub"]);
38
+ assert.deepEqual(Object.keys(plain.sub), ["file.txt"]);
39
+ assert.equal(String(plain["ReadMe.md"]), "This is a ReadMe file.\n");
40
+ assert.equal(
41
+ String(plain.sub["file.txt"]),
42
+ "This is a text file in a subfolder.\n",
43
+ );
44
+ });
45
+ });