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 +21 -0
- package/README.md +298 -0
- package/dist/chunk-23KN7BKZ.js +2822 -0
- package/dist/chunk-23KN7BKZ.js.map +1 -0
- package/dist/cli.cjs +3052 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +249 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2838 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +88 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
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
|
+
```
|