@quillmark/quiver 0.6.0 → 0.8.0

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/PROGRAM.md CHANGED
@@ -465,7 +465,7 @@ const result = quill.render(doc, { format: "pdf" });
465
465
  test runners wire their own loops against the main API.
466
466
 
467
467
  **Dependencies:**
468
- - Peer: `@quillmark/wasm@>=0.59.0` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
468
+ - Peer: `@quillmark/wasm@>=0.71.0` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
469
469
  - Runtime: `fflate ^0.8.2` for zip read/write (Node + browser)
470
470
  - Dev-only: `node:crypto` (MD5 hashing in `build()` — never reached at runtime)
471
471
  - No test-runner peer dependency; `/testing` uses `node:test` (built-in)
package/README.md CHANGED
@@ -50,6 +50,48 @@ surface on publish, not on the consumer's build. The harness uses
50
50
  required. If you prefer vitest/jest/mocha, write a 12-line loop against
51
51
  the main API instead.
52
52
 
53
+ ## Manual validation (rendering samples)
54
+
55
+ The CI harness proves every quill _compiles_; it does not produce output a
56
+ human can look at. To eyeball real renders, drop an `example.md` next to a
57
+ quill's template (`quills/<name>/<x.y.z>/example.md`) and run the
58
+ `@quillmark/quiver/preview` helper:
59
+
60
+ ```ts
61
+ // scripts/preview.ts — run with: node --experimental-strip-types scripts/preview.ts
62
+ import { Quillmark, Document } from "@quillmark/wasm";
63
+ import { renderQuiverSamples } from "@quillmark/quiver/preview";
64
+
65
+ await renderQuiverSamples(import.meta.url, {
66
+ engine: new Quillmark(),
67
+ Document,
68
+ });
69
+ // → writes ./preview/<name>@<version>.<fmt> + index.html
70
+ ```
71
+
72
+ It renders every quill's `example.md`, writes the artifacts to `outDir`
73
+ (default `preview/`), and emits an `index.html` gallery. A `.gitignore` is
74
+ written into `outDir` so the generated artifacts are never accidentally
75
+ committed. Quills without an `example.md` are skipped; a quill that throws
76
+ is recorded as failed — with every diagnostic, not just the first —
77
+ without aborting the run, so one broken quill never hides the rest.
78
+
79
+ To iterate on a subset, pass `include` / `exclude` (each entry matches a
80
+ quill name or canonical ref):
81
+
82
+ ```ts
83
+ await renderQuiverSamples(import.meta.url, {
84
+ engine: new Quillmark(),
85
+ Document,
86
+ exclude: ["broken-quill"], // or: include: ["memo@1.0.0"]
87
+ });
88
+ ```
89
+
90
+ > **Linking the source repo?** `@quillmark/quiver/preview` resolves to
91
+ > `./dist/preview.js`, which only exists after `npm install && npm run build`
92
+ > in the `@quillmark/quiver` checkout. If you `npm link` it and see
93
+ > `Cannot find module './dist/preview.js'`, build the linked package first.
94
+
53
95
  ## Consuming a quiver (Node)
54
96
 
55
97
  ```ts
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Minimal structural types matching @quillmark/wasm >=0.59.0.
2
+ * Minimal structural types matching @quillmark/wasm >=0.79.0.
3
3
  *
4
4
  * Shape:
5
5
  * class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Minimal structural types matching @quillmark/wasm >=0.59.0.
2
+ * Minimal structural types matching @quillmark/wasm >=0.79.0.
3
3
  *
4
4
  * Shape:
5
5
  * class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
package/dist/node.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Importing this module is the consumer's explicit declaration of intent:
5
5
  * "I am running in Node and want the Node-only Quiver factories." It exposes
6
6
  * the same `Quiver` class as the main entry, augmented with `fromDir`,
7
- * `fromPackage`, `fromBuiltDir`, and `build` static methods.
7
+ * `fromPackage`, `fromBuiltDir`, `build`, and `buildPackage` static methods.
8
8
  *
9
9
  * Side effect: at module evaluation time, the Node-only static methods are
10
10
  * installed on the shared `Quiver` constructor. Any other module that already
@@ -63,6 +63,16 @@ type NodeQuiverStatics = {
63
63
  * on I/O failures.
64
64
  */
65
65
  build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
66
+ /**
67
+ * Resolves an npm specifier against `node_modules` and builds the source
68
+ * layout at the package root. The resolved package must have `Quiver.yaml`
69
+ * at its root. Symmetric to `fromPackage` but writes a runtime build
70
+ * artifact to outDir instead of loading.
71
+ *
72
+ * Throws `transport_error` on resolution/I/O failure, `quiver_invalid` on
73
+ * source validation failures.
74
+ */
75
+ buildPackage(specifier: string, outDir: string, opts?: BuildOptions): Promise<void>;
66
76
  };
67
77
  export type Quiver = Base;
68
78
  export declare const Quiver: typeof Base & NodeQuiverStatics;
package/dist/node.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Importing this module is the consumer's explicit declaration of intent:
5
5
  * "I am running in Node and want the Node-only Quiver factories." It exposes
6
6
  * the same `Quiver` class as the main entry, augmented with `fromDir`,
7
- * `fromPackage`, `fromBuiltDir`, and `build` static methods.
7
+ * `fromPackage`, `fromBuiltDir`, `build`, and `buildPackage` static methods.
8
8
  *
9
9
  * Side effect: at module evaluation time, the Node-only static methods are
10
10
  * installed on the shared `Quiver` constructor. Any other module that already
@@ -55,6 +55,17 @@ Quiver.fromPackage = async function fromPackage(specifier) {
55
55
  Quiver.build = async function build(sourceDir, outDir, opts) {
56
56
  return buildQuiver(sourceDir, outDir, opts);
57
57
  };
58
+ Quiver.buildPackage = async function buildPackage(specifier, outDir, opts) {
59
+ const req = createRequire(import.meta.url);
60
+ let yamlPath;
61
+ try {
62
+ yamlPath = req.resolve(`${specifier}/Quiver.yaml`);
63
+ }
64
+ catch (err) {
65
+ throw new QuiverError("transport_error", `Failed to resolve quiver package "${specifier}": ${err.message}`, { cause: err });
66
+ }
67
+ return buildQuiver(dirname(yamlPath), outDir, opts);
68
+ };
58
69
  // ---------------------------------------------------------------------------
59
70
  // 3. Re-export the rest of the public surface so consumers get one import.
60
71
  // ---------------------------------------------------------------------------
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Manual-validation helper for Quiver authors.
3
+ *
4
+ * `runQuiverTests` (`@quillmark/quiver/testing`) proves every quill *compiles*
5
+ * — it never produces a rendered artifact a human can look at. This module
6
+ * closes that gap: it renders each quill's bundled `example.md`, writes the
7
+ * artifacts to a directory, and emits an `index.html` gallery so an author
8
+ * can eyeball real output before publishing.
9
+ *
10
+ * Node-only: writes files and loads a source quiver from disk.
11
+ *
12
+ * Usage (place a script next to your Quiver.yaml):
13
+ *
14
+ * import { Quillmark, Document } from "@quillmark/wasm";
15
+ * import { renderQuiverSamples } from "@quillmark/quiver/preview";
16
+ *
17
+ * await renderQuiverSamples(import.meta.url, {
18
+ * engine: new Quillmark(),
19
+ * Document,
20
+ * });
21
+ * // → open ./preview/index.html
22
+ *
23
+ * The example document is the `example.md` file inside each quill version
24
+ * directory (`quills/<name>/<version>/example.md`). Quills without one are
25
+ * skipped, not failed.
26
+ *
27
+ * A `.gitignore` is written into `outDir` so the generated artifacts are not
28
+ * accidentally committed.
29
+ */
30
+ import type { QuillmarkLike } from "./engine-types.js";
31
+ /**
32
+ * Structural shape of the `Document` class from `@quillmark/wasm`. Only the
33
+ * `fromMarkdown` factory is used; passing the real class satisfies this.
34
+ */
35
+ export interface DocumentFactoryLike {
36
+ fromMarkdown(markdown: string): unknown;
37
+ }
38
+ export interface RenderQuiverSamplesOptions {
39
+ /** Quillmark engine instance (`new Quillmark()` from `@quillmark/wasm`). */
40
+ engine: QuillmarkLike;
41
+ /** The `Document` class from `@quillmark/wasm`. */
42
+ Document: DocumentFactoryLike;
43
+ /** Directory to write rendered artifacts into. Default: `preview`. */
44
+ outDir?: string;
45
+ /** Force an output format (`pdf`/`svg`/`png`/`txt`). Default: engine's choice. */
46
+ format?: string;
47
+ /** Suppress the console summary. Default: false. */
48
+ quiet?: boolean;
49
+ /**
50
+ * Render only these quills. Each entry matches a quill name (`"memo"`) or a
51
+ * canonical ref (`"memo@1.0.0"`). Omit to render all quills.
52
+ */
53
+ include?: string[];
54
+ /**
55
+ * Skip these quills. Each entry matches a quill name (`"memo"`) or a
56
+ * canonical ref (`"memo@1.0.0"`). Applied after `include`.
57
+ */
58
+ exclude?: string[];
59
+ }
60
+ /** Per-quill outcome returned by `renderQuiverSamples`. */
61
+ export interface RenderedSample {
62
+ /** Canonical ref, e.g. `"memo@1.0.0"`. */
63
+ ref: string;
64
+ /** `rendered` — artifacts written; `skipped` — no example; `failed` — error. */
65
+ status: "rendered" | "skipped" | "failed";
66
+ /** Artifact filenames written under `outDir` (relative). */
67
+ files: string[];
68
+ /** Render warnings, formatted `"severity: message"`. */
69
+ warnings: string[];
70
+ /**
71
+ * Why the quill was skipped or failed. Empty when rendered. A failed render
72
+ * carries every diagnostic from the engine, not just the first.
73
+ */
74
+ reasons: string[];
75
+ }
76
+ /**
77
+ * Renders every quill's `example.md` and writes the artifacts plus an
78
+ * `index.html` gallery to `outDir`.
79
+ *
80
+ * Does NOT fail fast: a quill that throws is recorded as `failed` and the
81
+ * run continues, so one broken quill never hides the others. Inspect the
82
+ * returned array (or `index.html`) for the full picture.
83
+ *
84
+ * @param metaUrlOrDir `import.meta.url` when called from the quiver root, or
85
+ * an absolute path to the source quiver directory.
86
+ */
87
+ export declare function renderQuiverSamples(metaUrlOrDir: string, opts: RenderQuiverSamplesOptions): Promise<RenderedSample[]>;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Manual-validation helper for Quiver authors.
3
+ *
4
+ * `runQuiverTests` (`@quillmark/quiver/testing`) proves every quill *compiles*
5
+ * — it never produces a rendered artifact a human can look at. This module
6
+ * closes that gap: it renders each quill's bundled `example.md`, writes the
7
+ * artifacts to a directory, and emits an `index.html` gallery so an author
8
+ * can eyeball real output before publishing.
9
+ *
10
+ * Node-only: writes files and loads a source quiver from disk.
11
+ *
12
+ * Usage (place a script next to your Quiver.yaml):
13
+ *
14
+ * import { Quillmark, Document } from "@quillmark/wasm";
15
+ * import { renderQuiverSamples } from "@quillmark/quiver/preview";
16
+ *
17
+ * await renderQuiverSamples(import.meta.url, {
18
+ * engine: new Quillmark(),
19
+ * Document,
20
+ * });
21
+ * // → open ./preview/index.html
22
+ *
23
+ * The example document is the `example.md` file inside each quill version
24
+ * directory (`quills/<name>/<version>/example.md`). Quills without one are
25
+ * skipped, not failed.
26
+ *
27
+ * A `.gitignore` is written into `outDir` so the generated artifacts are not
28
+ * accidentally committed.
29
+ */
30
+ import { mkdir, writeFile } from "node:fs/promises";
31
+ import { join } from "node:path";
32
+ import { Quiver } from "./node.js";
33
+ /** The `example.md` filename looked up inside each quill version directory. */
34
+ const EXAMPLE_FILE = "example.md";
35
+ /** Default directory rendered artifacts are written to. */
36
+ const DEFAULT_OUT_DIR = "preview";
37
+ /**
38
+ * Renders every quill's `example.md` and writes the artifacts plus an
39
+ * `index.html` gallery to `outDir`.
40
+ *
41
+ * Does NOT fail fast: a quill that throws is recorded as `failed` and the
42
+ * run continues, so one broken quill never hides the others. Inspect the
43
+ * returned array (or `index.html`) for the full picture.
44
+ *
45
+ * @param metaUrlOrDir `import.meta.url` when called from the quiver root, or
46
+ * an absolute path to the source quiver directory.
47
+ */
48
+ export async function renderQuiverSamples(metaUrlOrDir, opts) {
49
+ const outDir = opts.outDir ?? DEFAULT_OUT_DIR;
50
+ const quiver = await Quiver.fromDir(metaUrlOrDir);
51
+ await mkdir(outDir, { recursive: true });
52
+ const results = [];
53
+ for (const name of quiver.quillNames()) {
54
+ for (const version of quiver.versionsOf(name)) {
55
+ if (!isSelected(name, `${name}@${version}`, opts))
56
+ continue;
57
+ results.push(await renderOne(quiver, name, version, outDir, opts));
58
+ }
59
+ }
60
+ await writeFile(join(outDir, ".gitignore"), "*\n");
61
+ await writeFile(join(outDir, "index.html"), renderIndexHtml(quiver.name, results));
62
+ if (!opts.quiet)
63
+ printSummary(quiver.name, outDir, results);
64
+ return results;
65
+ }
66
+ /** Whether a quill passes the `include`/`exclude` filters. */
67
+ function isSelected(name, ref, opts) {
68
+ const matches = (list) => list.includes(name) || list.includes(ref);
69
+ if (opts.include && !matches(opts.include))
70
+ return false;
71
+ if (opts.exclude && matches(opts.exclude))
72
+ return false;
73
+ return true;
74
+ }
75
+ /**
76
+ * Formats a thrown render error into one string per diagnostic. `@quillmark/wasm`
77
+ * attaches a `diagnostics` array to its errors; fall back to the message when
78
+ * an error carries none.
79
+ */
80
+ function failureReasons(err) {
81
+ const diagnostics = err.diagnostics;
82
+ if (Array.isArray(diagnostics) && diagnostics.length > 0) {
83
+ return diagnostics.map((d) => `${d.severity}: ${d.message}`);
84
+ }
85
+ return [err.message];
86
+ }
87
+ async function renderOne(quiver, name, version, outDir, opts) {
88
+ const ref = `${name}@${version}`;
89
+ const tree = await quiver.loadTree(name, version);
90
+ const exampleBytes = tree.get(EXAMPLE_FILE);
91
+ if (exampleBytes === undefined) {
92
+ return {
93
+ ref,
94
+ status: "skipped",
95
+ files: [],
96
+ warnings: [],
97
+ reasons: [`no ${EXAMPLE_FILE} in quill directory`],
98
+ };
99
+ }
100
+ let result;
101
+ try {
102
+ const quill = await quiver.getQuill(ref, { engine: opts.engine });
103
+ const markdown = new TextDecoder().decode(exampleBytes);
104
+ const doc = opts.Document.fromMarkdown(markdown);
105
+ result = quill.render(doc, opts.format ? { format: opts.format } : undefined);
106
+ }
107
+ catch (err) {
108
+ return {
109
+ ref,
110
+ status: "failed",
111
+ files: [],
112
+ warnings: [],
113
+ reasons: failureReasons(err),
114
+ };
115
+ }
116
+ const warnings = (result.warnings ?? []).map((w) => `${w.severity}: ${w.message}`);
117
+ const artifacts = result.artifacts ?? [];
118
+ if (artifacts.length === 0) {
119
+ return {
120
+ ref,
121
+ status: "failed",
122
+ files: [],
123
+ warnings,
124
+ reasons: ["render produced no artifacts"],
125
+ };
126
+ }
127
+ const files = [];
128
+ for (let i = 0; i < artifacts.length; i++) {
129
+ const artifact = artifacts[i];
130
+ const suffix = artifacts.length > 1 ? `.${i}` : "";
131
+ const fileName = `${ref}${suffix}.${artifact.format}`;
132
+ await writeFile(join(outDir, fileName), artifact.bytes);
133
+ files.push(fileName);
134
+ }
135
+ return { ref, status: "rendered", files, warnings, reasons: [] };
136
+ }
137
+ function printSummary(quiverName, outDir, results) {
138
+ const count = (s) => results.filter((r) => r.status === s).length;
139
+ console.log(`\nQuiver "${quiverName}" — sample render`);
140
+ for (const r of results) {
141
+ const detail = r.status === "rendered" ? r.files.join(", ") : (r.reasons[0] ?? "");
142
+ console.log(` [${r.status.padEnd(8)}] ${r.ref}${detail ? ` — ${detail}` : ""}`);
143
+ for (const extra of r.reasons.slice(1))
144
+ console.log(` ${extra}`);
145
+ for (const w of r.warnings)
146
+ console.log(` ⚠ ${w}`);
147
+ }
148
+ console.log(`\n${count("rendered")} rendered, ${count("skipped")} skipped, ` +
149
+ `${count("failed")} failed`);
150
+ console.log(`Open ${join(outDir, "index.html")} to review.\n`);
151
+ }
152
+ function escapeHtml(text) {
153
+ return text
154
+ .replace(/&/g, "&amp;")
155
+ .replace(/</g, "&lt;")
156
+ .replace(/>/g, "&gt;")
157
+ .replace(/"/g, "&quot;");
158
+ }
159
+ function embedArtifact(fileName) {
160
+ const ext = fileName.slice(fileName.lastIndexOf(".") + 1).toLowerCase();
161
+ const src = escapeHtml(fileName);
162
+ if (ext === "pdf") {
163
+ return `<iframe class="art" src="${src}" title="${src}"></iframe>`;
164
+ }
165
+ if (ext === "png" || ext === "svg") {
166
+ return `<img class="art" src="${src}" alt="${src}" />`;
167
+ }
168
+ return `<a href="${src}">${src}</a>`;
169
+ }
170
+ function renderIndexHtml(quiverName, results) {
171
+ const cards = results
172
+ .map((r) => {
173
+ const body = r.status === "rendered"
174
+ ? r.files.map(embedArtifact).join("\n")
175
+ : `<ul class="reasons">${r.reasons
176
+ .map((reason) => `<li>${escapeHtml(reason)}</li>`)
177
+ .join("")}</ul>`;
178
+ const warnings = r.warnings.length
179
+ ? `<ul class="warnings">${r.warnings
180
+ .map((w) => `<li>${escapeHtml(w)}</li>`)
181
+ .join("")}</ul>`
182
+ : "";
183
+ return `<section class="card ${r.status}">
184
+ <h2>${escapeHtml(r.ref)} <span class="badge">${r.status}</span></h2>
185
+ ${warnings}
186
+ ${body}
187
+ </section>`;
188
+ })
189
+ .join("\n");
190
+ return `<!doctype html>
191
+ <html lang="en">
192
+ <head>
193
+ <meta charset="utf-8" />
194
+ <title>Quiver preview — ${escapeHtml(quiverName)}</title>
195
+ <style>
196
+ body { font-family: system-ui, sans-serif; margin: 2rem; background: #fafafa; }
197
+ h1 { font-size: 1.4rem; }
198
+ .card { background: #fff; border: 1px solid #ddd; border-radius: 8px;
199
+ padding: 1rem; margin: 1rem 0; }
200
+ .card.failed { border-color: #e0b4b4; }
201
+ .card.skipped { opacity: 0.7; }
202
+ h2 { font-size: 1rem; margin: 0 0 0.5rem; }
203
+ .badge { font-size: 0.7rem; text-transform: uppercase; background: #eee;
204
+ border-radius: 4px; padding: 2px 6px; vertical-align: middle; }
205
+ .failed .badge { background: #f3d2d2; }
206
+ .art { width: 100%; height: 600px; border: 1px solid #eee; }
207
+ img.art { height: auto; }
208
+ .reasons { color: #a33; font-style: italic; }
209
+ .warnings { color: #96690a; font-size: 0.85rem; }
210
+ </style>
211
+ </head>
212
+ <body>
213
+ <h1>Quiver preview — ${escapeHtml(quiverName)}</h1>
214
+ ${cards}
215
+ </body>
216
+ </html>
217
+ `;
218
+ }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Quiver registry and build tooling for Quillmark",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/nibsbin/quillmark-quiver.git"
9
+ "url": "git+https://github.com/quillmark-org/quiver.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/nibsbin/quillmark-quiver/issues"
12
+ "url": "https://github.com/quillmark-org/quiver/issues"
13
13
  },
14
- "homepage": "https://github.com/nibsbin/quillmark-quiver#readme",
14
+ "homepage": "https://github.com/quillmark-org/quiver#readme",
15
15
  "keywords": [
16
16
  "quillmark",
17
17
  "quiver",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "author": "Quillmark Contributors",
22
22
  "engines": {
23
- "node": ">=18"
23
+ "node": ">=24"
24
24
  },
25
25
  "exports": {
26
26
  ".": {
@@ -34,11 +34,16 @@
34
34
  "./testing": {
35
35
  "types": "./dist/testing.d.ts",
36
36
  "import": "./dist/testing.js"
37
+ },
38
+ "./preview": {
39
+ "types": "./dist/preview.d.ts",
40
+ "import": "./dist/preview.js"
37
41
  }
38
42
  },
39
43
  "sideEffects": [
40
44
  "./dist/node.js",
41
- "./dist/testing.js"
45
+ "./dist/testing.js",
46
+ "./dist/preview.js"
42
47
  ],
43
48
  "files": [
44
49
  "dist",
@@ -46,14 +51,14 @@
46
51
  "README.md"
47
52
  ],
48
53
  "peerDependencies": {
49
- "@quillmark/wasm": ">=0.59.0"
54
+ "@quillmark/wasm": ">=0.79.0"
50
55
  },
51
56
  "dependencies": {
52
57
  "fflate": "^0.8.2",
53
58
  "yaml": "^2.8.3"
54
59
  },
55
60
  "devDependencies": {
56
- "@quillmark/wasm": "0.59.0",
61
+ "@quillmark/wasm": "^0.79.0",
57
62
  "@types/node": "^25.3.3",
58
63
  "typescript": "^5.9.3",
59
64
  "vitest": "^4.0.18"