@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 +87 -90
- package/package.json +2 -4
- package/src/adapters/dispatch.js +55 -10
- package/src/adapters/docx.js +40 -13
- package/src/adapters/epub.js +624 -0
- package/src/adapters/pagedjs.js +295 -0
- package/src/adapters/typst.js +16 -1
- package/src/assets/fetch.js +155 -0
- package/src/compileRegistrations.js +289 -1
- package/src/index.js +1 -0
- package/src/ir/attributes.js +18 -0
- package/src/ir/compile.js +93 -25
- package/src/useDocumentCompile.js +31 -1
- package/src/vite-plugin-typst.js +7 -3
package/README.md
CHANGED
|
@@ -1,76 +1,53 @@
|
|
|
1
1
|
# @uniweb/press
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
##
|
|
9
|
+
## What Press is
|
|
10
10
|
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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=
|
|
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
|
|
44
|
+
A few things worth noticing:
|
|
68
45
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
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
|
|
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
|
-
<
|
|
63
|
+
<DownloadButton />
|
|
87
64
|
</DocumentProvider>
|
|
88
65
|
)
|
|
89
66
|
}
|
|
90
67
|
|
|
91
|
-
function
|
|
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
|
|
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
|
-
|
|
86
|
+
For a fuller walkthrough, see the [quick start](./docs/quick-start.md).
|
|
87
|
+
|
|
88
|
+
## What's shipped
|
|
108
89
|
|
|
109
|
-
|
|
90
|
+
Press ships three format adapters today:
|
|
110
91
|
|
|
111
|
-
- **[
|
|
112
|
-
- **[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
Press doesn't prescribe what the site is *for*. Three shapes are all first-class:
|
|
122
101
|
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
141
|
-
| `@uniweb/press/docx` | React
|
|
142
|
-
| `@uniweb/press/typst` | React
|
|
143
|
-
| `@uniweb/press/sections` | `Section` and `StandardSection` — register-and-render helpers
|
|
144
|
-
| `@uniweb/press/ir` | IR utilities (`htmlToIR`, `attributeMap`, `compileOutputs`) for custom
|
|
145
|
-
| `@uniweb/press/vite-plugin-typst` | Dev-server Vite plugin
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
**Architecture and design:**
|
|
150
150
|
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) —
|
|
165
|
-
- [`@uniweb/kit`](https://github.com/uniweb/kit) — React components and hooks for
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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",
|
package/src/adapters/dispatch.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
|
31
|
-
if (!
|
|
73
|
+
const desc = getAdapterDescriptor(format)
|
|
74
|
+
if (!desc) {
|
|
32
75
|
throw new Error(`Unsupported document format: "${format}"`)
|
|
33
76
|
}
|
|
34
|
-
const adapter = await
|
|
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(
|
package/src/adapters/docx.js
CHANGED
|
@@ -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 —
|
|
91
|
-
* adapter still produces a valid document, it just has no named
|
|
92
|
-
* style
|
|
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: {
|
|
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
|
|
816
|
-
*
|
|
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
|
|
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
|
}
|