boxpdf-html 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Erik Aronesty
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # boxpdf-html
2
+
3
+ Readable HTML-to-PDF rendering built on [`boxpdf`](https://github.com/earonesty/boxpdf). It is for invoices, receipts, reports, emails, and other authored document HTML where a useful static PDF matters more than browser pixel emulation.
4
+
5
+ ```sh
6
+ npm install boxpdf-html boxpdf pdf-lib
7
+ ```
8
+
9
+ ## CLI
10
+
11
+ Render an HTML file directly:
12
+
13
+ ```sh
14
+ npx boxpdf-html invoice.html invoice.pdf
15
+ ```
16
+
17
+ With generated Tailwind CSS:
18
+
19
+ ```sh
20
+ npx tailwindcss -i ./tailwind.css -o ./dist/tailwind.css --minify
21
+ npx boxpdf-html invoice.html invoice.pdf --css ./dist/tailwind.css
22
+ ```
23
+
24
+ With custom fonts and local images:
25
+
26
+ ```sh
27
+ npx boxpdf-html invoice.html invoice.pdf \
28
+ --font ./Inter-Regular.ttf \
29
+ --bold-font ./Inter-Bold.ttf \
30
+ --font-family 'Inter=normal:Inter-Regular.ttf,bold:Inter-Bold.ttf'
31
+ ```
32
+
33
+ Useful flags:
34
+
35
+ ```sh
36
+ boxpdf-html <input.html> <output.pdf>
37
+ boxpdf-html - <output.pdf> # read HTML from stdin
38
+ boxpdf-html input.html output.pdf --css app.css
39
+ boxpdf-html input.html output.pdf --base-url ./public
40
+ boxpdf-html input.html output.pdf --debug
41
+ boxpdf-html input.html output.pdf --unsupported-css
42
+ boxpdf-html input.html output.pdf --profile
43
+ ```
44
+
45
+ The CLI defaults to pdf-lib's built-in Helvetica family. Use real embedded fonts for production output when brand matching, unicode coverage, or exact metrics matter.
46
+
47
+ ## API
48
+
49
+ `htmlToBoxpdf` turns HTML into normal boxpdf nodes. You render those nodes with `renderFlow`.
50
+
51
+ ```ts
52
+ import { readFile } from "node:fs/promises";
53
+ import { PDFDocument } from "pdf-lib";
54
+ import { loadFont, loadImage, renderFlow } from "boxpdf";
55
+ import { fontFamily, htmlToBoxpdf } from "boxpdf-html";
56
+
57
+ const html = await readFile("invoice.html", "utf8");
58
+ const pdf = await PDFDocument.create();
59
+
60
+ const inter = await loadFont(pdf, await readFile("Inter-Regular.ttf"));
61
+ const interBold = await loadFont(pdf, await readFile("Inter-Bold.ttf"));
62
+ const logo = await loadImage(pdf, await readFile("logo.png"));
63
+
64
+ const result = htmlToBoxpdf(html, {
65
+ font: inter,
66
+ boldFont: interBold,
67
+ resolveFont: fontFamily({
68
+ Inter: {
69
+ normal: inter,
70
+ bold: interBold
71
+ },
72
+ "sans-serif": {
73
+ normal: inter,
74
+ bold: interBold
75
+ }
76
+ }),
77
+ resolveImage: ({ url }) => (url === "logo.png" ? logo : undefined),
78
+ baseUrl: process.cwd(),
79
+ width: 532
80
+ });
81
+
82
+ await renderFlow(pdf, result.nodes, { margin: 40 });
83
+ const bytes = await pdf.save();
84
+ ```
85
+
86
+ `width` is the CSS containing block width in PDF points. A US Letter page with 40pt margins has a 532pt content width, so `width: 532` is a good default.
87
+
88
+ ## Fonts
89
+
90
+ Fonts are explicit. `boxpdf-html` does not discover system fonts and does not ship a browser font stack. This keeps rendering deterministic and works in serverless runtimes.
91
+
92
+ At minimum, pass `font`. Pass `boldFont` and `italicFont` if your HTML uses bold or italic text:
93
+
94
+ ```ts
95
+ const result = htmlToBoxpdf(html, {
96
+ font,
97
+ boldFont,
98
+ italicFont,
99
+ width: 532
100
+ });
101
+ ```
102
+
103
+ For CSS `font-family`, use `fontFamily()`:
104
+
105
+ ```ts
106
+ const resolveFont = fontFamily({
107
+ Inter: {
108
+ normal: interRegular,
109
+ bold: interBold,
110
+ italic: interItalic,
111
+ boldItalic: interBoldItalic
112
+ },
113
+ Helvetica: {
114
+ normal: fallback,
115
+ bold: fallbackBold
116
+ },
117
+ "sans-serif": {
118
+ normal: fallback,
119
+ bold: fallbackBold
120
+ }
121
+ });
122
+ ```
123
+
124
+ The resolver receives `{ families, weight, style }` and returns a pdf-lib `PDFFont`. You can provide your own resolver when you need looser mapping, font aliases, language-specific fallbacks, or weight synthesis.
125
+
126
+ Gotchas:
127
+
128
+ - `font-family: system-ui` only works if your resolver maps `system-ui`.
129
+ - Standard pdf-lib fonts are convenient but limited; use embedded TTF/OTF fonts for real documents.
130
+ - Complex shaping depends on pdf-lib/fontkit behavior. Western-language invoice/report text is the target.
131
+ - Font metrics affect layout. Use the same embedded fonts in tests and production when visual stability matters.
132
+
133
+ ## Tailwind CSS
134
+
135
+ Tailwind works when you render its generated CSS, not raw class names alone. The usual flow is:
136
+
137
+ 1. Write document HTML with Tailwind classes.
138
+ 2. Run Tailwind against that HTML.
139
+ 3. Inline or pass the generated CSS to `boxpdf-html`.
140
+ 4. Render with a containing width that matches your intended PDF content area.
141
+
142
+ Example source:
143
+
144
+ ```html
145
+ <div class="p-6 bg-[#f8fafc] text-gray-900">
146
+ <div class="max-w-[520px] rounded-[10px] border bg-white p-5 shadow-sm">
147
+ <div class="grid grid-cols-[1fr_2fr] gap-x-4 gap-y-3">
148
+ <div class="rounded-md border border-blue-200 bg-blue-50 p-3">
149
+ <p class="text-xs font-semibold uppercase tracking-wide text-blue-700">Status</p>
150
+ <p class="mt-1 text-sm font-bold">Paid</p>
151
+ </div>
152
+ <div class="rounded-md border border-gray-200 p-3">
153
+ <p class="text-xs font-semibold uppercase tracking-wide text-gray-600">Notes</p>
154
+ <p class="mt-1 text-sm leading-5">Two fraction column wraps later.</p>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ ```
160
+
161
+ Build CSS:
162
+
163
+ ```css
164
+ @import "tailwindcss";
165
+ @source "./invoice.html";
166
+ ```
167
+
168
+ ```sh
169
+ npx tailwindcss -i ./tailwind-input.css -o ./tailwind-output.css --minify
170
+ npx boxpdf-html invoice.html invoice.pdf --css ./tailwind-output.css
171
+ ```
172
+
173
+ Supported Tailwind patterns include common spacing, color, text, border, radius, width/height, flex, grid, table, image, and arbitrary-value utilities. Unsupported utility declarations can be reported with `--unsupported-css` or `diagnostics: { unsupportedCss: true }`.
174
+
175
+ Tailwind gotchas:
176
+
177
+ - Responsive/state variants are parsed as CSS; there is no viewport interaction. Choose a single generated CSS target for the PDF you want.
178
+ - `shadow-*`, transforms, filters, transitions, and browser-only effects are either ignored or reported as unsupported. The PDF should remain readable.
179
+ - Tailwind preflight resets are mostly harmless. Diagnostics intentionally focus on utility selectors instead of noisy base selectors.
180
+ - If text layout matters, use the same fonts in Tailwind design review and PDF rendering.
181
+
182
+ ## Images
183
+
184
+ The API uses `resolveImage` because pdf-lib images must be embedded before rendering:
185
+
186
+ ```ts
187
+ const images = new Map([
188
+ ["logo.png", await loadImage(pdf, await readFile("logo.png"))]
189
+ ]);
190
+
191
+ htmlToBoxpdf(html, {
192
+ font,
193
+ resolveImage: ({ url }) => images.get(url),
194
+ baseUrl: process.cwd()
195
+ });
196
+ ```
197
+
198
+ The CLI preloads local, `http(s)`, and `data:` image URLs referenced by `<img src>` and CSS `url(...)`. Missing images preserve their layout box when width/height can be inferred.
199
+
200
+ ## CSS And HTML Surface
201
+
202
+ Supported:
203
+
204
+ - HTML fragments and full documents via `parse5`.
205
+ - Stylesheets and inline styles via `css-tree`.
206
+ - Selectors: tag, class, id, attributes, descendants, child/sibling combinators, common structural pseudos, and escaped Tailwind selectors.
207
+ - Cascade basics: stylesheet rules, inline style, `!important`, inheritance, custom properties, `var()`, and common `calc()`.
208
+ - Layout: block, inline, inline-block, inline-flex, inline-grid, flex, grid fallback, tables, floats, absolute/relative positioning, z-index, overflow hidden, and replaced images.
209
+ - Text: rich inline runs, hard breaks, normal/no-wrap/pre-like whitespace, transforms, decoration, alignment, vertical-align, list hanging indents, and wrapping.
210
+ - Sizing/styling: CSS px to points, pt, em/rem, vw/vh, percentages in common places, min/max widths, box-sizing, margin/padding/gap, backgrounds, background images, borders, per-side borders, border collapse, radius, object-fit.
211
+
212
+ Not a browser:
213
+
214
+ - No JavaScript execution.
215
+ - No interactive or dynamic layout.
216
+ - No full browser paint model.
217
+ - No system font discovery.
218
+ - CSS support is intentionally expanded around static document output. Use diagnostics to find unsupported declarations in real templates.
219
+
220
+ ## Diagnostics
221
+
222
+ ```ts
223
+ const result = htmlToBoxpdf(html, {
224
+ font,
225
+ width: 532,
226
+ diagnostics: { unsupportedCss: true, sampleLimit: 3 },
227
+ profile: (event) => console.log(event.phase, event.elapsedMs)
228
+ });
229
+
230
+ console.log(result.diagnostics?.unsupportedCss);
231
+ ```
232
+
233
+ Unsupported CSS diagnostics are aggregated by property/value pair and include selector samples. Profile events cover parsing, CSS, style computation, render-tree construction, and output node counts.
234
+
235
+ ## Release
236
+
237
+ First publish is manual, because npm needs the package to exist before trusted publishing can be attached:
238
+
239
+ ```sh
240
+ pnpm install --frozen-lockfile
241
+ pnpm run typecheck
242
+ pnpm run test
243
+ pnpm run build
244
+ pnpm run pack:release
245
+ cd .pack
246
+ npm publish --access public
247
+ ```
248
+
249
+ Then configure npm trusted publishing for future releases:
250
+
251
+ ```sh
252
+ npm trust github boxpdf-html --repo earonesty/boxpdf-html --file release.yml
253
+ ```
254
+
255
+ If your npm CLI does not support that command, configure it in npmjs.com package settings:
256
+
257
+ - Publisher: GitHub Actions
258
+ - Owner: `earonesty`
259
+ - Repository: `boxpdf-html`
260
+ - Workflow filename: `release.yml`
261
+ - Environment: blank
262
+
263
+ Trusted publishing currently requires npm `11.5.1+` and Node `22.14+`; the `npm trust` CLI command itself requires npm `11.10+`. The release workflow uses Node 24 and upgrades npm before publishing. Future releases are tag-driven:
264
+
265
+ ```sh
266
+ git tag v1.0.0
267
+ git push origin v1.0.0
268
+ ```
269
+
270
+ The workflow runs typecheck, tests, build, publishes with OIDC/provenance, and creates a GitHub Release with generated notes.
271
+
272
+ ## Development
273
+
274
+ During local development, `package.json` depends on the adjacent checkout:
275
+
276
+ ```json
277
+ "boxpdf": "file:.."
278
+ ```
279
+
280
+ Release packing is done through `scripts/prepare-publish.mjs`, which stages the package and rewrites the published manifest to a real semver dependency:
281
+
282
+ ```json
283
+ "boxpdf": "^1.7.0"
284
+ ```
285
+
286
+ The script fails if a packed or published manifest would contain a local `file:` dependency.
287
+
288
+ Useful commands:
289
+
290
+ ```sh
291
+ pnpm run typecheck
292
+ pnpm run test
293
+ pnpm run build
294
+ pnpm run tailwind:fixture
295
+ pnpm run visual:check
296
+ pnpm run pack:release
297
+ BOXPDF_DEP_VERSION=^1.7.0 pnpm run publish:release
298
+ ```