@uniweb/press 0.3.0 → 0.4.1

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/README.md CHANGED
@@ -1,76 +1,53 @@
1
1
  # @uniweb/press
2
2
 
3
- **Alternate outputs from Uniweb content.** A Uniweb site is pure content — markdown files, parsed into a guaranteed shape, delivered to React components. Press is a framework that lets foundations turn that content into downloadable files (Word, Excel, PDF, anything else) using the same section components that render the website. One source, many outputs.
3
+ **Downloadable files from Uniweb content.** A Uniweb site is, in a useful sense, a URL that represents a document — titles, paragraphs, tables, lists, tagged data blocks, all already parsed and available to section components. Press lets those same section components turn that content into downloadable files: Word documents, spreadsheets, typeset PDFs, and whatever else a foundation wants to support. One source, many outputs.
4
4
 
5
5
  ```bash
6
6
  npm install @uniweb/press
7
7
  ```
8
8
 
9
- ## Why
9
+ ## What Press is
10
10
 
11
- A Uniweb site is, in a useful sense, a URL that represents a document. Titles, paragraphs, tables, lists, tagged data blocks — all already parsed, all already available to section components. With Loom and dynamic data fetching, that document can be live and hierarchical: a report whose sections instantiate against a CV profile, a publication list that updates automatically, a program whose entries are filtered per department. The content is right there.
11
+ Press is the **output layer of the Uniweb ecosystem**. It's a small registration layer (what section components call into to opt in to a format) plus a set of format adapters (the things that know how to produce `.docx`, `.typ`, `.xlsx` files). The registration layer is format-agnostic; the adapters are format-specific and dynamic-loaded, so a foundation only pays for the formats it actually offers.
12
12
 
13
- Press is the output layer of that system. It gives foundations:
13
+ The core insight that makes Press tractable is that Uniweb has already done the semantic work. Press doesn't parse markdown, resolve Loom expressions, or fetch data — by the time a section component renders, `content.paragraphs`, `content.title`, and everything else are already concrete values. Press just walks what's there and emits.
14
14
 
15
- - A **registration hook** (`useDocumentOutput`) so a section component can say "here is what I produce for this format."
16
- - A **compile hook** (`useDocumentCompile`) that walks all registered sections in a page and produces a `Blob` for the requested format.
17
- - A **download utility** (`triggerDownload`) for saving the Blob to a file.
18
- - A **docx toolkit** — React builder components and a lazy-loaded format adapter — for the most common output format.
15
+ Three properties follow from that design:
19
16
 
20
- The core is **format-agnostic**. Press ships a docx toolkit today, and xlsx and pdf toolkits are on the roadmap. But you can use Press right now to generate any format you want by writing your own adapter against `@uniweb/press/ir`. Press is a framework for alternate outputs; the docx toolkit is one implementation of it.
21
-
22
- ## Ways to use it
23
-
24
- Press doesn't prescribe what the site is *for*. Three shapes are all first-class, and you pick based on what you're building. See [concepts](./docs/concepts.md) for the full discussion.
25
-
26
- **1. Interactive report + downloads.** A live web document — navigable, themed, possibly with dynamic data and Loom-instantiated content — that also offers one or more download buttons. Readers browse on screen; those who need a file click Download. This is the flagship case for annual reports, CVs, program guides, and anything else that wants to exist as both a URL and a document.
27
-
28
- **2. Multi-format exports.** The same site registers fragments for several formats at once — docx for the narrative, xlsx for the tabular data, a custom JSON export for machine consumers. Each section chooses which formats it contributes to. Compilation is per-format and lazy: a user clicks "Download xlsx" and only the xlsx adapter loads.
29
-
30
- **3. Headless export.** Sections register fragments and return `null`. The site has no visible output — it exists purely as a compile target for an automated pipeline, an admin tool, or a generator UI whose only job is to produce files. Press works here because registration is decoupled from rendering; a component that returns `null` still contributes to `compile()`.
31
-
32
- For cases 1 and 2, you also choose **how the preview works**:
33
-
34
- - **JSX-as-preview (recommended).** Press's `/docx` builder components emit ordinary HTML (`<p>`, `<h1>`, `<span>` with `data-*` attributes). The exact same JSX serves as the browser preview *and* as the source the compile pipeline walks. One tree, zero drift, no extra library. Your section is already rendering JSX — using Press builders costs nothing more. You can also split: Kit components for a richer themed preview, Press builders for the registered docx fragment, mirroring the same structure.
35
- - **Compiled-blob preview via `docx-preview`.** Instead of previewing the *content*, preview the *compiled file*. Render whatever you want (or nothing) in the component, then feed the Blob from `compile('docx')` into [`docx-preview`](https://github.com/VolodymyrBaydalka/docxjs) inside a sandboxed iframe to see Word's actual formatting. Useful as a cross-check view or when the downloaded file's exact appearance matters more than a browseable web view. Pays the `docx-preview` library cost; see the [preview pattern guide](./docs/guides/preview-pattern.md).
36
-
37
- The two strategies compose — a foundation can do JSX-as-preview for normal reading and *also* offer a "Preview compiled file" button that opens the docx-preview iframe.
17
+ - **Same source, many outputs.** The JSX a section renders for the browser can be the *same* JSX Press compiles into a Word doc or a PDF. No duplication, no drift between preview and file.
18
+ - **Frontend-first.** Most formats compile entirely in the browser. When a format needs a backend (Typst PDFs, future LaTeX), it's an escape hatch — not the architecture.
19
+ - **Additive formats.** Adding a new format is a new adapter module plus an entry in a map. Nothing else in Press needs to change.
38
20
 
39
21
  ## Hello world
40
22
 
41
- A Uniweb section component that renders both a preview and a registered docx fragment from the same JSX:
23
+ A section component that renders as a preview *and* registers itself for docx compilation, using the same JSX for both:
42
24
 
43
25
  ```jsx
44
- // src/sections/Cover/index.jsx
45
26
  import { useDocumentOutput } from '@uniweb/press'
46
27
  import { H1, H2, Paragraphs } from '@uniweb/press/docx'
47
28
 
48
29
  function Cover({ content, block }) {
49
- const { title, subtitle, paragraphs } = content
50
-
51
30
  const body = (
52
31
  <>
53
- <H1 data={title} />
54
- <H2 data={subtitle} />
55
- <Paragraphs data={paragraphs} />
32
+ <H1 data={content.title} />
33
+ <H2 data={content.subtitle} />
34
+ <Paragraphs data={content.paragraphs} />
56
35
  </>
57
36
  )
58
37
 
59
38
  useDocumentOutput(block, 'docx', body)
60
39
 
61
- return <div className="max-w-4xl mx-auto">{body}</div>
40
+ return <div className='max-w-4xl mx-auto'>{body}</div>
62
41
  }
63
-
64
- export default Cover
65
42
  ```
66
43
 
67
- A few things to notice:
44
+ A few things worth noticing:
68
45
 
69
- - **Standard Uniweb section shape.** `{ content, block }` destructured. `content.paragraphs` is a guaranteed array — empty if there are none, never null. No outer `<section>` wrapper, because the Uniweb runtime wraps every section in `<section>` with the right context class and background.
70
- - **Same JSX, two consumers.** `body` is used twice: registered for docx compilation and rendered into the visible layout. There is no duplication to drift between them.
71
- - **Any section is opt-in.** Sections that don't call `useDocumentOutput` are never included in the compiled document. The hook is a no-op when called outside a provider, so you can freely mix opt-in and non-opt-in sections in the same page.
46
+ - The `body` variable is used twice — registered for docx compilation and rendered into the visible layout. No duplication to drift between them.
47
+ - `useDocumentOutput` is a no-op outside a `DocumentProvider`, so sections that don't opt into docx simply don't contribute. Mixing opt-in and non-opt-in sections on the same page is fine.
48
+ - The `/docx` builders (`H1`, `H2`, `Paragraphs`, etc.) emit ordinary HTML with `data-*` attributes. The same tree serves as the browser preview and as the source the compile pipeline walks.
72
49
 
73
- Somewhere higher in the tree (a layout, a header, a dedicated "download" section), add a `<DocumentProvider>` and a button that calls `compile`:
50
+ Somewhere higher in the tree — a layout, a header, a dedicated download section — wrap everything in a `DocumentProvider` and wire up a button:
74
51
 
75
52
  ```jsx
76
53
  import {
@@ -83,17 +60,19 @@ function ReportLayout({ children }) {
83
60
  return (
84
61
  <DocumentProvider>
85
62
  {children}
86
- <DownloadControls />
63
+ <DownloadButton />
87
64
  </DocumentProvider>
88
65
  )
89
66
  }
90
67
 
91
- function DownloadControls() {
68
+ function DownloadButton() {
92
69
  const { compile, isCompiling } = useDocumentCompile()
70
+
93
71
  const handleDownload = async () => {
94
72
  const blob = await compile('docx', { title: 'Annual Report' })
95
73
  triggerDownload(blob, 'annual-report.docx')
96
74
  }
75
+
97
76
  return (
98
77
  <button onClick={handleDownload} disabled={isCompiling}>
99
78
  {isCompiling ? 'Generating…' : 'Download'}
@@ -102,75 +81,97 @@ function DownloadControls() {
102
81
  }
103
82
  ```
104
83
 
105
- Every section between the provider and the button that called `useDocumentOutput` contributes its registered fragment to the compiled document. The rest is ignored. The docx library itself is loaded dynamically on the first `compile('docx')` call, not at page load.
84
+ Every section between the provider and the button that called `useDocumentOutput('docx', ...)` contributes to the compiled document. The docx library itself is loaded dynamically on the first `compile('docx')` call — it's never in the main bundle.
106
85
 
107
- ## Documentation
86
+ For a fuller walkthrough, see the [quick start](./docs/quick-start.md).
87
+
88
+ ## What's shipped
108
89
 
109
- Start here:
90
+ Press ships three format adapters today:
110
91
 
111
- - **[Quick start](./docs/quick-start.md)** — Ten minutes from `npm install` to a working Download button, with a live preview.
112
- - **[Concepts](./docs/concepts.md)** — Why Press exists, the registration pattern, the four ways to combine preview and compiled output, why compile is separate from download, why the docx toolkit is just a toolkit.
92
+ - **docx** — Word documents via the [`docx`](https://docx.js.org) library. Paragraphs, headings, text runs, tables, lists, images, hyperlinks, headers/footers, page numbering. The flagship format.
93
+ - **typst** — Typeset PDFs via the [Typst](https://typst.app) compiler. Same-source preview from `/typst` builders; whole-book compile via `compileSubtree`; `sources` mode (pure frontend, user compiles locally) and `server` mode (via a companion endpoint — see [deployment.md](./docs/architecture/deployment.md)).
94
+ - **xlsx** — Spreadsheets via `exceljs`. Plain `{ title, headers, data }` objects, multi-sheet, preview and compiled output independent (because spreadsheets aren't typography).
113
95
 
114
- API reference:
96
+ For what's next (Paged.js for browser-paginated PDFs, EPUB, slides, and a handful of speculative formats) see the [format roadmap](./docs/architecture/format-roadmap.md). For writing a custom adapter against an unsupported format, see the [custom adapter guide](./docs/guides/custom-adapter.md).
115
97
 
116
- - **[Core](./docs/api/core.md)** — `DocumentProvider`, `useDocumentOutput`, `useDocumentCompile`, `triggerDownload`.
117
- - **[/docx](./docs/api/docx.md)** — Every builder component with runnable examples.
118
- - **[/sections](./docs/api/sections.md)** — `Section` and `StandardSection` — higher-level templates intended for non-Uniweb contexts where you need an explicit section wrapper.
119
- - **[/ir](./docs/api/ir.md)** — IR layer for custom format adapters.
98
+ ## Three ways to use Press
120
99
 
121
- Guides:
100
+ Press doesn't prescribe what the site is *for*. Three shapes are all first-class:
122
101
 
123
- - **[Publishing a book](./docs/guides/book-publishing.md)** — Whole-site aggregation, three compile modes (sources / server / wasm roadmap), the Vite dev plugin, and the two web modalities (same-JSX preview vs its-own-medium web UX).
124
- - **[The preview pattern](./docs/guides/preview-pattern.md)** — Render a compiled `.docx` back into a sandboxed iframe via `docx-preview` as a cross-check view, separate from the per-component React preview.
125
- - **[Multi-block reports](./docs/guides/multi-block-reports.md)** — How `DocumentProvider` aggregates output across many section components into one document.
126
- - **[Writing a custom adapter](./docs/guides/custom-adapter.md)** — Build a non-docx format adapter using `@uniweb/press/ir`.
127
- - **[Citations](./docs/guides/citations.md)** — The `citestyle` + Press pattern for bibliographies in preview and docx.
128
- - **[Style pack](./docs/guides/style-pack.md)** — Copy-paste `paragraphStyles` and `numbering` definitions for hanging-indent and numbered bibliography entries.
102
+ **Interactive report with downloads.** A live web document — navigable, themed, possibly with dynamic data — that also offers one or more Download buttons. Readers browse on screen; those who need a file click Download. The flagship case: annual reports, CVs, research outputs.
129
103
 
130
- Migration:
104
+ **Multi-format exports.** The same site registers fragments for several formats. A report section might contribute to docx (for the narrative) and xlsx (for the underlying data); a cover might contribute only to docx. Compilation is per-format and lazy — only the adapter you click loads.
131
105
 
132
- - **[Migration from phase 1](./docs/migration-from-phase-1.md)** — For readers holding phase-1 examples with `@uniweb/press/react` and `DownloadButton`.
106
+ **Headless export.** Sections register fragments and return `null`. The site has no visible output and exists purely as a compile target for an automated pipeline, admin tool, or generator UI. Press works here because registration is decoupled from rendering.
133
107
 
134
- There is also a runnable demo at [`examples/preview-iframe/`](./examples/preview-iframe/) — a minimal standalone Vite app exercising the compile + preview + download flow end to end. It's not a Uniweb foundation; it's a plain React app demonstrating that Press works outside Uniweb as well.
108
+ For how these shapes interact with preview strategies (same-source JSX vs compiled-blob), and the fuller mental model, see [concepts](./docs/concepts.md).
109
+
110
+ ## Books and whole-site compilation
111
+
112
+ The patterns above cover one-page documents. For books — where the download's scope is *every chapter*, not just the current page — Press provides `compileSubtree`:
113
+
114
+ ```js
115
+ await compileSubtree(<ChildBlocks blocks={website.allBlocks} />, 'typst')
116
+ ```
117
+
118
+ This renders the whole content graph off-screen through its own provider, gathers every registration, and compiles. No page routing, no DOM mounts. See the [book publishing guide](./docs/guides/book-publishing.md) for the end-to-end flow.
135
119
 
136
120
  ## Subpath exports
137
121
 
138
122
  | Entry point | What's in it |
139
123
  |---|---|
140
- | `@uniweb/press` | `DocumentProvider`, `useDocumentOutput`, `useDocumentCompile`, `triggerDownload`, `createStore`, `compileRegistrations`, `compileSubtree` — the format-agnostic core |
141
- | `@uniweb/press/docx` | React builder components for docx output (`Paragraph`, `TextRun`, `H1`–`H4`, `Image`, `Link`, `List`, …) |
142
- | `@uniweb/press/typst` | React builder components for Typst output (`ChapterOpener`, `Heading`, `Paragraph`, `CodeBlock`, `List`, `Table`, `BlockQuote`, `Image`, `Asterism`, `Raw`, `Sequence`, …) |
143
- | `@uniweb/press/sections` | `Section` and `StandardSection` — register-and-render helpers that include their own `<section>` wrapper (for non-Uniweb React contexts) |
144
- | `@uniweb/press/ir` | IR utilities (`htmlToIR`, `attributeMap`, `compileOutputs`) for custom-adapter authors |
145
- | `@uniweb/press/vite-plugin-typst` | Dev-server Vite plugin that answers the Typst `server` compile mode by running `typst compile` locally and streaming back a PDF |
124
+ | `@uniweb/press` | Core: `DocumentProvider`, `useDocumentOutput`, `useDocumentCompile`, `triggerDownload`, `createStore`, `compileRegistrations`, `compileSubtree` |
125
+ | `@uniweb/press/docx` | React builders for docx (`Paragraph`, `TextRun`, `H1`–`H4`, `Image`, `Link`, `List`, …) |
126
+ | `@uniweb/press/typst` | React builders for Typst (`ChapterOpener`, `Heading`, `Paragraph`, `CodeBlock`, `Table`, `BlockQuote`, `Image`, `Asterism`, `Raw`, `Sequence`, …) |
127
+ | `@uniweb/press/sections` | `Section` and `StandardSection` — register-and-render helpers with their own `<section>` wrapper (for non-Uniweb React contexts) |
128
+ | `@uniweb/press/ir` | IR utilities (`htmlToIR`, `attributeMap`, `compileOutputs`) for custom adapter authors |
129
+ | `@uniweb/press/vite-plugin-typst` | Dev-server Vite plugin answering Typst `server` compile mode locally |
130
+
131
+ Format adapters themselves (`compileDocx`, `compileTypst`, `compileXlsx`, and the libraries they depend on) are deliberately **not** in the `exports` field. They're reached only through the dynamic `import()` inside `useDocumentCompile`, which keeps multi-megabyte libraries out of the main bundle.
132
+
133
+ ## Documentation
134
+
135
+ **Start here:**
136
+
137
+ - [Concepts](./docs/concepts.md) — the mental model: why Press exists, the registration pattern, preview strategies, how to think about fragments. Read this if you're deciding how Press fits into a foundation.
138
+ - [Quick start](./docs/quick-start.md) — ten minutes from `npm install` to a working Download button, with a live preview.
139
+
140
+ **Guides:**
146
141
 
147
- The ~3.4 MB `docx` library is never pulled into the main bundle. It's loaded dynamically the first time `compile('docx')` runs.
142
+ - [Publishing a book](./docs/guides/book-publishing.md) — whole-site aggregation via `compileSubtree`, the Typst pipeline, the two web modalities.
143
+ - [The preview pattern](./docs/guides/preview-pattern.md) — render a compiled `.docx` back into a sandboxed iframe via `docx-preview` for high-fidelity cross-check views.
144
+ - [Multi-block reports](./docs/guides/multi-block-reports.md) — how `DocumentProvider` aggregates output across many section components into one document.
145
+ - [Writing a custom adapter](./docs/guides/custom-adapter.md) — build a non-shipped format adapter using `@uniweb/press/ir`.
146
+ - [Citations](./docs/guides/citations.md) — bibliographies in preview and docx via the `citestyle` + Press pattern.
147
+ - [Style pack](./docs/guides/style-pack.md) — copy-paste `paragraphStyles` and `numbering` definitions for common bibliography patterns.
148
148
 
149
- ## Shipped toolkits and roadmap
149
+ **Architecture and design:**
150
150
 
151
- Press separates the **framework** (registration, compile, download) from the **toolkits** (per-format React builders and adapters). The framework is stable and usable today for any format. The toolkits are the shipped conveniences:
151
+ - [Principles](./docs/architecture/principles.md) — the architectural commitments Press is built on. Read before proposing any change that touches the public surface or introduces a new format.
152
+ - [Overview](./docs/architecture/overview.md) — how Press is put together under the hood, for contributors orienting to the codebase.
153
+ - [Adding a format](./docs/architecture/adding-a-format.md) — worked examples (LaTeX, Paged.js) and the general checklist.
154
+ - [Deployment](./docs/architecture/deployment.md) — wire protocol, reference implementations, and operational concerns for formats that need a backend.
155
+ - [Format roadmap](./docs/architecture/format-roadmap.md) — what's shipped, what's next, what's speculative.
152
156
 
153
- | Toolkit | Status | Notes |
154
- |---|---|---|
155
- | **docx** | ✅ Shipped | Paragraphs, headings, text runs, tables, borders/margins/widths, bullet/numbered lists, hyperlinks, images with async fetch, page numbering, default headers/footers, `firstPageOnly` semantics. Uses the [`docx`](https://docx.js.org) library. |
156
- | **typst** | ✅ Shipped | Whole-book compile via `compileSubtree`, two compile modes (`sources` ZIP + `server` PDF via included Vite dev plugin), 12 builders + `Sequence` walker. Produces real PDFs through the [Typst](https://typst.app) compiler. See [Publishing a book](./docs/guides/book-publishing.md). |
157
- | **xlsx** | 🔜 Roadmap | Plain `{ title, headers, data }` objects, multi-sheet. Different shape from docx because spreadsheets aren't typography. |
158
- | **pdf** | 🔜 Roadmap | Either docx JSX via Paged.js or `@react-pdf/renderer` for fine control. Decision deferred. |
157
+ **Runnable example:**
159
158
 
160
- Writing a custom adapter (Markdown, RTF, a domain-specific XML, a JSON export) is supported today via `@uniweb/press/ir`. See the [custom adapter guide](./docs/guides/custom-adapter.md).
159
+ - [`examples/preview-iframe/`](./examples/preview-iframe/) — a minimal standalone Vite app demonstrating compile + preview + download end to end. Not a Uniweb foundation; a plain React app demonstrating that Press works outside Uniweb as well.
161
160
 
162
161
  ## See also
163
162
 
164
- - [`@uniweb/loom`](https://github.com/uniweb/loom) — A small expression language for weaving data into text. The typical Uniweb report foundation uses a content handler that runs Loom's `instantiateContent` over the parsed content tree, resolving placeholders against live data before Press ever sees the section — which means dynamic reports look identical to static ones from Press's point of view.
165
- - [`@uniweb/kit`](https://github.com/uniweb/kit) — React components and hooks for building Uniweb foundations. The preview side of a Press-aware component typically uses Kit for theme-aware typography (`H1`, `H2`, `P`, `Prose`, `ChildBlocks`, `Visual`, etc.) alongside Press's builder components for the compiled output. See the [concepts doc](./docs/concepts.md) for the "same JSX vs separate preview" discussion.
163
+ - [`@uniweb/loom`](https://github.com/uniweb/loom) — expression language for weaving data into text. A typical Uniweb report foundation runs Loom's `instantiateContent` over the parsed content tree *before* Press sees it, which means dynamic reports look identical to static ones from Press's point of view.
164
+ - [`@uniweb/kit`](https://github.com/uniweb/kit) — React components and hooks for Uniweb foundations. Preview-side typography often uses Kit components alongside Press builders for compiled output.
166
165
 
167
166
  ## Status
168
167
 
169
- **Pre-1.0, unpublished.** The registration architecture, builder components, section templates, IR layer, and docx adapter are stable and covered by 133 tests. The public surface is expected to hold through 1.0. xlsx and pdf toolkits are on the roadmap.
168
+ **Pre-1.0, published on npm.** The registration architecture, builder components, section templates, IR layer, and the docx, typst, and xlsx adapters are stable; the public surface is expected to hold through 1.0.
169
+
170
+ Releases go through the workspace publish pipeline. Breaking changes are acceptable when justified, but each release is a published artifact — the `exports` field and documented subpaths stay coherent across versions.
170
171
 
171
172
  ## Development
172
173
 
173
- Press ships raw source — there is no build step. Edits in `src/` are immediately effective in any linked workspace package.
174
+ Press ships raw source with no build step. Edits in `src/` are immediately effective in any linked workspace package.
174
175
 
175
176
  ```bash
176
177
  pnpm test # vitest run — full suite
@@ -179,9 +180,7 @@ pnpm test tests/docx/ # one directory
179
180
  pnpm test -t 'inline marks' # by test name
180
181
  ```
181
182
 
182
- Test layout mirrors `src/`: `tests/core/`, `tests/docx/`, `tests/sections/`, `tests/ir/`, plus `tests/integration/` for end-to-end cases.
183
-
184
- To run the preview-iframe demo:
183
+ To run the `preview-iframe` demo:
185
184
 
186
185
  ```bash
187
186
  cd examples/preview-iframe
@@ -189,10 +188,8 @@ pnpm install
189
188
  pnpm dev
190
189
  ```
191
190
 
192
- It registers three sections in a `DocumentProvider` and exposes Preview (compiles and renders via `docx-preview` into a sandboxed iframe) and Download (compiles and calls `triggerDownload`) buttons.
193
-
194
- **Adding a builder component, format adapter, or section helper:** see `CLAUDE.md` for the runbooks.
191
+ For contributor conventions — adding a builder, format adapter, or section helper; the docx image-emission invariants; testing patterns — see [`CLAUDE.md`](./CLAUDE.md).
195
192
 
196
193
  ## License
197
194
 
198
- Apache-2.0 — see [LICENSE](./LICENSE).
195
+ Apache-2.0 — see [LICENSE](./LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/press",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Press your React components into downloadable files — Word, Excel, PDF. Registration-based pattern with lazy-loaded format adapters.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,9 +50,7 @@
50
50
  "devDependencies": {
51
51
  "@testing-library/react": "^16.0.0",
52
52
  "jsdom": "^25.0.0",
53
- "vitest": "^2.0.0",
54
- "@uniweb/semantic-parser": "1.1.9",
55
- "@uniweb/content-reader": "1.1.4"
53
+ "vitest": "^2.0.0"
56
54
  },
57
55
  "scripts": {
58
56
  "test": "vitest run",
@@ -7,35 +7,80 @@
7
7
  * package.json's exports field. Callers must go through runCompile(...)
8
8
  * so the dynamic-import stays the only path — which is how the ~3.4 MB
9
9
  * docx library (and any future WASM runtime) stays out of the main bundle.
10
+ *
11
+ * ## Descriptor shape
12
+ *
13
+ * Each adapter is described by a triple:
14
+ *
15
+ * { load, consumes, ir }
16
+ *
17
+ * load — () => import('./<adapter>.js'). Dynamic-imported only.
18
+ * consumes — the store key foundations register fragments under. May
19
+ * differ from the output format name. Paged.js compiles
20
+ * 'pagedjs' output but reads fragments registered under
21
+ * 'html'; future EPUB will do the same. (See principle 6's
22
+ * "When the generalization is already earned" carve-out in
23
+ * docs/architecture/principles.md — this split was introduced
24
+ * with the first HTML-string adapter rather than deferred.)
25
+ * When omitted, defaults to the format name (self-aliased).
26
+ * ir — true for HTML-based adapters that walk each fragment into
27
+ * the IR tree (docx, typst). false for passthrough adapters
28
+ * that consume rendered HTML strings (pagedjs) or plain data
29
+ * objects (xlsx). Drives the compile-pipeline branch in
30
+ * src/ir/compile.js without per-format if-ladders.
31
+ *
32
+ * Output format names (keys of ADAPTERS) are the public contract — what
33
+ * callers pass to compileSubtree / useDocumentCompile.compile. Input shape
34
+ * names (values of `consumes`) are the contract between the registration
35
+ * store and the adapter; foundations register under these.
10
36
  */
11
37
 
12
- // Dynamic-import loaders keyed by format. Each loader resolves to a
13
- // module exporting compile<Format>(compiledInput, documentOptions) → Blob.
38
+ /** @type {Record<string, { load: () => Promise<any>, consumes?: string, ir?: boolean }>} */
14
39
  const ADAPTERS = {
15
- docx: () => import('./docx.js'),
16
- xlsx: () => import('./xlsx.js'),
17
- typst: () => import('./typst.js'),
18
- // Phase 3: pdf: () => import('./pdf.js'),
40
+ docx: { load: () => import('./docx.js'), consumes: 'docx', ir: true },
41
+ xlsx: { load: () => import('./xlsx.js'), consumes: 'xlsx', ir: false },
42
+ typst: { load: () => import('./typst.js'), consumes: 'typst', ir: true },
43
+ // Paged.js consumes 'html' — an input shape shared with EPUB below.
44
+ // Foundations register once under 'html' and both adapters read it.
45
+ pagedjs: { load: () => import('./pagedjs.js'), consumes: 'html', ir: false },
46
+ epub: { load: () => import('./epub.js'), consumes: 'html', ir: false },
47
+ }
48
+
49
+ /**
50
+ * Look up an adapter descriptor by output-format name. Returns null when
51
+ * the format is unknown — callers decide how to surface that (runCompile
52
+ * throws; compileOutputs warns and returns an empty-shape input).
53
+ */
54
+ export function getAdapterDescriptor(format) {
55
+ const desc = ADAPTERS[format]
56
+ if (!desc) return null
57
+ return {
58
+ load: desc.load,
59
+ consumes: desc.consumes || format,
60
+ ir: desc.ir !== false, // default to true for safety on under-specified entries
61
+ }
19
62
  }
20
63
 
21
64
  /**
22
65
  * Run the format adapter against already-compiled input.
23
66
  *
24
- * @param {string} format - 'docx' | 'xlsx' | 'typst' | …
67
+ * @param {string} format - 'docx' | 'xlsx' | 'typst' | 'pagedjs' | …
25
68
  * @param {Object} compiledInput - Output of compileOutputs(store, format).
26
69
  * @param {Object} [documentOptions] - Adapter-specific options.
27
70
  * @returns {Promise<Blob>}
28
71
  */
29
72
  export async function runCompile(format, compiledInput, documentOptions = {}) {
30
- const loader = ADAPTERS[format]
31
- if (!loader) {
73
+ const desc = getAdapterDescriptor(format)
74
+ if (!desc) {
32
75
  throw new Error(`Unsupported document format: "${format}"`)
33
76
  }
34
- const adapter = await loader()
77
+ const adapter = await desc.load()
35
78
  const compileFn =
36
79
  adapter.compileDocx ||
37
80
  adapter.compileXlsx ||
38
81
  adapter.compileTypst ||
82
+ adapter.compilePagedjs ||
83
+ adapter.compileEpub ||
39
84
  adapter.compilePdf
40
85
  if (!compileFn) {
41
86
  throw new Error(
@@ -44,6 +44,7 @@ import {
44
44
  Bookmark,
45
45
  FootnoteReferenceRun,
46
46
  } from 'docx'
47
+ import { fetchAsset } from '../assets/fetch.js'
47
48
 
48
49
  // ============================================================================
49
50
  // Public API
@@ -84,12 +85,20 @@ export async function compileDocx(input, options = {}) {
84
85
  * - numbering: Array<NumberingConfig> — numbering definitions that
85
86
  * paragraphs can reference via `data-numbering-reference="…"`.
86
87
  * Passed through to `new Document({ numbering: { config } })`.
88
+ * - pageMargin: Object — page margin overrides, merged into the
89
+ * section's `properties.page.margin`. Shape matches the docx
90
+ * library's `IPageMarginAttributes` — all keys are twips (1 inch =
91
+ * 1440): `top`, `right`, `bottom`, `left`, `header`, `footer`,
92
+ * `gutter`, `mirror`. The `header` and `footer` margins define the
93
+ * *carrier-paragraph* slot used by registered headers/footers; set
94
+ * both to a non-zero value or floating anchors inside those slots
95
+ * may be dropped. Omit to keep the docx library's defaults.
87
96
  * - Any other key (title, subject, creator, description, keywords, …)
88
97
  * is forwarded as-is to the Document constructor.
89
98
  *
90
- * Callers that omit paragraphStyles/numbering pay nothing — the
91
- * adapter still produces a valid document, it just has no named
92
- * style or numbering definitions for paragraphs to reference.
99
+ * Callers that omit paragraphStyles/numbering/pageMargin pay nothing —
100
+ * the adapter still produces a valid document, it just has no named
101
+ * style, numbering definition, or margin override.
93
102
  * @returns {Promise<Document>}
94
103
  */
95
104
  export async function buildDocument(input, options = {}) {
@@ -104,6 +113,7 @@ export async function buildDocument(input, options = {}) {
104
113
  const {
105
114
  paragraphStyles,
106
115
  numbering,
116
+ pageMargin,
107
117
  ...documentMetadata
108
118
  } = options
109
119
 
@@ -125,7 +135,10 @@ export async function buildDocument(input, options = {}) {
125
135
  const sectionOptions = {
126
136
  properties: {
127
137
  type: SectionType.CONTINUOUS,
128
- page: { pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL } },
138
+ page: {
139
+ pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL },
140
+ ...(pageMargin ? { margin: pageMargin } : {}),
141
+ },
129
142
  },
130
143
  children,
131
144
  }
@@ -162,6 +175,20 @@ export async function buildDocument(input, options = {}) {
162
175
  sectionOptions.footers = { default: createDefaultHeaderFooter(false) }
163
176
  }
164
177
 
178
+ // When either side opts in to `applyTo: 'first'`, the section gets
179
+ // `titlePage = true`, which tells Word to look up `first` variants on
180
+ // page 1. If the opposite side only registered a `default`, Word leaves
181
+ // page 1's header or footer blank — the default isn't consulted. Mirror
182
+ // `default` into `first` so all-pages content still renders on page 1.
183
+ if (sectionOptions.properties.titlePage) {
184
+ if (sectionOptions.headers && !sectionOptions.headers.first) {
185
+ sectionOptions.headers.first = sectionOptions.headers.default
186
+ }
187
+ if (sectionOptions.footers && !sectionOptions.footers.first) {
188
+ sectionOptions.footers.first = sectionOptions.footers.default
189
+ }
190
+ }
191
+
165
192
  const docOptions = {
166
193
  ...documentMetadata,
167
194
  sections: [sectionOptions],
@@ -812,15 +839,15 @@ async function irToImageParagraph(node) {
812
839
  }
813
840
 
814
841
  /**
815
- * Fetch image data from a URL and return as ArrayBuffer.
816
- * Works in browser (via fetch + blob) and Node (via fetch + arrayBuffer).
842
+ * Fetch image bytes for a docx ImageRun.
843
+ *
844
+ * Thin wrapper over the shared src/assets/fetch.js helper — docx wants
845
+ * a raw ArrayBuffer (Uint8Array.buffer works too; the library accepts
846
+ * either). The shared helper is what EPUB and any future asset-embedding
847
+ * adapter use; see principle 6 ("Extract shared logic when a second
848
+ * adapter needs it") in docs/architecture/principles.md.
817
849
  */
818
850
  async function fetchImageData(url) {
819
- const response = await fetch(url)
820
-
821
- if (!response.ok) {
822
- throw new Error(`HTTP error fetching image: ${response.status}`)
823
- }
824
-
825
- return response.arrayBuffer()
851
+ const { bytes } = await fetchAsset(url)
852
+ return bytes
826
853
  }