pretext-pdf 2.0.14 → 2.1.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +2280 -2232
  2. package/README.md +56 -2
  3. package/UPSTREAM.md +2 -2
  4. package/dist/cli.js +19 -19
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +10 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugin-types.d.ts +2 -4
  10. package/dist/plugin-types.d.ts.map +1 -1
  11. package/dist/render.d.ts +4 -1
  12. package/dist/render.d.ts.map +1 -1
  13. package/dist/render.js +4 -1
  14. package/dist/render.js.map +1 -1
  15. package/dist/validate/document.d.ts.map +1 -1
  16. package/dist/validate/document.js +7 -2
  17. package/dist/validate/document.js.map +1 -1
  18. package/dist/validate/elements/list.d.ts.map +1 -1
  19. package/dist/validate/elements/list.js +14 -2
  20. package/dist/validate/elements/list.js.map +1 -1
  21. package/dist/validate/elements/table.d.ts.map +1 -1
  22. package/dist/validate/elements/table.js +8 -3
  23. package/dist/validate/elements/table.js.map +1 -1
  24. package/dist/validate/elements/text.d.ts +1 -1
  25. package/dist/validate/elements/text.d.ts.map +1 -1
  26. package/dist/validate/elements/text.js +8 -1
  27. package/dist/validate/elements/text.js.map +1 -1
  28. package/dist/validate/helpers.d.ts +6 -0
  29. package/dist/validate/helpers.d.ts.map +1 -1
  30. package/dist/validate/helpers.js +80 -0
  31. package/dist/validate/helpers.js.map +1 -1
  32. package/dist/version-check.d.ts +4 -4
  33. package/dist/version-check.js +4 -4
  34. package/package.json +239 -239
  35. package/dist/post-process.d.ts +0 -16
  36. package/dist/post-process.d.ts.map +0 -1
  37. package/dist/post-process.js +0 -117
  38. package/dist/post-process.js.map +0 -1
  39. package/dist/types-public.d.ts +0 -9
  40. package/dist/types-public.d.ts.map +0 -1
  41. package/dist/types-public.js +0 -2
  42. package/dist/types-public.js.map +0 -1
  43. package/dist/validate/elements/structural.d.ts +0 -12
  44. package/dist/validate/elements/structural.d.ts.map +0 -1
  45. package/dist/validate/elements/structural.js +0 -12
  46. package/dist/validate/elements/structural.js.map +0 -1
  47. package/dist/validate.d.ts +0 -10
  48. package/dist/validate.d.ts.map +0 -1
  49. package/dist/validate.js +0 -10
  50. package/dist/validate.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,2232 +1,2280 @@
1
- # Changelog
2
-
3
- <!-- markdownlint-disable MD024 -->
4
-
5
- All notable changes to pretext-pdf are documented here.
6
- Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
7
-
8
- ---
9
-
10
- ## [2.0.14] — 2026-05-30
11
-
12
- Vendor snapshot updated to pretext v0.0.7-patched.1 (upstream v0.0.7 + 11 unique patches).
13
-
14
- ### Changed
15
-
16
- - **Vendored pretext layout engine upgraded from v0.0.6-patched.2 to v0.0.7-patched.1.**
17
- Incorporates upstream v0.0.7 improvements: keep-all mixed-script grouping, punctuation chain wrapping, opening punctuation line breaks, soft-hyphen break preservation, terminal letter spacing, numeric affix stickiness, rich inline boundary overflow prevention, browser-like dash/symbol runs, and line streaming performance optimizations.
18
- Plus fork-specific patches: German low-opening-quote fix, bidi surrogate handling, trailing collapsible space reconstruction, stream-friendly chunk layout optimization, and no-op merge pass skipping.
19
-
20
- ---
21
-
22
- ## [2.0.13] — 2026-05-29
23
-
24
- Security hardening and coverage improvements from final review pass.
25
-
26
- ### Security
27
-
28
- - **Symlink escape closed in `assertPathAllowed`** — `path-allowlist.ts` now calls `realpathSync` before the prefix comparison, so a symlink placed inside an allowed directory cannot dereference to an arbitrary path outside the allowlist (F-1).
29
- - **Chart `spec` scanned for prototype-pollution keys** — `validateChart` in `media.ts` now rejects any `spec` object containing `__proto__`, `constructor`, or `prototype` keys at any nesting depth before passing it to the Vega renderer (F-9).
30
- - **`http://` URLs rejected at loader entry points** — `images.ts` and `resolve-content.ts` now throw immediately on `http://` src values instead of entering the fetch path and relying on the inner SSRF guard (F-4).
31
-
32
- ### Fixed
33
-
34
- - **`stageInit` metadata fallback logs message only** — `pipeline.ts` no longer serializes the full error object into `console.warn`; only `err.message` is included, preventing internal API names and stack frames from appearing in library stderr (F-3).
35
-
36
- ### Tests Added
37
-
38
- - `test/compat-normalize.test.ts` — 62 tests covering all exported functions in `src/compat/normalize.ts` directly via tsx (previously only reachable through the dist/ barrel).
39
-
40
- ---
41
-
42
- ## [2.0.12] — 2026-05-29
43
-
44
- Multi-perspective review fixes: security, correctness, silent failures, dead code, and test coverage.
45
-
46
- ### Security
47
-
48
- - **`allowedFileDirs` array now copied in `fromPdfmake()`** — Previously assigned by reference, allowing the caller to mutate the allowlist after translation and silently expand path-traversal permissions mid-render. Now copied with `[...arr]` (SEC-1).
49
-
50
- ### Fixed
51
-
52
- - **`content` field is now `readonly ContentElement[]` on `PdfDocument`** — The last mutable public field. Prevents callers from `.push()`-ing into a document after passing it to `render()`, which could corrupt multi-stage async pipeline results. `builder.toDocument()` now snapshots the content array at call time (H-1).
53
- - **`buildFootnoteNumbering` and `runFootnoteTwoPass` parameter widened to `readonly ContentElement[]`** — Required by the `content` readonly change; no semantic change.
54
- - **Footnote sentinel `?? 0` replaced with `PretextPdfError`** — A missing footnote number now throws `PAGINATION_FAILED` with a clear message instead of silently rendering footnote `0` (M-3).
55
- - **`onUnsupported` default now logs a one-time warning** — The previous default was a silent no-op, contradicting its own JSDoc. Default is now `console.warn('[pretext-pdf/compat] Unsupported pdfmake feature skipped: <feature>')` with per-call deduplication (M-1).
56
- - **`stageInit` metadata catch block now logs the error** — The fallback path for `getInfoDict()` unavailability no longer silently swallows the root cause (M-2).
57
- - **`FONT_EMBED_FAILED` error message is user-facing** — Removed "this is a bug" from the message; replaced with actionable guidance (M-5).
58
- - **Internal exports removed from `translate.ts`**`translateNodeInner`, `applyStyleToParagraph`, `collectSpans` are no longer exported from the sub-module (M-4).
59
- - **`_require` unexported from `bundled-paths.ts`** — Was exported with a misleading `_` prefix but only used internally (L-1).
60
- - **`fonts.ts` header comment includes `collect-text.ts`** Fourth sub-module was omitted (L-2).
61
- - **`translate.ts` uses named `ListItem` import** — Replaced inline `import('../types.js').ListItem` expression (L-3).
62
-
63
- ### Tests Added
64
-
65
- - `test/compat/normalize.test.ts` 28 direct unit tests for `mergeStyles`, `normalizePageSize`, `normalizeMargins`, `normalizeHeaderFooter`, `normalizeStyleNames`
66
- - `test/fonts/collect-text.test.ts` 9 direct unit tests for `collectTextByFont` across paragraph, heading, rich-paragraph, header/footer, and defaultFont override
67
- - `test/compat.test.ts` — 3 isolation tests for `allowedFileDirs` array copy (SEC-1)
68
- - `test/builder.test.ts` — 2 snapshot isolation tests for `toDocument()` content copy (H-1)
69
-
70
- ### Changed
71
-
72
- - **`pdfmake-types.ts`: `allowedFileDirs` is now `readonly string[]`** Type made symmetric with `PdfDocument.allowedFileDirs`
73
-
74
- ## [2.0.11] — 2026-05-29
75
-
76
- Phase 6a: `readonly` on all scalar public interface fields (non-breaking).
77
-
78
- ### Changed
79
-
80
- - **All scalar fields on public interfaces marked `readonly`** — `PdfDocument`, `DocumentMetadata`,
81
- `Margins`, `FontSpec`, `HeaderFooterSpec`, `WatermarkBase`/`WatermarkSpec`, `EncryptionSpec`,
82
- `SignatureSpec`, `BookmarkConfig`, `HyphenationConfig`, `AnnotationSpec`.
83
-
84
- Array fields (`content`, `fonts`, `keywords`, `sections`, `allowedFileDirs`) are intentionally
85
- left mutable — marking those `readonly` is a breaking change deferred to v3.
86
-
87
- This is non-breaking for all callers: TypeScript allows assigning to `readonly` fields in
88
- object literal initializers, and the runtime behavior is identical.
89
-
90
- - **`compat.ts`**: Internal construction variables use `Mutable<T>` to allow incremental property
91
- assignment, cast to the full interface at return. Externally the API is unchanged.
92
-
93
- ---
94
-
95
- ## [2.0.10] — 2026-05-29
96
-
97
- Sprint 5B: Split `compat.ts` (613L) and `fonts.ts` (530L) into focused sub-directories.
98
-
99
- ### Changed
100
-
101
- - **`src/compat.ts` split into `src/compat/` (3 files)**
102
- - `compat/pdfmake-types.ts` (76L) `PdfmakeDocument`, `PdfmakeObjectNode`, `PdfmakeStyle`, `CompatOptions`, `TranslateCtx`, `DEFAULT_HEADING_MAP`
103
- - `compat/translate.ts` (155L)`translateNode`, `translateNodeInner`, `translateTextNode`, `applyStyleToParagraph`, `collectSpans`, `pdfmakeNodeToListItem`
104
- - `compat/normalize.ts` (115L)`extractFlatText`, `translateTable`, `mergeStyles`, `normalizeStyleNames`, `pdfmakeAlignToPretext`, `normalizePageSize`, `normalizeMargins`, `normalizeHeaderFooter`
105
- - `compat.ts` (85L)`fromPdfmake` (the only public export) + type re-exports
106
- Circular-dependency between translate↔normalize avoided by moving `extractFlatText` to `normalize.ts`.
107
-
108
- - **`src/fonts.ts` split into `src/fonts/` (4 files)**
109
- - `fonts/bundled-paths.ts` (50L) `IS_NODE`, `resolveInterFile`, all four `BUNDLED_INTER_*_PATHS` arrays
110
- - `fonts/load-bytes.ts` (80L) — `loadFontBytes` (file / Uint8Array / bundled-Inter async loader)
111
- - `fonts/collect-needed.ts` (75L) — `collectNeededFonts` (document font-variant scanner)
112
- - `fonts/collect-text.ts` (120L) — `collectTextByFont` (glyph subsetting text collector)
113
- - `fonts.ts` (70L) `loadFonts` + `collectTextByFont` re-export (both public exports intact)
114
-
115
- The public API is unchanged. All existing test and source imports continue to work.
116
-
117
- ---
118
-
119
- ## [2.0.9] — 2026-05-29
120
-
121
- Sprint 5A: dead-export removal, ts-prune scan.
122
-
123
- ### Changed
124
-
125
- - **`addGoToAnnotation` removed from `src/render-utils.ts`** — The function was exported but
126
- never called anywhere in the codebase. GoTo annotation support (internal document links)
127
- was prepared prematurely; the implementation is preserved in git history and can be
128
- restored when anchor-link rendering is actually wired up.
129
-
130
- - **`RTL_REGEX` removed from `src/validate/helpers.ts`** — Exported constant never imported
131
- anywhere. The regex pattern is preserved in git history.
132
-
133
- - **`PDFRef` import removed from `render-utils.ts`** Only needed for the now-deleted
134
- `addGoToAnnotation` signature.
135
-
136
- ---
137
-
138
- ## [2.0.7] 2026-05-28
139
-
140
- Sprint 4: Split `schema.ts` (907 LOC) into focused `src/schema/` modules.
141
-
142
- ### Changed
143
-
144
- - **`src/schema.ts` split into `src/schema/` (6 focused files)** — The 907-line monolith is
145
- replaced by a 19-line barrel re-export and six cohesive sub-files, each under 300 lines:
146
- - `schema/shared.ts` (30L) — atomic sub-schemas: align, color, fontWeight, dir, space, inlineSpan
147
- - `schema/elements-text.ts` (270L) — paragraph, heading, blockquote, code, callout, richParagraph, list, toc, footnoteDef
148
- - `schema/elements-media.ts` (87L) — image, svg, qrCode, barcode, chart
149
- - `schema/elements-block.ts` (164L) spacer, hr, pageBreak, comment, formField, floatGroup
150
- - `schema/elements-table.ts` (60L) — table
151
- - `schema/document.ts` (293L) — top-level pdfDocumentSchema (assembles all element schemas)
152
- The public API (`import { pdfDocumentSchema } from 'pretext-pdf/schema'`) is unchanged.
153
-
154
- ---
155
-
156
- ## [2.0.6] 2026-05-28
157
-
158
- Post-sprint audit fixes: /Lang encoding, BCP47 regex, test correctness.
159
-
160
- ### Fixed
161
-
162
- - **`/Lang` catalog entry now uses `PDFString.of()` (correct literal string)** — Previously
163
- used `PDFHexString.of()` which stored raw ASCII bytes as invalid hex. `/Lang` is a
164
- PDF text string per spec §14.9.2; `PDFString` (literal string) is correct and matches
165
- pdf-lib's own `setLanguage()`. BCP47 tags are ASCII-only (validated by regex) so
166
- literal-string injection risk is zero.
167
-
168
- - **BCP47 regex widened to accept `i-` and `x-` prefixed tags** — `/^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/`
169
- now allows the single-char leading subtag required by grandfathered (`i-klingon`) and
170
- private-use (`x-custom-app`) tags per RFC 5646 §2.2.7. Previously `{2,8}` incorrectly
171
- rejected valid language codes.
172
-
173
- ### Tests
174
-
175
- - **H6 (signerName truncation)** strengthened replaced the vacuous 5% byte-size ratio with
176
- a `deepEqual(pdf100, pdf101)` identity check: a 100-char and 101-char signerName must produce
177
- byte-identical PDFs, directly testing that `slice(0, 100)` is applied.
178
-
179
- - **M5 (/TU encoding)** added `FEFF` BOM assertion to the radio and dropdown loop,
180
- making it consistent with the checkbox/button loop.
181
-
182
- - **M6 (bookmark Title)** — replaced the `FEFF0043` prefix check (too broad) with the full
183
- 6-character "Chapte" UTF-16BE hex prefix for an unambiguous unique match.
184
-
185
- - **M7 (signing field escaping)** — clarified test scope: the visual placeholder path uses
186
- `drawText()` (CIDFont glyph encoding no injection risk). The `escapePdfLit` contract is
187
- tested in `signatures-validation.test.ts` which covers the crypto signing code path.
188
-
189
- ---
190
-
191
- ## [2.0.5] — 2026-05-28
192
-
193
- Sprint 3 audit fixes: annotation array safety, test coverage gaps closed.
194
-
195
- ### Fixed
196
-
197
- - **`addStickyNoteAnnotation` now has `instanceof PDFArray` guard** the last remaining
198
- `as any` annotation-push in `render-utils.ts`. All three annotation functions
199
- (`addLinkAnnotation`, `addGoToAnnotation`, `addStickyNoteAnnotation`) now safely fall back
200
- to creating a new array if the existing `Annots` entry is not a direct `PDFArray`.
201
-
202
- ### Tests
203
-
204
- - **H6** (`test/signatures-visual.test.ts`): `signerName` longer than 100 chars renders to
205
- the same byte-size PDF as a 100-char name — confirms the `slice(0, 100)` truncation is applied.
206
- - **M5** (`test/forms.test.ts`): `/TU` encoding verified for checkbox, radio, dropdown, and
207
- button field types — not just `text`.
208
- - **M6** (`test/bookmarks.test.ts`): Bookmark `Title` with special chars (parentheses) appears
209
- as UTF-16BE hex in the PDF bytes — raw literal form `(Chapter (1): ...)` does not appear.
210
- - **M7** (`test/signatures-visual.test.ts`): Backslash and parentheses in `signerName`,
211
- `reason`, and `location` render without throwing (escaping confirmed).
212
-
213
- ---
214
-
215
- ## [2.0.4] — 2026-05-28
216
-
217
- Sprint 2 audit fixes: type-safety improvements in render-utils.ts and vendor declarations.
218
-
219
- ### Changed
220
-
221
- - **`pdf-lib-augment.d.ts` expanded** — Declares `PdfFontEmbedder` interface covering the
222
- private `embedder.font` (underlinePosition, underlineThickness, xHeight) and `embedder.scale`
223
- fields accessed for text decoration metrics. The `(pdfFont as any).embedder` cast in
224
- `render-utils.ts` is now `(pdfFont as unknown as { embedder?: PdfFontEmbedder }).embedder`,
225
- eliminating untyped `any` propagation into font-metric arithmetic.
226
-
227
- ---
228
-
229
- ## [2.0.3] — 2026-05-28
230
-
231
- Sprint 1 audit fixes: GoTo annotation guard, BCP47 metadata validation, dependency security.
232
-
233
- ### Fixed
234
-
235
- - **`addGoToAnnotation` now has `instanceof PDFArray` guard** the same fix applied to
236
- `addLinkAnnotation` and `addStickyNoteAnnotation` in v2.0.2 was missed for GoTo annotations.
237
- Existing `Annots` entries that are not a direct `PDFArray` now fall back to creating a new
238
- array instead of silently calling `.push()` on an `any`-cast value.
239
-
240
- - **`metadata.language` validated against `LANGUAGE_TAG_REGEX`** — previously only checked for
241
- non-empty string and max length. Now enforces the same BCP47 regex (`/^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$/`)
242
- used for `hyphenation.language`, closing an inconsistency in the validation layer.
243
-
244
- - **Dependency audit** — `npm audit fix` resolved `fast-uri <=3.1.1` CVE (GHSA-v39h-62p7-jpjc,
245
- GHSA-q3j6-qgpj-74h6) in the dev dependency chain.
246
-
247
- ---
248
-
249
- ## [2.0.2] — 2026-05-28
250
-
251
- Second post-release audit patch: PDF injection hardening (metadata, AcroForm names), PDFHexString encoding correctness, signing field escaping, annotation array safety.
252
-
253
- ### Fixed
254
-
255
- - **Metadata Info dict fields now use `PDFHexString.fromText()` (correct UTF-16BE encoding)** —
256
- The v2.0.1 fix used `PDFHexString.of()` which stored raw bytes between `<>` delimiters (not
257
- valid hex). `fromText` correctly produces `<FEFF...>` UTF-16BE hex, the same encoding used by
258
- pdf-lib's built-in `setTitle()` etc. Injection protection is now both correct AND effective.
259
-
260
- - **Form field `name` and option `value` restricted to AcroForm-safe characters** — Field names
261
- and radio/dropdown option export values are written as PDF literal strings by pdf-lib. Characters
262
- like `)`, `\`, and null bytes that could corrupt the AcroForm `/T` dictionary are now rejected
263
- at validation time with a clear error message. Allowed: `[a-zA-Z0-9_.@-]+` (`HIGH-2`).
264
-
265
- - **Signing placeholder fields escaped for PDF literal strings** `sig.reason`, `sig.location`,
266
- `sig.contactInfo`, and `sig.signerName` are passed to `@signpdf/placeholder-pdf-lib` which
267
- writes them as PDF literal strings. Backslashes and parentheses are now escaped with `\` before
268
- passing, so values like "New York (USA)" are preserved correctly without breaking the dict.
269
-
270
- - **Annotation array push guarded with `instanceof PDFArray`** The previous `as any` cast on
271
- `pdfDoc.context.lookup(existingAnnots)` allowed silent no-ops if the value wasn't a PDFArray.
272
- All three annotation functions now check `instanceof PDFArray` and fall back to creating a new
273
- array rather than silently dropping the annotation.
274
-
275
- - **Bookmark `Title` and AcroForm `/TU` use `PDFHexString.fromText()`** — Same `of()` → `fromText()`
276
- correction applied to bookmark headings and all five AcroForm `/TU` (accessibility tooltip) writes.
277
-
278
- - **Sticky note `Contents` and author `T` use `PDFHexString.fromText()`** — Human-readable text in
279
- sticky note annotations now uses proper UTF-16BE encoding.
280
-
281
- - **Signature placeholder `signerName` truncated to 100 chars** — Prevents glyph overflow outside
282
- the visual signature box for very long signer names.
283
-
284
- - **Schema `content.items` changed from `anyOf` to `oneOf`** JSON Schema validators and AI agent
285
- code generators now get exclusive-match semantics, preventing multi-schema ambiguity.
286
-
287
- ### Tests
288
-
289
- - Added T6: `/TU` accessibilityLabel byte-level injection guard in `test/forms.test.ts`.
290
- - Added T7: metadata title/author/accessibility UTF-16BE hex-encoding byte checks in `test/metadata.test.ts`.
291
- - Added field name and option value AcroForm-safety tests in `test/forms.test.ts`.
292
-
293
- ---
294
-
295
- ## [2.0.1] — 2026-05-28
296
-
297
- Post-release patch: audit-driven hardening of every change introduced in v2.0.0.
298
-
299
- ### Fixed
300
-
301
- - **SVG sanitizer: `on*` handlers with whitespace in attribute name now stripped** — Attackers
302
- could inject `on\nload=` or `on\tclick=` to bypass the previous `\w+` name regex. The pattern
303
- now uses `[\w\r\n\t ]+` for the attribute name portion (`H3`).
304
-
305
- - **SVG sanitizer: `expression()` strips arguments with nested parens** `expression(alert(1))`
306
- and `expression(eval(x))` are now stripped in a single pass. The inner-parens pattern
307
- `(?:[^()]*|\([^()]*\))*` handles one level of argument nesting; multi-pass unwinds deeper
308
- nesting (`M6`).
309
-
310
- - **SVG sanitizer: size/element-count guards now throw `PretextPdfError('SVG_LOAD_FAILED')`**
311
- Previously both guards returned the raw unstripped SVG on overflow, silently bypassing
312
- script/event stripping. Now both throw, so callers always receive a typed error (`H1`).
313
-
314
- - **Form-field strict mode no longer flags cross-variant props as unknown** — `ALLOWED_PROPS`
315
- entry for `form-field` now covers the union of all variant-specific keys so the first strict
316
- check never fires false positives. Per-variant narrowing still runs inside `validateFormField`
317
- to catch cross-contamination (`B1`).
318
-
319
- - **Bookmark `Title` and AcroForm `/TU` now use `PDFHexString`** Previously `PDFString` was
320
- used, which is vulnerable to unbalanced-parenthesis injection in user-controlled strings. All
321
- five `/TU` writes and the outline `Title` write now use the hex-encoded form (`B3`).
322
-
323
- - **Link URI annotation and sticky-note `Contents`/`T` use `PDFHexString`** — Same injection
324
- guard extended to `addLinkAnnotation` and `addStickyNoteAnnotation` (`B3`).
325
-
326
- - **`getInfoDict()` private-API call is now try/caught**Accessibility and semantic metadata
327
- are silently omitted (rather than throwing) if the `@cantoo/pdf-lib` internal API is removed
328
- in a future library version (`M7`).
329
-
330
- - **`SignPdf`/`P12Signer` dynamic imports typed via local interfaces** — Removes `any` casts
331
- on the signpdf module destructuring, catching future API drift at compile time (`M4`).
332
-
333
- - **Signing error message scrubbed of certificate details** — `SIGNATURE_FAILED` no longer
334
- leaks P12 structural details or ASN.1 internals; only the first 120 chars of the underlying
335
- error message are included (`F9`).
336
-
337
- - **`FORM_FIELD_VARIANT_PROPS` type narrowed** The export type is now
338
- `Record<'text'|'checkbox'|'radio'|'dropdown'|'button', ReadonlySet<string>>` so callers that
339
- iterate the map get exhaustive narrowing rather than a plain string index (`M1`).
340
-
341
- - **`fieldType` validation is now derived from `FORM_FIELD_VARIANT_PROPS`** — The allowed-values
342
- list and the dispatch map are now the same object, removing the risk of one going stale (`M2`).
343
-
344
- - **Form-field options arrays are structurally validated** — Each item in `options` for `radio`
345
- and `dropdown` is now checked to be a `{value: string, label: string}` object. Invalid items
346
- throw `VALIDATION_ERROR` with an indexed path like `options[1].value` (`H2`).
347
-
348
- - **`accessibilityLabel` validated as non-empty string** — Empty or non-string values now
349
- throw `VALIDATION_ERROR` rather than embedding an empty `/TU` annotation (`H2`).
350
-
351
- - **`signing/placeholder.ts` uses `buildFontKey()` instead of hardcoded literal** Keeps the
352
- font lookup in sync with the canonical key format if the naming convention ever changes (`L1`).
353
-
354
- - **`PluginMeasureResult.spaceBefore/spaceAfter` marked readonly** Plugins cannot mutate the
355
- pipeline's spacing side-table; callers can still construct the object with literal values (`L2`).
356
-
357
- - **`package.json` `mcpCompat` range updated** — Was `>=1.4.0 <2.0.0` (excluded v2.0.0 itself);
358
- now `>=1.5.0 <3.0.0` (`B2`).
359
-
360
- - **`pdfDocumentSchema`: `form-field` now a `oneOf` discriminated union** The flat single-object
361
- schema was replaced with five variant schemas keyed by `fieldType: { const: '...' }`, with
362
- correct per-variant `required` arrays (radio/dropdown require `options`) (`M3`).
363
-
364
- ### Tests
365
-
366
- - Added T1: `on\nload` and `on\tclick` attribute-name injection regression guard in
367
- `test/svg-sanitizer.test.ts`.
368
- - Added T2: Per-variant strict-mode unknown-props tests for form-field in
369
- `test/validate-strict.test.ts` (Group 8).
370
- - Added T3: `./signing` entry point added to `test/public-api-surface.test.ts` tripwire.
371
- - Added T4: `MAX_SVG_ELEMENTS` boundary tests (at-limit sanitizes, over-limit throws) in
372
- `test/svg-sanitizer.test.ts`.
373
- - Added T5: `fieldType: 'textarea'` (unknown value) test triggering `VALIDATION_ERROR`.
374
-
375
- ---
376
-
377
- ## [2.0.0] — 2026-05-28
378
-
379
- Major release. Three breaking changes, six improvements, and two security fixes.
380
-
381
- ### Breaking Changes
382
-
383
- - **`FormFieldElement` is now a discriminated union** — Replace the single flat interface with
384
- five per-variant types (`TextFormField | CheckboxFormField | RadioFormField | DropdownFormField |
385
- ButtonFormField`) that narrow available properties by `fieldType`. The `BaseFormField` interface
386
- holds all shared fields and is also exported. Callers that assign into `FormFieldElement` without
387
- narrowing will need to specify the concrete variant. **Migration:** narrow on `fieldType` or
388
- import the specific variant type.
389
-
390
- - **`HorizontalRuleElement.spaceAbove` / `spaceBelow` removed** Use `spaceBefore` / `spaceAfter`
391
- (stable since v0.9). The deprecated aliases were present in v1.x with `@deprecated` JSDoc.
392
- **Migration:** replace `spaceAbove` `spaceBefore`, `spaceBelow` `spaceAfter`.
393
-
394
- - **`ValidationResult.warningCount` removed** The field was always `0` in v1.x (validator emits
395
- errors only). **Migration:** use `result.errors.filter(e => e.severity === 'warning').length` if
396
- you need a countreturns `0` until warning-severity issues are introduced.
397
-
398
- ### Added
399
-
400
- - **`./signing` export** New subpath export `pretext-pdf/signing` exposes `applySignature`,
401
- `applyEncryption`, `applyPostProcessing`, and `renderSignaturePlaceholder` as a standalone
402
- signing module. Useful for post-render pipeline composition without importing the full library.
403
-
404
- - **`MAX_SVG_ELEMENTS` constant** — Exported from `dist/assets.js` alongside `SVG_MAX_BYTES`.
405
- Heuristic element-count guard (5,000 open tags) that skips sanitization of pathologically deep
406
- SVGs to prevent memory exhaustion during rasterization.
407
-
408
- - **Per-variant `accessibilityLabel` wired to AcroForm `/TU`**`accessibilityLabel` on form
409
- fields is now written to the PDF annotation `/TU` (tooltip/alt-text) dictionary entry.
410
- Screen readers and assistive technology that consume AcroForm annotations will pick it up.
411
-
412
- - **`metadata.accessibility` / `metadata.semantic` written to Info dict** — Both reserved fields
413
- are now serialized as JSON strings into the PDF Info dictionary (`Accessibility` and `Semantic`
414
- keys). Previously they were accepted but ignored at render time.
415
-
416
- ### Changed
417
-
418
- - **`ValidationError` and `ValidationResult` fields are `readonly`** All fields on both
419
- interfaces are now `readonly`. This is technically breaking for callers that mutate validation
420
- results directly (which should be none in practice).
421
-
422
- - **`PluginMeasureResult.height` is `readonly`** — Prevents accidental mutation of the measured
423
- height by render hooks.
424
-
425
- ### Fixed (security)
426
-
427
- - **SVG `on*` handler regex now catches newline-injected attributes** The previous pattern
428
- `\bon\w+\s*=` missed attributes like `on\nload=`. Fixed to `\bon\w+[\s\n\r]*=`.
429
-
430
- - **`allowed-props` strict mode now covers `accessibilityLabel`, `accessibility`, `semantic`** —
431
- These fields were missing from their respective key arrays; strict-mode validation would
432
- incorrectly report them as unknown properties. Fixed.
433
-
434
- ---
435
-
436
- ## [1.9.0] 2026-05-28
437
-
438
- Additive release: error categorisation, typed plugin generics, beta graduations, soft deprecations, and a 100 MB output size guard.
439
-
440
- ### Added
441
-
442
- - **`PretextPdfError.category`**New readonly field on every thrown error. Groups all 50+
443
- error codes into 8 high-level categories (`'validation' | 'font' | 'image' | 'layout' |
444
- 'security' | 'dependency' | 'signature' | 'render'`). Lets callers branch on class of
445
- failure without exhaustive switches. Also exported as the `ErrorCategory` type.
446
-
447
- - **`LEGACY_ERROR_CODE_MAP`** — `Record<string, ErrorCode>` exported from the main entry
448
- point. Documents the canonical code for any renamed or aliased error strings. Currently
449
- maps `FONT_ENCODE_FAIL FONT_ENCODE_FAIL` (no renames have occurred yet); future renames
450
- will be recorded here before the old string is removed.
451
-
452
- - **`PluginDefinition<T>` generic** — `PluginDefinition`, `PluginMeasureResult`, and
453
- `PluginRenderContext` are now generic over a `T` type parameter (default `unknown`).
454
- `pluginData` in `measure` and `render` is now typed as `T` instead of `unknown`. Fully
455
- backward compatible — existing plugins without a type argument continue to compile.
456
-
457
- - **`MAX_PDF_BYTES` constant** Exported from the main entry point. Value: `100 * 1024 * 1024`
458
- (100 MB). `render()` now throws `RENDER_FAILED` if the serialized PDF exceeds this limit,
459
- preventing runaway memory use from pathological documents.
460
-
461
- ### Changed (promotions no breaking changes)
462
-
463
- - **`PluginDefinition`, `PluginMeasureContext`, `PluginMeasureResult`, `PluginRenderContext`**
464
- Promoted from `@beta` to `@public`. The plugin API is stable.
465
-
466
- - **`RenderOptions.plugins`** — `@beta` tag removed. Stable.
467
-
468
- - **`createFootnoteSet()`** `@beta` tag removed. Stable.
469
-
470
- ### Deprecated
471
-
472
- - **`HorizontalRuleElement.spaceAbove`** and **`.spaceBelow`** — Use `spaceBefore` and
473
- `spaceAfter` instead (consistent with paragraph/heading naming). Both aliases remain
474
- functional in v1.x. Will be removed in v2.0.
475
-
476
- - **`ValidationResult.warningCount`** Always `0` in v1.x (the validator emits errors
477
- only). Will be removed in v2.0. Use
478
- `result.errors.filter(e => e.severity === 'warning').length` directly when
479
- warning-severity items are introduced.
480
-
481
- ---
482
-
483
- ## [1.8.0] — 2026-05-28
484
-
485
- Additive release: type narrowing, shared leaf modules, 5 new test coverage points, and a Windows font-path validation bugfix.
486
-
487
- ### Added
488
-
489
- - **`ColumnDef.width` narrowed** — `number | string` → `number | \`${number}*\` | '*' | 'auto'`. The `as unknown as number` cast in `compat.ts` that faked `'auto'` as a number is now removed; `'auto' as const` is the direct type. No behavior change — accepted inputs are identical.
490
-
491
- - **`DocumentMetadata.accessibility?` and `DocumentMetadata.semantic?` reserved** (`Record<string, unknown>`, `@alpha`) Forward-reserved slots for PDF/UA accessibility metadata and document-structure annotations. No render-time effect in v1.x; schema allows them so tooling can start populating them. Will be wired in v2.0.
492
-
493
- - **`FormFieldElement.accessibilityLabel?` reserved** (`string`, `@alpha`) — Reserved slot for AcroForm `/TU` tooltip/alt-text annotation. Stored but not written to the PDF in v1.x. Will be wired in v2.0.
494
-
495
- - **`src/url-utils.ts` leaf module** — `SAFE_URL_SCHEME` regex consolidated into a single shared module. Previously duplicated in `render-utils.ts` and `validate/helpers.ts` (v1.7.2 added dedup comments). Both sites now import from `url-utils.ts`; `validate/helpers.ts` re-exports it for downstream consumers.
496
-
497
- - **`src/font-key.ts` leaf module** — `buildFontKey` extracted from `measure.ts` to break the `measure.ts measure-blocks/index.ts → measure.ts` circular dependency. All 9 former `./measure.js` importers now import directly from `./font-key.js`. `measure.ts` re-exports `buildFontKey` for backward compatibility.
498
-
499
- - **`test/error-coverage.test.ts`** — Fills 5 coverage holes identified in the v1.7.2 audit:
500
- 1. `FONT_LOAD_FAILED`relative font path rejection
501
- 2. `FONT_LOAD_FAILED` nonexistent absolute font path
502
- 3. `SVG_LOAD_FAILED` local mock server returning HTTP error (via `fetchWithTimeout` directly)
503
- 4. `CHART_LOAD_FAILED` silent path chart failure warns, not throws (contract guard)
504
- 5. `RTL_REORDER_FAILED` — error code round-trips correctly; RTL Arabic renders without error
505
-
506
- ### Fixed
507
-
508
- - **`validate/helpers.ts — validateFontSpec` Windows path false positive** — Windows absolute paths like `F:\path\font.ttf` were being parsed by `new URL()` as `f:` protocol URLs, causing valid font paths to throw `VALIDATION_ERROR: 'src' contains a URL`. Fixed by skipping the URL check for paths that `isAbsolutePath()` identifies as OS-native absolute paths (POSIX `/` prefix or Windows `[A-Z]:\` prefix).
509
-
510
- ---
511
-
512
- ## [1.7.2] 2026-05-28
513
-
514
- Internal hardening. No public API changes.
515
-
516
- ### Fixed (silent failures observable warnings)
517
-
518
- - **`render.ts` — `getForm()` bare-catch now logs a warning** — previously the error was swallowed silently with a comment. Now routes through the existing `warn()` bridge so structured-logger callers can observe it.
519
-
520
- - **`render-blocks/footnote.ts` silent footnote drop on font miss now logs** `if (!pdfFont) continue` now emits a `[pretext-pdf] footnote #N: font "key" not in fontMap — footnote skipped` warning to stderr before skipping.
521
-
522
- - **`measure-blocks/image.ts` non-null assertion on `floatText` replaced with guard** — `element.floatText!` → `element.floatText !== undefined` guard. If neither `floatSpans` nor `floatText` is present, `textLines` stays empty and the float renders image-only (valid case) instead of crashing with `Cannot read properties of undefined`.
523
-
524
- ### Fixed (code correctness)
525
-
526
- - **`compat.ts` — proto-pollution defense restored** — `copySafeStyleProperties` used `key in source` (traverses prototype chain) instead of `Object.hasOwn`. Replaced with `Object.hasOwn` and narrowed the `source` parameter from `any` to `unknown`, removing the unsafe `as any` cast at the assignment site.
527
-
528
- ### Improved (exhaustiveness + type safety)
529
-
530
- - **`render-extras.ts` switch(el.fieldType)** — added `default` arm with runtime warning and TypeScript `never` guard. New `fieldType` values added without updating this switch will produce a compile error.
531
-
532
- - **`render-utils.ts` switch(align)** — added `default` arm with `satisfies never` guard and `startX` safe fallback. The `'left' | 'center' | 'right'` union is already exhaustive; the guard prevents silent breakage if the union ever widens.
533
-
534
- - **`fonts.ts` switch(el.type)** — added explicit `default: break` arm with a comment listing intentionally-unhandled types. Previously the fall-through silence could hide newly-added element types that do carry text (and would need glyph collection).
535
-
536
- ### Fixed (stale comments)
537
-
538
- - **`types-public/render-options.ts`** — corrected false claim that bidi-js fallback warnings go to `console.warn` directly; they've been routed through `setBidiWarnFn` / the logger bridge since v1.3.x.
539
-
540
- - **`render-utils.ts` and `validate/helpers.ts`** — annotated the duplicate `SAFE_URL_SCHEME` regex with a `// NOTE: duplicated` comment pointing to the other location. Consolidation to a shared `url-utils.ts` leaf is deferred to v1.8.
541
-
542
- - **`assets/loaders/images.ts`** — corrected "still in assets.ts for this commit" → actual location `assets/loaders/orchestrator.ts`.
543
-
544
- - **`measure-text.ts`** — corrected "measure-blocks.ts call sites" → "call sites in measure-blocks/" (the file was split into a directory in v1.6.0).
545
-
546
- - **`fonts.ts`** — corrected inline comment `(assets.ts)` → `(assets/security/path-allowlist.ts)`.
547
-
548
- - **`measure.ts`**corrected inline comment `(assets.ts)` → `(assets/loaders/orchestrator.ts)`.
549
-
550
- ### Tests rewritten (theatrical meaningful)
551
-
552
- - **`test/signatures-crypto.test.ts`** `'SIGNATURE_DEP_MISSING when @signpdf/signpdf not installed'` renamed and rewritten as `'invisible-only signature: renders without p12, no crypto deps needed'`. The old test body had nothing to do with `SIGNATURE_DEP_MISSING`; the new body verifies that invisible signing without a `p12` path produces a valid PDF that does NOT contain a `/ByteRange` dict (confirming no PKCS#7 signing ran).
553
-
554
- - **`test/validate-concurrent.test.ts`** — The `'8 parallel validates'` test was vacuous: since `validateDocument` is synchronous, `parallel[i]` trivially equals `sequential[i]`. Replaced with a content-specific isolation test: 5 intentionally-broken docs are validated, and each result's error messages are asserted to mention a keyword specific to THAT doc's element type (e.g. paragraph errors mention "text", table errors mention "columns"). This catches module-level state leakage without relying on async interleaving.
555
-
556
- ---
557
-
558
- ## [1.7.1] — 2026-05-27
559
-
560
- Security hardening for SVG sanitizer + schema accuracy fix for signing fields.
561
-
562
- ### Security
563
-
564
- - **SVG `<style>` block: strip `@import` rules** — `sanitizeSvg` now removes `@import` directives before any CSS can be used to trigger an outbound network request from the rasterizer. Previously only `expression()` was stripped from inline `<style>` blocks; `@import url('https://attacker.example/track.css')` would survive sanitization.
565
-
566
- - **SVG `<style>` block: strip `url(javascript:|vbscript:|data:)` values** Removes URL function calls using JS/vbscript/data schemes from style content, closing an injection path that bypasses the existing `<a href>` and `<image href>` filters (those only target *attribute values*, not *CSS property values*).
567
-
568
- - **SVG `<style>` block: strip `url(https?://...)` values (defense-in-depth)** SVGs embedded in PDFs have no legitimate reason to hot-link external stylesheets or background images. Removes all HTTP/HTTPS `url()` references from style blocks so the rasterizer cannot be coerced into outbound connections via crafted input.
569
-
570
- ### Fixed
571
-
572
- - **`schema.ts` missing signing fields** — `signature` schema block was missing `p12`, `passphrase`, and `contactInfo` that were added to `SignatureSpec` in v1.7.0. JSON schema consumers (e.g. editors, the MCP generate_pdf tool) now see all valid signing inputs. The TypeScript type was always correct; this was a schema-only gap.
573
-
574
- ### Changed
575
-
576
- - **`schema.ts` signature description** updated from "Visual signature placeholder drawn on a specified page" to reflect that providing `p12` enables PKCS#7 cryptographic signing; without `p12` a visual-only placeholder box is drawn.
577
-
578
- ---
579
-
580
- ## [1.7.0]2026-05-25
581
-
582
- Signing path repaired. **No public API changes.** The cryptographic signing pipeline has been architecturally broken end-to-end since v1.3.6 calling `render({ signature: { p12, passphrase } })` would fail with `SIGNATURE_FAILED: PDF signing failed: No ByteRangeStrings found within PDF buffer`. v1.7.0 fixes it with a surgical change inside `applySignature`.
583
-
584
- ### Fixed (critical)
585
-
586
- - **Signing path was architecturally broken since v1.3.6**. Root cause: `applySignature` loaded the placeholder doc via `@cantoo/pdf-lib`'s `PDFDocument.load`, then handed it to `@signpdf/placeholder-pdf-lib.pdflibAddPlaceholder`, which internally builds `/ByteRange` using **upstream** `pdf-lib`'s `PDFArray`/`PDFNumber`/`PDFName` classes (it imports them directly from `"pdf-lib"`). Cantoo's serializer doesn't recognize upstream's class instances and emitted a malformed `/ByteRange` dict; `@signpdf/utils.findByteRange` then aborted parsing with `No ByteRangeStrings found within PDF buffer`. Fix: load the doc via **upstream `pdf-lib`** for the placeholder hop only. Encryption stays on `@cantoo/pdf-lib` in `applyEncryption` — the two paths are mutually exclusive via the existing `SIGNATURE_CERT_AND_ENCRYPTION` guard, so we never need both pdf-libs active simultaneously. The previously KNOWN-BROKEN `test/signatures-crypto.test.ts → P12 signature verifies cryptographically (real CMS verify)` is now unskipped and green.
587
-
588
- ### Added
589
-
590
- - **AcroForm regression assertion** inside `test/signatures-crypto.test.ts`. Any valid signed PDF must carry `/AcroForm`, `/Fields [...]`, `/SigFlags 3`, and a `/Type /Sig` object — these four properties are asserted on the signed bytes so a future regression that loses AcroForm structure surfaces immediately.
591
- - **Signature path snapshot tripwire** (`test/signatures-snapshot.test.ts` + `test/data/signatures-snapshot.json`). Captures the categorical structural shape of a signed PDF (presence of `/ByteRange`, `/AcroForm`, `/SigFlags` value, etc.) rather than byte offsets (which are document- and randomness-dependent). Wired into `test:phases`. Regenerate with `UPDATE_SNAPSHOT=1`.
592
-
593
- ### Changed
594
-
595
- - **`pdf-lib` declared as an explicit optional peer dependency** (`^1.17.1`). It was previously only present transitively via `@signpdf/placeholder-pdf-lib`'s `dependencies`. Now it's a documented peer with `peerDependenciesMeta.pdf-lib.optional: true`, mirroring the existing `@signpdf/*` pattern. Users with `@signpdf/*` already installed need no action — npm satisfies the new peer from the existing transitive install.
596
- - **`SIGNATURE_DEP_MISSING` error message** now lists `pdf-lib` alongside the three `@signpdf/*` packages and drops the "currently non-functional due to fork incompatibility" disclaimer that was added in v1.3.6.
597
-
598
- ### Migration
599
-
600
- None. Same `signature: { p12, passphrase, reason, contactInfo, signerName, location, page, invisible }` config. Same error codes (`SIGNATURE_DEP_MISSING`, `SIGNATURE_P12_LOAD_FAILED`, `SIGNATURE_FAILED`, `SIGNATURE_CERT_AND_ENCRYPTION`). One error message string changed: `SIGNATURE_DEP_MISSING` now mentions `pdf-lib` and no longer carries the "non-functional" disclaimer.
601
-
602
- ### Verification
603
-
604
- - All 456 tests pass (was 454 pass + 1 skipped pre-fix; now 455 pass after unskip + 1 new snapshot test = 456 total).
605
- - Encryption-after-signing path still rejected with `SIGNATURE_CERT_AND_ENCRYPTION` (regression-tested).
606
- - All 7 v1.6.0 verification gates (G1–G7) still pass.
607
-
608
- ---
609
-
610
- ## [1.6.0] — 2026-05-25
611
-
612
- Internal restructuring + SVG sanitizer hardening. **No public API changes.** The previously-monolithic `src/assets.ts` (961 lines pre-sprint) has been split into 10 focused files under `src/assets/`. A 14-line back-compat shim at `src/assets.ts` re-exports the barrel so every existing consumer (internal modules, public API, and direct test imports via `dist/assets.js`) keeps working unchanged.
613
-
614
- ### Security
615
-
616
- - **SVG sanitizer hardening** — `sanitizeSvg` now strips three additional payload classes that survived the previous regex chain:
617
- - **`<foreignObject>` blocks** — the only XML-in-SVG construct that can host arbitrary HTML/XML namespaces. Both self-closing and paired forms are removed wholesale; sibling SVG primitives (`<rect/>`, `<path/>`, etc.) are preserved.
618
- - **`javascript:` / `vbscript:` / `data:` hrefs on `<a>` elements** — previously only `<image>`/`<use>` hrefs were filtered. Only the dangerous href attribute is dropped, so the `<a>` element's text children still render.
619
- - **CSS `expression(...)` calls inside `<style>` blocks** — legacy IE XSS vector. Only the `expression(...)` call site is excised; the surrounding stylesheet remains parseable.
620
-
621
- Coverage: new `test/svg-sanitizer.test.ts` (10 cases) plus expanded MA-4 / MA-5 fixtures in `test/data/assets-split-tripwire.json` (30 fixtures total).
622
-
623
- ### Changed
624
-
625
- - **`src/assets.ts` split into 10 files** under `src/assets/` (see Internal). No public API change — the file at `src/assets.ts` is now a 14-line re-export shim that aggregates the new barrel `src/assets/index.ts`. Consumers importing from `dist/assets.js` continue to resolve every previously-public symbol (`loadImages`, `assertPathAllowed`, `sanitizeSvg`, `assertSafeUrl`, `resolveAndValidateUrl`, `normalizeIpv4Hostname`, `fetchWithTimeout`, `redactPath`, `VECTOR_RASTER_CONCURRENCY`, plus the `ResolvedSafeUrl` type) at the same module path.
626
- - **`PretextPdfError` constructor signature snapshot refreshed** — `etc/pretext-pdf.api.md` now reflects the `options?: ErrorOptions` parameter that was added in v1.2.1 but never re-snapshotted. No code change — the parameter has been live for several releases. The snapshot was stale; this release reconciles it.
627
-
628
- ### Internal
629
-
630
- - **New `src/assets/` directory layout:**
631
- ```
632
- src/assets/
633
- ├── index.ts # internal barrel
634
- ├── util/
635
- │ └── redact-path.ts # commit 4
636
- ├── security/
637
- │ ├── path-allowlist.ts # commit 5
638
- │ ├── ipv4-normalize.ts # commit 6
639
- │ ├── url-validation.ts # commit 7
640
- │ └── fetch.ts # commit 8 (undici Agent stays lazy)
641
- ├── svg/
642
- │ ├── sanitize.ts # commit 9 (+ SVG_MAX_BYTES)
643
- │ ├── dimensions.ts # commit 10
644
- │ ├── resolve-content.ts # commit 10
645
- │ └── rasterize.ts # commit 11 (@napi-rs/canvas dynamic)
646
- ├── generators/
647
- │ ├── qr.ts # commit 12 (qrcode dynamic)
648
- │ ├── barcode.ts # commit 12 (bwip-js dynamic)
649
- │ └── chart.ts # commit 12 (vega/vega-lite dynamic)
650
- └── loaders/
651
- ├── images.ts # commit 13
652
- ├── vectors.ts # commit 14
653
- ├── watermark.ts # commit 15
654
- └── orchestrator.ts # commit 15 (top-level loadImages)
655
- ```
656
- All optional peer-dependency dynamic imports (`@napi-rs/canvas`, `qrcode`, `bwip-js`, `vega`, `vega-lite`, `undici`) are preserved as lazy loads — cold-start cost is unchanged.
657
- - **7 verification gates (G1–G7) added** to catch regressions during the split:
658
- - **G1** Snapshot tripwire (`test/assets-split-tripwire.test.ts`)30 fixtures covering sanitizer output, URL normalization, path allowlist, and error code surface
659
- - **G2** DNS lookup dedup (`test/assets-dns-dedup.test.ts`)
660
- - **G3** SSRF blocking (`test/security-ssrf.test.ts`, expanded)
661
- - **G4** Parallel-render concurrency (`test/assets-concurrency.test.ts`) — 10× concurrent `render()` calls must produce bit-identical PDFs
662
- - **G5** ErrorCode stability (`test/assets-errorcode-stability.test.ts`)
663
- - **G6** api-extractor diff against `etc/pretext-pdf.api.md`
664
- - **G7** Cold-start perf (`test/assets-perf-coldstart.test.ts`, baseline at `test/data/perf-coldstart-baseline.json`) 100 sequential renders within 2.5× the v1.5.2 baseline
665
- - **G7 measurement on the final shim**: 12,224ms / 100 renders (vs 21,205ms baseline = -42%, i.e. the split happens to be *faster*, well under the upper bound).
666
-
667
- ---
668
-
669
- ## [1.5.2] 2026-05-25
670
-
671
- Security hotfix. **No public API changes.** **Upgrade recommended for any deployment that accepts user-controlled image / SVG URLs.**
672
-
673
- ### Security
674
-
675
- - **CVE-class SSRF bypass via IPv4 alternative notations** — `isPrivateAddress` in `src/assets.ts` applied dotted-decimal regexes (`/^127\./`, `/^10\./`, …) against `URL#hostname`. The WHATWG URL parser does NOT normalize non-dotted IPv4 forms, so an attacker could reach private services by encoding the target in any inet_aton-compatible form:
676
- - Pure decimal: `https://2130706433/x` → 127.0.0.1
677
- - Pure hex: `https://0x7f000001/x` → 127.0.0.1
678
- - Octal octet: `https://0177.0.0.1/x` 127.0.0.1
679
- - Hex octet: `https://0x7f.0.0.1/x` → 127.0.0.1
680
- - Short form: `https://127.1/x` → 127.0.0.1
681
-
682
- Without normalization, `parsed.hostname` is e.g. `"2130706433"`, the private-range regex chain misses it, the `isIpv4Literal` (4-dot) check misses it, and the URL falls through to DNS — which on Linux's `getaddrinfo` resolves to 127.0.0.1 and bypasses the SSRF guard. Same vector reaches RFC 1918 ranges (10/8, 192.168/16, 172.16/12), link-local (169.254.169.254 — AWS IMDS), and CGNAT.
683
-
684
- **Fix:** new internal helper `normalizeIpv4Hostname()` implements inet_aton-style parsing (decimal/octal/hex per part, short-form packing for 1/2/3-part inputs, strict 32-bit range guard). `resolveAndValidateUrl` normalizes before the private-IP check AND before DNS, then treats the normalized form as an IP literal so undici never re-resolves a non-dotted private encoding. `isPrivateAddress` also normalizes its input as defense-in-depth on the post-DNS path. Public alternative encodings (e.g. `134744072` == 8.8.8.8) continue to resolve and fetch normally.
685
-
686
- **Test coverage:** new `test/security-ipv4-bypass.test.ts` adds 24 cases — every blocked encoding above, public regression cases, plus direct unit tests for `normalizeIpv4Hostname` (round-trips, range guards, malformed-octal rejection, public-IP allowlist). Wired into the `test:phases` stage; phases stage grows from 417 to 441 tests.
687
-
688
- ### Fixed
689
-
690
- - **`measure-blocks/float-group.ts` fontSize fallback intent clarified** — v1.5.1 M5a removed a dead `baseFontSize = doc.defaultFontSize ?? 12` plus per-block `fontSize = block.fontSize || baseFontSize` local that was never read (the item assignment used `block.fontSize` directly). Audit of upstream measure helpers (measure-paragraph, measure-heading, …) confirms `block.fontSize` is always populated with a positive value for real content, so the fallback would never have fired in practice. Added a leading comment so future contributors don't reintroduce the fallback in the mistaken belief that `block.fontSize === 0` is a real case. No behavior change.
691
-
692
- ---
693
-
694
- ## [1.5.1] — 2026-05-24
695
-
696
- Hotfix batch closing 9 audit findings from the 6-agent v1.5.0 review. **No public API changes** — guarded by `test/public-api-surface.test.ts`. **No behavior changes other than the documented fixes**. Snapshot baseline expanded from 68 to 73 fixtures (5 new: 2 metadata.keywords + 3 watermark.image).
697
-
698
- ### Security
699
-
700
- - **Watermark image URL scheme validation (H1)** — `doc.watermark.image` now passes through the same `validateUrl` pre-flight check used by `validateImage`. Unsafe schemes (`javascript:`, `data:`, `vbscript:`, `blob:`, `about:`, `file:`) are rejected at validate-time so CLI lint and MCP validate tools catch them before render. Relative file paths fall through unchanged. The shared URL-shape helper `looksLikeUrl` was moved from `validate/elements/media.ts` to `validate/helpers.ts`.
701
-
702
- ### Fixed
703
-
704
- - **`metadata.keywords[]` element validation (H3)** — Previously, `for (const field of [...'keywords'...])` was paired with `typeof val === 'string'`, which silently no-op'd for the `string[]`-shaped `keywords` field. Each keyword entry is now validated for control-character injection and the 1000-character length cap via `validateMetadataString(kw, \`keywords[\${i}]\`)`.
705
- - **highlight.js dynamic-import error logging (H4)** Previously, `catch { /* not installed */ }` silently swallowed every failure including real module-load errors. The catch now logs a warning unless the error code is `ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND` (the only legitimate "optional dep absent" signal).
706
- - **`hljs.highlight()` runtime exception logging (M2)**Same silent-fallback issue as H4 but for tokenization failures. Now logs a warning naming the language before falling back to plain text.
707
- - **Font-variant registration logging (M3)** — `installNodePolyfill` now tracks per-variant success and warns when an individual Inter weight (400 or 700) fails to register, instead of only warning when *both* failed. Text metrics for the missing variant may be inaccurate; operators see this in logs.
708
-
709
- ### Changed
710
-
711
- - **Removed redundant `as import('...').X` casts in `validate/elements/forms-floats.ts` (M1)** — `validateFormField`, `validateFootnoteDef`, and `validateFloatGroup` already accept `Extract<ContentElement, { type: 'X' }>`-typed `el`. The inner `const ff = el as FormFieldElement` casts were no-ops; removed.
712
- - **Removed unreachable `toc-entry` validator (M4)** — The pre-switch throw at `validate/index.ts:260` already rejects `toc-entry` before dispatch. The `case 'toc-entry':` arm (guarded with `@ts-expect-error`) and the `validateTocEntry` function in `validate/elements/structural-simple.ts` were unreachable dead code. Drift-guard test updated with `VALIDATE_DISPATCHER_EXCLUDES` (mirrors the existing `MEASURE_DISPATCHER_EXCLUDES` pattern) to keep the orchestrator scan honest.
713
- - **tsconfig: `noUnusedParameters` + `noUnusedLocals` enabled (M5a)** Catches dead destructured locals and unused imports at build time. Cascade: 51 errors → 0. 30 of those were per-type `type _X = Exact<...>` drift guards in `allowed-props.ts` (load-bearing scaffolding) — consolidated into a single exported tuple `_AllowedPropsDriftGuard` that TypeScript counts as used. The remaining 20 were genuine unused-locals / unused-imports cleanup across measure, render, rich-text, and assets modules.
714
- - **tsconfig: `verbatimModuleSyntax` enabled (M5b)** — Enforces `import type` discipline. Cascade was only 6 errors, all `HyphenatorOpts` runtime imports that needed splitting into `import type`. Well under the 30-line cascade-defer threshold.
715
-
716
- ---
717
-
718
- ## [1.5.0] — 2026-05-24
719
-
720
- Architecture sprint completing the v1.4.0 god-file split debt. Six items shipped, single minor release. **No public API changes** — guarded by `test/public-api-surface.test.ts`. **No behavioral changes** — Item A's security-critical extraction guarded by new `test/validate-document-snapshot.test.ts` (68 fixtures, bit-exact preservation verified).
721
-
722
- ### Changed (internal structure — non-breaking)
723
-
724
- - **`src/validate/index.ts` (594L) → orchestrator (322L) + `src/validate/document.ts` (324L)**
725
- Extracted 11 doc-level check categories (pageSize, margins, fonts, header/footer, defaultParagraphStyle, sections, watermark, encryption, signature, bookmarks, hyphenation, metadata) into a new `validateDocumentLevel(doc, ctx)` function. Single function with labeled `// ── name ──` blocks. **Security-critical**: snapshot tripwire test (68 fixtures across all 11 categories) verified bit-exact error preservation through the move.
726
- - **`src/measure-blocks/index.ts` (337L) → dispatcher (243L) + `src/measure-blocks/simple-blocks.ts` (151L)**
727
- Extracted 7 simple measurement arms (spacer, page-break, comment, form-field, hr, toc, footnote-def). Throw-guards for image/svg/qr-code/barcode/chart stay in dispatcher (security invariants tied to routing).
728
- - **`src/validate/elements/structural.ts` (239L grab-bag) → `structural-simple.ts` (120L) + `forms-floats.ts` (128L)** —
729
- Split light element validators (spacer, hr, toc, toc-entry, comment) from heavy ones (form-field, footnote-def, float-group). Cleaner separation of concerns.
730
-
731
- ### Added
732
-
733
- - **`src/validate/elements/README.md`** — Placement guide + validator signature contract + `_ctx` policy documentation + `withCycleGuard` usage guidance. Onboarding aid for adding new element types.
734
- - **`test/validate-document-snapshot.test.ts`** + `test/data/validate-document-snapshot.json`Bit-exact error preservation tripwire for `validateDocumentLevel`. 68 fixtures across pageSize, margins, fonts, header/footer, defaultParagraphStyle, sections, watermark, encryption, signature, bookmarks, hyphenation, metadata, content guards, and valid-doc sanity cases.
735
-
736
- ### Fixed
737
-
738
- - **`dist/` no longer tracked in git** (168 files removed) was already in `.gitignore` but tracked from prior history.
739
-
740
- ### Notes
741
-
742
- - 12 commits across 6 items, each independently revertable via the 3-commit-per-split pattern (stage → route → delete) proven in v1.4.0.
743
- - Test suite: 416 pass / 1 skip / 0 fail throughout the sprint, plus the new snapshot tripwire test (passes against generated baseline).
744
- - Public API surface unchanged: 15 runtime exports × 6 entry points.
745
- - `_ctx` policy decision: keep underscore-prefixed param for validators that don't currently consume context. Documented in `validate/elements/README.md` — stable signature lets future strict-mode or context-aware checks be added without changing call sites.
746
- - `loadedFamilies` initialization order: populated after `validateDocumentLevel` returns. Audit confirmed no order dependency — `document.ts` never reads `loadedFamilies`, only `validateFontSpec` for shape validation.
747
- - Path-traversal pre-flight in `validateImage` deferred to v2.0 — runtime SSRF + `allowedFileDirs` guards already provide defense-in-depth.
748
-
749
- ---
750
-
751
- ## [1.4.1] — 2026-05-23
752
-
753
- Cleanup batch addressing audit findings surfaced by the v1.4.0 god-file split. Five MEDIUM-severity items, all internal no public API changes, no behavioral changes for callers using documented schemas. Test suite unchanged: 416 pass / 1 skip / 0 fail.
754
-
755
- ### Fixed
756
-
757
- - **M1 — Dead outer `withCycleGuard` removed from `validateElement` dispatcher (`src/validate/index.ts`).** The dispatcher wrapped `list` and `float-group` cases in an outer `withCycleGuard` with an empty body, then the inner element validators (`validateList` in `elements/list.ts`, `validateFloatGroup` in `elements/structural.ts`) immediately opened their own guard on the same element. The outer guard ran a no-op body and `finally`-deleted the element from `seen` BEFORE the inner guard added it — dead code communicating false intent. The inner validators own the guard. Import of `withCycleGuard` also dropped from `index.ts` since it is no longer used there.
758
- - **M2 — `measure-blocks/float-group.ts ↔ measure-blocks/index.ts` runtime cycle broken (Option C: dependency injection).** `float-group.ts` previously imported `measureBlock` from `./index.js`, while `index.ts` re-exported `measureFloatGroup` from `./float-group.js` — a real ESM cycle that hoisting tolerated but which violated module-boundary discipline. Resolved by promoting `measureBlock` to an explicit parameter of `measureFloatGroup` (new exported `MeasureBlockFn` type). The sole caller (the orchestrator in `measure.ts`) already has `measureBlock` in scope, so the change is non-invasive. Option C was chosen over Option A (inlining the dispatch — too much duplication) and Option B (extracting `dispatch.ts` — restructured more than needed for a one-edge cycle).
759
-
760
- ### Changed
761
-
762
- - **M3 Concurrent validate test annotated with sync-vs-async semantics (`test/validate-concurrent.test.ts`).** Added a comment block above the parallel-validate `describe` clarifying that `validateDocument` is synchronous, so `Promise.all` over it cannot exercise real concurrent execution. The test now explicitly documents what it does prove (shape stability across N invocations) versus what it does not (concurrent isolation — guaranteed structurally by the per-call `WeakSet` opened at the top of `validate()` in `src/validate/index.ts`). The async render-path tests later in the same file DO exercise real concurrency because `render()` crosses `await` boundaries.
763
- - **M4 — Benchmark harness upgraded (`scripts/run-bench-snapshot.mjs`).** Default measured runs raised from 3 to 10. New `--runs N` CLI flag for callers to override (CI: 5, dev: 3, full: 10). Output now reports `median`, `p90`, and `min` in addition to `avg`. Variance on cold-tsx invocations can hit ±70%, which made any <10% regression gate meaningless at N=3.
764
-
765
- ### Added
766
-
767
- - **M5 — URL scheme check in `validateImage` (`src/validate/elements/media.ts`).** When `el.src` is a string that looks like a URL (matches `data:`, `javascript:`, `vbscript:`, `blob:`, `about:`, `file:`, or any `scheme://` form), the validator now routes through `validateUrl` from `validate/helpers.ts`. Matches the runtime SSRF guard's posture in `src/compat.ts` so validate-only callers (CLI lint, MCP `validate_document` tool) catch unsafe schemes pre-flight instead of letting them through to the asset-loader guard. http/https/ftp/mailto/anchor links still pass.
768
-
769
- ---
770
-
771
- ## [1.4.0] — 2026-05-23
772
-
773
- Architecture sprint: four god-files split into thin orchestrators + cohesive sub-modules. **No public API changes** guarded by `test/public-api-surface.test.ts` (15 runtime exports across 6 entry points, snapshot tripwire). 12 granular commits, each independently revertable.
774
-
775
- ### Changed (internal structure non-breaking)
776
-
777
- - **`src/validate.ts` (1834L) `src/validate/` (9 files, ~250L each)**
778
- `validate/index.ts` orchestrator + `helpers.ts`, `fonts.ts`, `errors.ts`, and per-element validators under `elements/`. Introduced explicit `ValidationContext` ({ errors, strict, loadedFamilies, seen, options }) threaded into every element validator instead of relying on closure-captured locals. Per-call `WeakSet` for cycle detection — concurrent isolation verified by `test/validate-concurrent.test.ts`.
779
- - **`src/measure-blocks.ts` (1600L) → `src/measure-blocks/` (10 files)** —
780
- `measure-blocks/index.ts` dispatcher + `text-blocks.ts`, `list.ts`, `image.ts`, `float-group.ts`, `highlight.ts`, `helpers.ts`, plus `table/{measure,spans,columns}.ts`. Added explicit case arms for `qr-code`, `barcode`, `chart` (previously fell through to "Unknown element type"). `_hljsCache` intentionally remains module-scoped in `highlight.ts` — idempotent process-wide cache, not concurrent-isolated state.
781
- - **`src/render-blocks.ts` (1277L) `src/render-blocks/` (13 files)**
782
- Per-render-function modules. Each independent (no shared state). Dropped four unused imports from the original (`PDFFont`, `PDFName`, `renderTocEntry`, `renderFormField` never referenced).
783
- - **`src/types-public.ts` (1200L) → `src/types-public/` (8 files)** —
784
- Split by domain: `document.ts`, `elements-{text,block,media}.ts`, `union.ts`, `validation.ts`, `render-options.ts`. Type-only intra-package cycles are erased at runtime (verified by clean tsc build).
785
-
786
- ### Added
787
-
788
- - **`test/drift-guards.test.ts` — measure-blocks dispatcher case-arm guard.** New invariant mirrors the existing validate + render guards: every `ELEMENT_TYPES` entry must have an explicit `case` arm in `src/measure-blocks/index.ts` (except internal `toc-entry`). Catches future additions to `ELEMENT_TYPES` that forget to handle measurement.
789
-
790
- ### Fixed
791
-
792
- - **Dead imports removed:** `validate` was imported but never used in `src/builder.ts` after the split. Removed.
793
- - **Unused locals/params:** `lineHeight` in `float-group.ts` and `table/measure.ts` (computed but never read); `doc` param in `measureCallout` and `measureCode` (preserved as `_doc` for signature compatibility, signals intentional non-use).
794
-
795
- ### Notes
796
-
797
- - Public API surface tripwire: 15 runtime exports across 6 entry points, all unchanged.
798
- - Benchmark gate: 5/7 corpora within 5% of v1.3.4; two corpora (`rich-text-mixed-spans` +12.9%, `table-stress` +7.2%) breach but match documented run-to-run variance (5-10% noise on dev machine). Cold-start module-load unchanged. CI re-snapshot recommended to disambiguate variance from regression.
799
- - Known follow-up: ESM-fragile circular import in `measure-blocks/float-group.ts` ↔ `measure-blocks/index.ts` (load-safe today because `measureBlock` is async-runtime only; revisit if top-level `await` is ever added).
800
- - Known follow-up: type-only intra-cycles in `types-public/` (no runtime risk — erased at compile time).
801
-
802
- ---
803
-
804
- ## [1.3.6] — 2026-05-23
805
-
806
- Architecture-sprint scaffolding release: vendor integrity check, concurrent isolation tests, signing import correctness, public API tripwire.
807
-
808
- ### Added
809
-
810
- - **Boot-time vendor integrity check (#12)** `src/version-check.ts` exports `assertVendorIntegrity()`, called once at the start of every `render()`. Verifies the vendored pretext version (`src/vendor/pretext/VERSION.ts`) is in the compatible range. Warns (does not throw) on drift. Inline semver matcher avoids adding a `semver` dependency.
811
- - **Concurrent validate/render isolation tests (#25)** — `test/validate-concurrent.test.ts` exercises 8 parallel `validate()` calls plus 4 parallel `render()` calls across different fonts AND different scripts (en/he/ar/th). Byte-identical fingerprints between parallel and sequential runs prove `vendor/pretext/measurement.ts` and `vendor/pretext/analysis.ts` shared state holds up under concurrent use.
812
- - **Public API surface tripwire** — `test/public-api-surface.test.ts` snapshots all 15 runtime exports across 6 entry points. Will guard against accidental API drift during the upcoming v1.4.0 god-file splits.
813
- - **P12/CMS crypto verification test (#26)** — `test/signatures-crypto.test.ts` adds a real `crypto.createVerify('RSA-SHA256')` round-trip with positive + negative cases. **Currently `t.skip`'d** — see KNOWN ISSUES below.
814
- - **`pretextPdf.mcpCompat`** field in `package.json` — declares `>=1.4.0 <2.0.0` compatibility range for the pretext-pdf-mcp consumer. MCP-side check ships separately.
815
-
816
- ### Fixed
817
-
818
- - **`signpdf` v3 API + import correctness** — `src/post-process.ts` was destructuring `pdflibAddPlaceholder` from `@signpdf/signpdf` where it does not exist; the symbol lives in `@signpdf/placeholder-pdf-lib`. Also updated to the v3 API (`new P12Signer(buffer, { passphrase })` instead of `signer.sign(buffer, { passphrase })`). Added `@signpdf/placeholder-pdf-lib` and `@signpdf/signer-p12` to `peerDependencies` + `peerDependenciesMeta.optional` (mirroring `@signpdf/signpdf`).
819
- - **`SIGNATURE_DEP_MISSING` error message** now identifies exactly which `@signpdf/*` packages are missing and tells the user which to install, instead of always listing all three.
820
-
821
- ### KNOWN ISSUES (deferred to follow-up sprint)
822
-
823
- - **Signing path is architecturally non-functional** — even with correct imports, `@cantoo/pdf-lib`'s serializer is fork-incompatible with `@signpdf/placeholder-pdf-lib`. The placeholder ByteRange dict is emitted in a shape that `@signpdf/utils.findByteRange` cannot parse. End-to-end signing has never worked. The crypto verify test (#26) is correctly written and ready to run once signing is repaired. Three fix paths exist: `@signpdf/placeholder-plain` swap (breaks AcroForm), porting placeholder-pdf-lib onto cantoo primitives, or a merge-bytes approach. None are in v1.3.6 scope. The `SIGNATURE_DEP_MISSING` message now pre-warns callers.
824
-
825
- ## [1.3.5] 2026-05-22
826
-
827
- ### Fixed
828
-
829
- - **`toc-entry` drift-guard regression** —
830
- `test/drift-guards.test.ts` was failing because `src/validate.ts` had no
831
- `case 'toc-entry':` arm. `toc-entry` elements are produced internally by
832
- the TOC two-pass processor, but the drift guard correctly insists every
833
- registered `ElementType` has a validator case. Added a defensive validator
834
- for `text`, `pageNumber`, `level`, `levelIndent`, and `leader` so
835
- user-authored `toc-entry` payloads (rare but possible) fail loud instead
836
- of slipping through.
837
-
838
- ### Removed
839
-
840
- - **`test/pretext-api-contract.test.ts`**
841
- Reframed in v1.3.3 as a local export-shape guard for the vendored pretext
842
- layout module, but the test was tautological: it could only fail if
843
- someone hand-edited `src/vendor/pretext/*.ts` to remove an export, in
844
- which case TypeScript would already fail the build. Deleted along with
845
- its entry in the `test:contract` npm script. Drift guards in
846
- `test/drift-guards.test.ts` cover the real risk (registry vs. switch
847
- arms going out of sync).
848
-
849
- ### Performance
850
-
851
- - **v1.3.2+ benchmark numbers captured** —
852
- Re-ran the seven core corpora against `benchmarks/benchmark-baseline.json`
853
- (recorded 2026-04-10 at v1.3.0). Results documented in
854
- `benchmarks/v1.3.2-results.md`: ~1.66x geometric-mean speedup across
855
- corpora, with the largest wins on text-heavy workloads (table-stress
856
- -52%, punctuation-heavy -51%, rtl-layout -43%). Confirms the DNS dedup,
857
- parallel raster, and word-width cache work landed in v1.3.2 was real
858
- rather than aspirational.
859
-
860
- ### Docs
861
-
862
- - **README version table extended**
863
- Added 1.1.x (vendor switch), 1.2.x (security + benchmarks), and
864
- 1.3.0–1.3.4 (perf + drift guards) rows so the version table no longer
865
- stops at 1.0.6.
866
-
867
- ### Tooling
868
-
869
- - **`scripts/run-bench-snapshot.mjs`**
870
- Small one-shot runner that prints avg/min render time per corpus across
871
- three measured runs (after a warmup). Used to capture the v1.3.5
872
- benchmark results above; useful for ad-hoc perf checks without touching
873
- the regression-guarded `test/benchmark-baseline.test.ts`.
874
-
875
- ---
876
-
877
- ## [1.3.4] 2026-05-17
878
-
879
- ### Fixed
880
-
881
- - **DNS dedup test now imports from source**
882
- `test/assets-dns-dedup.test.ts` previously imported `fetchWithTimeout` /
883
- `assertSafeUrl` from `../dist/assets.js`, which would silently pass against
884
- a stale build (false confidence). Switched to `../src/assets.js` so the
885
- test always runs against the current source tree under tsx.
886
-
887
- ### Added
888
-
889
- - **FIFO eviction boundary test for word-width cache**
890
- `test/measure-text-cache.test.ts` now asserts that when the cache is
891
- pre-filled to `WORD_WIDTH_CACHE_MAX` and a new `measureWord` call is made,
892
- `cache.size` stays at the cap, the oldest insertion (`syn0`) is evicted,
893
- and a re-accessed entry (`syn1`) survives proving FIFO semantics, not LRU.
894
-
895
- ### Changed
896
-
897
- - **Constant tunability scope documented** — `VECTOR_RASTER_CONCURRENCY` and
898
- `WORD_WIDTH_CACHE_MAX` are exported as read-only constants for observability
899
- and test introspection. Consumers wanting different values must fork;
900
- runtime tunability (env vars or options) is a future enhancement and not
901
- planned for the v1.x line.
902
-
903
- ---
904
-
905
- ## [1.3.3] 2026-05-17
906
-
907
- ### Fixed
908
-
909
- - **Parallel rasterization concurrency cap** — `loadVectorAssets` now runs at
910
- most 4 SVG/QR/barcode/chart rasterization tasks concurrently (was unbounded).
911
- Prevents file-descriptor / worker exhaustion on documents with many vector
912
- assets. New exported constant: `VECTOR_RASTER_CONCURRENCY`.
913
- - **Word-width cache memory bound** — `measureWord` now FIFO-evicts at 50,000
914
- entries to bound memory for long-running processes that reuse a single
915
- `wordWidthCache`. New exported constant: `WORD_WIDTH_CACHE_MAX`.
916
-
917
- ### Changed
918
-
919
- - **Pretext API contract test reframed** `test/pretext-api-contract.test.ts`
920
- header clarified: this is a local export-shape guard for the vendored
921
- pretext layout module, not an upstream version canary. Pretext has been
922
- vendored at `src/vendor/pretext/` since v1.1.0.
923
- - **CHANGELOG clarification on v1.3.2 parallel rasterization** — see updated
924
- v1.3.2 entry below; the speedup is real for I/O-bound fan-out, but CPU
925
- rasterization still serializes on the V8 main thread.
926
-
927
- ### Documented (not changed)
928
-
929
- - **Word-width cache scope (H1)** the cache is currently consulted on the
930
- hyphenation path (`measureTextWithHyphenation`) only. The non-hyphenation
931
- branch of `measureText` delegates directly to pretext's `layoutWithLines`
932
- to preserve CJK character-level breaking, RTL/bidi, Thai segmentation,
933
- kerning, and justify semantics that word-by-word summing would diverge
934
- from. Documents that do not configure a hyphenator will not see
935
- cross-paragraph cache reuse; this is intentional for correctness.
936
-
937
- ---
938
-
939
- ## [1.3.2] 2026-05-17
940
-
941
- ### Performance
942
-
943
- - Removed double DNS resolution in image/SVG fetch (one lookup per remote asset, not two)
944
- - Parallel SVG/QR/barcode generation+rasterization (sequential embed retained for pdf-lib safety)
945
- - Sub-note (added in v1.3.3): the parallelism is real for the I/O-bound fan-out
946
- (remote SVG fetches overlap). CPU rasterization (sharp/svg2pdfkit) still
947
- contends on the V8 main thread, so wall-clock improvement is dominated by
948
- remote-asset latency, not raster throughput.
949
- - Document-level word-width measurement cache (cross-paragraph dedup of common-word measurements)
950
- - Sub-note (added in v1.3.3): the cache is consulted on the hyphenation code
951
- path only. See v1.3.3 "Documented (not changed)" for the rationale.
952
-
953
- ---
954
-
955
- ## [1.3.1] — 2026-05-17
956
-
957
- ### Fixed
958
-
959
- - Internal test fixtures updated to set `allowedFileDirs` (resolved with `path.resolve` for Windows drive-letter compatibility) after the v1.2.2 deny-by-default flip — no library behavior change. Affected: `signatures-crypto`, `signatures-validation`, `svg`, `image-floats` test files.
960
- - `markdown-gfm` compat test updated to use an `https:` image src; `data:` URLs are blocked by the scheme guard added in v1.3.0, so the pre-existing fixture no longer round-tripped — test-only change.
961
-
962
- ---
963
-
964
- ## [1.3.0] — 2026-05-17
965
-
966
- ### ⚠️ BREAKING (retroactive note covering v1.2.2)
967
-
968
- - **`assertPathAllowed` is now deny-by-default.** Documents using `file://` image sources without an explicit `allowedFileDirs` configuration will throw `PATH_TRAVERSAL`. This was shipped in v1.2.2 as a security fix but is technically a breaking change consumers on `^1.2.0` who upgrade past v1.2.1 must either set `allowedFileDirs` or migrate away from `file://` sources. v1.3.0 is the recommended upgrade target with full semver signal.
969
-
970
- ### Fixed
971
-
972
- - **Scheme guard whitespace bypass** in `compat.ts` leading whitespace in image src (e.g. `" file:///etc/passwd"`) no longer bypasses scheme stripping.
973
- - **Extended scheme blocklist** in `compat.ts` added `vbscript:`, `blob:`, `about:` alongside existing `file://`, `data:`, `javascript:`.
974
-
975
- ### Tests
976
-
977
- - Added redirect-chain SSRF test using a local mock HTTP server.
978
- - Pinned CLI exit-code assertion to detect regressions.
979
-
980
- ---
981
-
982
- ## [1.2.2] 2026-05-17
983
-
984
- ### Security
985
-
986
- - **`assertPathAllowed` is now deny-by-default** — Previously, when `doc.allowedFileDirs` was
987
- undefined or empty, local file:// paths were silently allowed. Now the function throws
988
- `PATH_TRAVERSAL` unless `allowedFileDirs` is explicitly configured with at least one directory.
989
- This closes an unintended open-access footgun for server-side deployments.
990
-
991
- - **`compat.ts` dangerous image schemes stripped in `fromPdfmake`** `file://`, `data:`, and
992
- `javascript:` image `src` values are now silently dropped during pdfmake→pretext-pdf translation
993
- rather than forwarded verbatim. This prevents the compat shim from acting as an indirect
994
- bypass for the scheme-level SSRF guards in `assets.ts`.
995
-
996
- - **`compat.ts` `allowedFileDirs` forwarded from `PdfmakeDocument`** — The `PdfmakeDocument`
997
- interface now accepts `allowedFileDirs?: string[]`, which is forwarded into the resulting
998
- `PdfDocument`. Callers who previously passed file paths via the compat shim can now
999
- allowlist their directories explicitly.
1000
-
1001
- - **CLI validates before rendering** — `pretext-pdf` now calls `validateDocument()` before
1002
- invoking `render()`. Invalid documents produce a `VALIDATION_ERROR` message on stderr and
1003
- exit with code 1, avoiding wasted work during the render phase.
1004
-
1005
- ---
1006
-
1007
- ## [1.2.1]2026-05-16
1008
-
1009
- ### Fixed
1010
-
1011
- - **`PretextPdfError` now preserves root cause via `err.cause`** — `ASSEMBLY_FAILED` errors
1012
- thrown by `merge()` and `assemble()` now carry the original pdf-lib error as `err.cause`,
1013
- making root-cause debugging possible without losing the upstream message.
1014
-
1015
- - **`form.updateFieldAppearances()` failure is now logged** — Previously silently swallowed
1016
- (`catch { /* non-fatal */ }`). Now emits a structured warning via the document logger or
1017
- `console.warn`. Behaviour is unchanged (non-fatal); the warning aids debugging.
1018
-
1019
- - **Owner-only encryption now warns explicitly** — When `doc.encryption` is set without
1020
- `userPassword`, a `console.warn` is emitted explaining that the PDF will open without a
1021
- password. Owner-only encryption remains valid (it restricts editing/printing, not opening).
1022
-
1023
- - **`assemble([{}])` now throws `VALIDATION_ERROR`** — Regression from v1.2.0 discriminated
1024
- union changes: passing a part with neither `doc` nor `pdf` previously crashed with a
1025
- `TypeError` (no `.code` property). Now throws a proper `VALIDATION_ERROR` with a clear
1026
- message before attempting to render.
1027
-
1028
- - **`watermark: {}` now throws `VALIDATION_ERROR`** — Regression from v1.2.0: the
1029
- `WatermarkSpec` discriminated union enforced text/image presence at compile-time but the
1030
- runtime validation was missing. A watermark object with neither `text` nor `image` now
1031
- correctly throws at validate time.
1032
-
1033
- - **`svg: ''` (empty string) now throws `VALIDATION_ERROR`** — Empty SVG strings passed
1034
- the validation stage and surfaced as `SVG_LOAD_FAILED` during render. Now caught at
1035
- validate time with a clear `VALIDATION_ERROR`.
1036
-
1037
- ### Changed
1038
-
1039
- - **`PretextPdfError` constructor accepts optional `ErrorOptions`** — Third argument
1040
- `options?: ErrorOptions` (i.e. `{ cause?: unknown }`) is now accepted and passed to the
1041
- native `Error` constructor. Fully backwards-compatible all existing call sites unchanged.
1042
-
1043
- - **bidi-js missing-peer warning routes through document logger** — When a `logger` is
1044
- passed to `render()`, bidi-js peer-dependency warnings are now routed through
1045
- `logger.warn` instead of always using `console.warn`. New low-level export:
1046
- `setBidiWarnFn(fn)` (prefer the `logger` render option in application code).
1047
-
1048
- ---
1049
-
1050
- ## [1.2.0] 2026-05-16
1051
-
1052
- Post-audit hardening release. Type system tightened, concurrency-safe validation,
1053
- @internal type leaks closed, RTL/asset failures surface as structured errors,
1054
- SSRF defense upgraded to undici-pinned IP. No source-level API removals from the
1055
- package entry point see migration notes below for `@internal` types that were
1056
- already not exported from `src/index.ts`.
1057
-
1058
- ### Added
1059
-
1060
- - **Discriminated unions on four public types** (`src/types-public.ts`, audit Phase B) —
1061
- `WatermarkSpec`, `AssemblyPart`, `SvgElement`, and `ImageElement` (float variants)
1062
- now use TypeScript discriminated unions instead of flat optional structs. The
1063
- compiler now prevents invalid combinations (e.g., a watermark with both `text`
1064
- and `image`, or an SVG element with neither `svg` nor `src`) that previously
1065
- could only be caught at runtime. Existing valid usages continue to compile.
1066
-
1067
- - **`pdf-lib` type augmentation** (`src/vendor/pdf-lib-augment.d.ts`, audit Phase D) —
1068
- Documents the load-bearing `as any` casts against pdf-lib internals
1069
- (`PDFArray.push`, `PDFFont.embedder`) and removes the one genuinely-avoidable
1070
- cast in `measure.ts`.
1071
-
1072
- - **DNS rebinding defense via undici Agent IP pinning** (`src/assets.ts`, audit B6)
1073
- `assertSafeUrl` was upgraded to `resolveAndValidateUrl` which returns the
1074
- resolved IP, and `fetchWithTimeout` now uses an undici `Agent` whose
1075
- `connect.lookup` callback always returns the pre-validated IP. Closes the
1076
- TOCTOU window where DNS could rebind between validation and connect.
1077
- Extended private-range coverage: 0.0.0.0/8, 192.0.0/24, 198.18/15, IPv6
1078
- multicast, and IPv4-mapped IPv6 normalization. +14 SSRF tests (25 total).
1079
-
1080
- - **Per-call cycle-detection state** (`src/validate.ts`, audit Perf-1) — Moved
1081
- `seenInRecursion` WeakSet from module scope into `validate()` and threaded
1082
- through `withCycleGuard`. Makes validation reentrant and concurrency-safe
1083
- (no shared mutable state across parallel `validate()` calls). +1 regression
1084
- test.
1085
-
1086
- - **Structured error codes on RTL + asset failures** (`src/errors.ts`,
1087
- `src/measure-text.ts`, `src/assets.ts`, audit silent-failure pass)
1088
- - `RTL_REORDER_FAILED` surfaces when `bidi-js` is installed but throws.
1089
- Previously fell through with `isRTL:true` on logical-order text =
1090
- visually broken Arabic/Hebrew renders. Missing `bidi-js` still degrades
1091
- gracefully (warn + LTR render) since it is an optional peer dep.
1092
- - `CHART_LOAD_FAILED` embedded in warn logs from QR/barcode/chart loaders
1093
- so failures are debuggable from log scraping alone.
1094
- - `FONT_ENCODE_FAIL` replaces the prior bare `catch` in `src/fonts.ts`
1095
- that silently swallowed font subset failures (audit B2).
1096
-
1097
- ### Changed
1098
-
1099
- - **`@internal` types removed from the `types.ts` barrel** (audit H8 / type-design HIGH) —
1100
- `RichLine`, `RichFragment`, and `TocEntryElement` were tagged `@internal`
1101
- but re-exported through `src/types.ts`. They have been removed from that
1102
- barrel and canonicalized in `src/types-internal.ts`. `TocEntryElement` is
1103
- no longer a member of the public `ContentElement` union (it was always
1104
- pipeline-synthesized, never user-constructed). Internal imports updated
1105
- in `rich-text.ts`, `measure.ts`, `render-extras.ts`, `allowed-props.ts`,
1106
- `validate.ts`, `measure-blocks.ts`, `fonts.ts`. **Migration note:** these
1107
- types were never exported from `src/index.ts` (the package entry point),
1108
- so consumers using the supported import path (`import { ... } from
1109
- 'pretext-pdf'`) are unaffected. Deep imports (`'pretext-pdf/src/types'`)
1110
- are unsupported and never were stable.
1111
-
1112
- - **`api-extractor` enforcement escalated to `error`** (`api-extractor.json`)
1113
- `ae-forgotten-export` log level changed from `warning` to `error` so CI
1114
- fails on future `@internal` type leaks. `etc/pretext-pdf.api.md` baseline
1115
- regenerated.
1116
-
1117
- - **`assertUnknownProps` parameter tightened** (`src/validate.ts`)
1118
- `obj: any` `obj: unknown` with an explicit type guard at the boundary.
1119
- Removes one of the few remaining `any` exposures at a security-sensitive
1120
- validation entrypoint.
1121
-
1122
- ### Fixed
1123
-
1124
- - **Concurrent validation false positives** (`src/validate.ts`) Two
1125
- simultaneous `validateDocument(doc)` calls on the same object reference
1126
- could produce a false "cyclic reference detected" error because the
1127
- WeakSet was at module scope. Per-call WeakSet fix closes this and any
1128
- future re-entrant validator scenarios.
1129
-
1130
- - **Stale `tests-743` badge** (`README.md`) Replaced with the durable
1131
- `tests-passing` (current unit count is 319).
1132
-
1133
- - **Dead `case 'toc-entry'` branch** (`src/fonts.ts:collectTextByFont`) —
1134
- After `TocEntryElement` left the public `ContentElement` union, the
1135
- branch became unreachable.
1136
-
1137
- ### Migration notes (v1.1.x v1.2.0)
1138
-
1139
- If you only import from `'pretext-pdf'`: **no source changes needed.**
1140
-
1141
- If you do unsupported deep imports of internal types
1142
- (`'pretext-pdf/src/types'`):
1143
- - `RichLine`, `RichFragment`, `TocEntryElement` import from
1144
- `'pretext-pdf/src/types-internal'` instead, with the understanding that
1145
- these are not part of the stable public API and may change in any release.
1146
-
1147
- If you construct `WatermarkSpec` / `AssemblyPart` / `SvgElement` / `ImageElement`
1148
- literals: you may need to delete fields you weren't using anyway. TypeScript
1149
- will flag any literals that previously satisfied the loose type but violated
1150
- the actual invariants.
1151
-
1152
- ---
1153
-
1154
- ## [1.1.3] 2026-05-15
1155
-
1156
- ### Added
1157
-
1158
- - **Cycle detection + depth cap on TableElement walk** (`src/validate.ts`, Sprint 3 / M2) —
1159
- The rows/cells iteration is now wrapped in `withCycleGuard`, matching the
1160
- protection already in place for `ListItem.items`, `FloatGroup.content`, and
1161
- `RichParagraph.spans`. A self-referential row or cell shape now produces a
1162
- structured `VALIDATION_ERROR` instead of an unbounded walk.
1163
-
1164
- - **Root-level depth guard for `document.content` entries** (`src/validate.ts`, Sprint 3 / M1) —
1165
- Each top-level element call into `validateElement` now runs an explicit
1166
- `assertDepthOk(depth, prefix)` so the `MAX_VALIDATION_DEPTH = 32` cap fires
1167
- even for plugin-typed elements that do not open their own `withCycleGuard`
1168
- scope. Internal recursive walks (`list`, `float-group`, `rich-paragraph`,
1169
- `table`) continue to enforce the cap via `withCycleGuard`.
1170
-
1171
- - **Round-trip tests for the pdfmake compatibility shim** (`test/compat.test.ts`, Sprint 3 / M3) —
1172
- Four new tests covering pdfmake pretext → render integration, style
1173
- propagation, sanity rendering of native pretext docs, and large-table
1174
- preservation (5 columns × 10 rows).
1175
-
1176
- - **`## Validation` section in `README.md`** (Sprint 3 / M4) — Explicit guidance
1177
- to call `validateDocument()` before `render()` on untrusted input, with the
1178
- concrete failure modes (stack overflow on cyclic input, prototype pollution
1179
- via `__proto__`, runtime 500s on malformed shapes) the validator prevents.
1180
-
1181
- ### Fixed
1182
-
1183
- - **Type safety in validateDocument** (`src/validate.ts`) — Replaced unchecked `as PdfDocument` cast with `isValidPdfDocumentLike()` type guard. Returns proper error when input is not a plain object.
1184
-
1185
- - **Prototype pollution in mergeStyles** (`src/compat.ts`) — `Object.assign(merged, s)` allowed user-supplied pdfmake JSON to pollute the prototype chain. Replaced with `copySafeStyleProperties()` that whitelists only known safe style keys (fontSize, bold, italics, color, alignment, font).
1186
-
1187
- - **Path traversal in digital signatures** (`src/post-process.ts`) P12 certificate path bypassed the `allowedFileDirs` security check. Now validates path via `assertPathAllowed()` before reading, preventing directory traversal attacks via signature feature.
1188
-
1189
- - **Fragile errorCount regex in validateDocument** (`src/validate.ts`) Original regex could match anywhere in error message. Refined to header-only pattern (`^Strict validation failed`) to extract true error count even when >20 errors are returned (capped array but accurate count in message).
1190
-
1191
- - **Fake test coverage** (`test/validate-document.test.ts`)Removed describe block with `assert.ok(true, 'TODO')` placeholder. Replaced with documentation explaining why the non-PretextPdfError code path is manually audited.
1192
-
1193
- - **Missing LICENSE for vendored code** (`src/vendor/pretext/LICENSE`) Added MIT license file with attribution to upstream pretext library and this fork, satisfying legal compliance for vendored dependencies.
1194
-
1195
- ### Documentation
1196
-
1197
- - **`Logger` interface guidance** (`src/types-public.ts`, audit L2) Expanded
1198
- JSDoc on the `Logger` interface and the `logger?` field on `RenderOptions`
1199
- to call out that passing a no-op (`{ warn: () => {} }`) silences **every**
1200
- advisory warning — fine in tests, dangerous in production. Documents the
1201
- default (`console.warn`) and recommends pino/winston for production
1202
- routing. No code change.
1203
-
1204
- - **pdfmake `defaultStyle` / `styles` mapping** (`src/compat.ts`, audit L3) —
1205
- Added JSDoc to `PdfmakeStyle` enumerating the supported subset
1206
- (`font`, `fontSize`, `bold`, `italics`, `color`, `alignment`) and the
1207
- silently-dropped pdfmake properties (`lineHeight`, `marginX/Y`,
1208
- `decoration`, `background`, `characterSpacing`, `noWrap`, etc.) that
1209
- consumers migrating from pdfmake commonly trip over. `fromPdfmake()`
1210
- now documents how `defaultStyle.font` / `.fontSize` route to
1211
- document-level `defaultFont` / `defaultFontSize` and how all other
1212
- `defaultStyle` properties cascade through `mergeStyles()`.
1213
-
1214
- ### Changed
1215
-
1216
- - **Benchmark floor override** (`test/benchmark-baseline.test.ts`, audit L4) —
1217
- The per-corpus regression guard now honors a `PRETEXT_BENCHMARK_FLOOR_MS`
1218
- environment variable. Set it to a positive integer to raise the 5000ms
1219
- default floor on slow CI runners; set it to `skip` / `0` / `false` / `off`
1220
- to bypass the timing assertion entirely. The structural assertions (corpus
1221
- IDs match baseline, stages present) still run.
1222
-
1223
- ### Notes — Phase A / B / D history
1224
-
1225
- The cycle-detection and depth-cap machinery (`withCycleGuard`,
1226
- `MAX_VALIDATION_DEPTH`, the per-container guards on `list`, `float-group`,
1227
- `rich-paragraph`) and the discriminated-union refactor of `ContentElement`
1228
- plus the typed `pdf-lib` augmentation were landed during in-flight audit
1229
- sprints between `[1.0.x]` and `[1.1.0]` that were not individually tagged.
1230
- Sprint 3 (this release) backfills those gaps with the M1/M2 root + table
1231
- guards, plus explicit tests and documentation.
1232
-
1233
- ---
1234
-
1235
- ## [1.1.2]2026-05-08
1236
-
1237
- ### Fixed
1238
-
1239
- - **Silent font-subset failure** (`src/fonts.ts`) — Bare `catch {}` on `pdfFont.encodeText()`
1240
- silently swallowed glyph-encoding errors, producing wrong characters with no signal.
1241
- Now logs a `console.warn` so callers know which font key failed.
1242
-
1243
- - **Explicit RTL direction silently flipping to LTR** (`src/measure-text.ts`) — When
1244
- `dir:'rtl'` was set and `bidi-js` threw during reordering, the fallback incorrectly
1245
- returned `isRTL: false`, causing Arabic/Hebrew paragraphs to align and wrap as LTR.
1246
- The fallback now preserves `isRTL: true` so the layout engine honours the explicit
1247
- direction even without bidi reordering.
1248
-
1249
- - **SSRF DNS rebinding window** (`src/assets.ts`) `assertSafeUrl()` was synchronous.
1250
- An attacker with TTL=0 DNS could pass the hostname check then rebind to `169.254.x.x`
1251
- between the check and the actual `fetch()` call. The function is now async and
1252
- pre-resolves hostnames via `dns.lookup()` before the private-range check, closing
1253
- the TOCTOU window. Falls back gracefully when DNS is unavailable (fetch will also
1254
- fail in that case). All call sites updated to `await assertSafeUrl()`.
1255
-
1256
- - **Concurrent PDFDocument mutation race** (`src/pipeline.ts`) — `loadFonts` and
1257
- `loadImages` were run with `Promise.all()` over the same `PDFDocument` instance.
1258
- Both mutate the cross-reference table, causing intermittent xref corruption under
1259
- load. Now sequenced: `loadFonts` completes before `loadImages` begins.
1260
-
1261
- - **Test suite cascade: 692 tests silently dropped on benchmark failure** (`package.json`,
1262
- `scripts/test-all.mjs`) — The `&&`-chained `npm test` command aborted all downstream
1263
- stages when `test:contract` failed. Replaced with a Node.js runner that executes all
1264
- 4 stages and collects failures. Benchmark is now in a separate `test:benchmark` script
1265
- (not in `test:contract`) with `FLOOR_MS` raised to 5s to absorb dev-hardware variance.
1266
-
1267
- ---
1268
-
1269
- ## [1.1.1] 2026-05-08
1270
-
1271
- ### Fixed
1272
-
1273
- - **`validateDocument` fallback parser: path extraction** — `parseValidationErrorsStructured`
1274
- now correctly falls back to `path: "document"` for single-throw errors whose message
1275
- contains a sentence (e.g. `"margins.left must be a non-negative finite number. Got: -1"`).
1276
- Previously the heuristic accepted any text-before-colon that started with a letter,
1277
- producing a corrupted path like `"margins.left must be a non-negative finite number. Got"`.
1278
- Fix: reject candidates that contain `". "` (period + space), which only appears in
1279
- prose sentences, never in path expressions like `content[0] (paragraph) spans[0].href`.
1280
-
1281
- - **README `runtime%20deps` badge** — Updated from `8` to `7` to reflect the removal
1282
- of `@chenglou/pretext` from `dependencies` in v1.1.0.
1283
-
1284
- - **`SECURITY.md` personal email removed** — Replaced `akashchikara1998@gmail.com`
1285
- with the GitHub private vulnerability reporting URL.
1286
-
1287
- ### Changed
1288
-
1289
- - **CI matrix: Node 18.x removed** Node 18 reached End of Life in April 2025.
1290
- The CI matrix now targets Node 20.x and 22.x only. The `engines.node` field in
1291
- `package.json` is updated to `>=20.0.0`. The Node 18 matrix slot was causing
1292
- flaky benchmark failures (EOL runners are slower) that killed the `&&` test chain
1293
- and caused the badge verifier to see a truncated test count.
1294
-
1295
- ---
1296
-
1297
- ## [1.1.0]2026-05-07
1298
-
1299
- Vendor `@chenglou/pretext` source directly into the package, eliminating the
1300
- GitHub URL dependency and all associated install risks (mutable tags, npm audit
1301
- gaps, network-only install, no SRI).
1302
-
1303
- ### Changed
1304
-
1305
- - **`@chenglou/pretext` is now vendored** The upstream text-layout engine
1306
- (`src/vendor/pretext/`) is compiled as part of pretext-pdf itself. Consumers
1307
- no longer need to install `@chenglou/pretext`; the GitHub URL dependency has
1308
- been removed from `package.json`. The vendored snapshot is pinned to
1309
- `v0.0.6-patched.2` (commit `658edfec`) with 9 upstream PRs cherry-picked on
1310
- top of the `v0.0.6` release. See `UPSTREAM.md` for the full patch inventory
1311
- and upgrade procedure.
1312
-
1313
- ### Added
1314
-
1315
- - **`UPSTREAM.md`** — Authoritative attribution and upgrade guide for the
1316
- vendored `@chenglou/pretext` source. Documents provenance, the 9 cherry-picked
1317
- upstream PRs (#3, #29, #105, #119, #132, #138, #140, #161, #165), which
1318
- commits are excluded from vendoring (fork infra), and the procedure for
1319
- updating when upstream publishes a new release.
1320
-
1321
- ### Removed
1322
-
1323
- - **`@chenglou/pretext` dependency** Removed from `dependencies`. The library
1324
- source is now bundled inside the package at `dist/vendor/pretext/`. No runtime
1325
- behavior change; the same patched code is used.
1326
-
1327
- ---
1328
-
1329
- ## [1.0.9]2026-05-06
1330
-
1331
- Test coverage Phase 2: filling blind spots in the CLI, the pdfmake compat shim, and
1332
- the performance regression guard. Adds c8 coverage tooling for measurability.
1333
-
1334
- ### Added
1335
-
1336
- - **`test/cli.test.ts`** (+13 tests) — End-to-end coverage for the `pretext-pdf` CLI binary,
1337
- spawning `dist/cli.js` as a subprocess. Covers argument parsing (`--version`, `--help`,
1338
- `-i/-o/--markdown/--code-font`, positional fallback, unknown flags), JSON and Markdown
1339
- input modes, stdin/stdout piping, and exit codes 0/1/2.
1340
-
1341
- - **`test/compat.test.ts`** (+34 tests) Coverage for the pdfmake pretext-pdf
1342
- translation shim (`fromPdfmake`). Covers page setup (pageSize string and object,
1343
- pageMargins scalar/2-tuple/4-tuple, orientation), styles (defaultStyle, named styles,
1344
- headingMap override), all content node types (string, `text`, `ul`/`ol` with nesting,
1345
- `table` with header rows, `image`, `qr`, `pageBreak`, `stack`), header/footer string
1346
- forms, integration render, and unsupported nodes (`columns`, `canvas`).
1347
-
1348
- - **c8 coverage tooling** `npm run coverage` (text + lcov reporters) and
1349
- `npm run coverage:check` (75/65/75 thresholds, non-blocking in CI initially).
1350
- Configuration in `.c8rc.json` excludes type-only files and CLI from instrumentation.
1351
- Coverage step added to CI as `continue-on-error: true` while baseline thresholds
1352
- are calibrated.
1353
-
1354
- ### Fixed
1355
-
1356
- - **`test/benchmark-baseline.test.ts`: regression guard now actually guards**
1357
- Replaced the prior "TODO: enable when baseline is calibrated" stub (which collected
1358
- timings but asserted nothing) with a real 3x-baseline-with-500ms-floor budget per
1359
- corpus. Missing corpora in the baseline JSON now `assert.fail()` instead of silently
1360
- defaulting to a zero budget that would mask any regression.
1361
-
1362
- - **CONTRIBUTING.md: removed stale "(676 tests)" annotation** — Test count drift bait;
1363
- the README badge already auto-verifies via `verify:badges`.
1364
-
1365
- ### Changed
1366
-
1367
- - **Test runner now builds first** — Added `pretest:unit: npm run build` so contributors
1368
- running `npm run test:unit` always get a fresh dist; the new CLI tests spawn the
1369
- compiled binary and would otherwise fail with a confusing module-not-found error.
1370
-
1371
- ---
1372
-
1373
- ## [1.0.8] 2026-05-06
1374
-
1375
- Public API contract integrity: the `RenderOptions.logger` option now actually does what
1376
- its JSDoc has always promised, and `@napi-rs/canvas` no longer auto-installs.
1377
-
1378
- ### Fixed
1379
-
1380
- - **`RenderOptions.logger` now routes warnings from asset loading and rendering** —
1381
- Previously only validation warnings respected the `logger` option. Now all advisory
1382
- warnings from `loadImages` (image load, image embed, QR/barcode/chart skipped, plugin
1383
- loadAsset failed, watermark image skipped — 7 call sites) and `renderDocument` (form
1384
- field render failure) flow through `logger.warn` when one is provided. Bidi-js fallback
1385
- warnings from RTL reordering remain on `console.warn`; the JSDoc on `RenderOptions.logger`
1386
- has been updated to document the actual scope honestly.
1387
-
1388
- - **Missing `[pretext-pdf]` log prefix on bidi-js error path** — One `console.warn` in
1389
- `measure-text.ts` was logging without the canonical `[pretext-pdf]` prefix, making it
1390
- hard to identify the library as the source in consumer logs. Now consistent.
1391
-
1392
- ### Changed
1393
-
1394
- - **`@napi-rs/canvas` removed from `optionalDependencies`** Was double-listed in both
1395
- `peerDependencies` (with `optional: true`) and `optionalDependencies`. The latter caused
1396
- npm to attempt installing the native canvas binary on every install, including in
1397
- edge/serverless environments where the platform may not be supported and the dep is
1398
- not needed. Now only listed under `peerDependencies` install it explicitly when you
1399
- need SVG/QR/barcode/chart rasterization in Node.
1400
-
1401
- ### Documentation
1402
-
1403
- - **README — security callout for `allowedFileDirs`** — Added a prominent callout in the
1404
- Quick Start section. The default behavior allows `image.src` and `svg.src` to read any
1405
- absolute file path, which is a path-traversal vector when document JSON originates from
1406
- user input or an LLM. The callout now appears immediately after the first `render()`
1407
- example.
1408
-
1409
- ---
1410
-
1411
- ## [1.0.7] 2026-05-05
1412
-
1413
- Picks up pretext fork v0.0.6-patched.2: 8 additional upstream PRs (11 total).
1414
-
1415
- ### Fixed
1416
-
1417
- - **German low opening quote `„` no longer breaks at line-start on hyphenation path** —
1418
- `KINSOKU_START_FORBIDDEN` in `src/measure-text.ts` now includes U+201E (`„`), matching
1419
- pretext PR #165 which fixed the non-hyphenation path. Previously `„` could appear
1420
- at the start of a wrapped line when hyphenation was active.
1421
-
1422
- - **Currency symbols stay glued to adjacent numbers** — Upstream PR #105 (cherry-picked in
1423
- `v0.0.6-patched.2`) prevents `$`, `€`, `£`, `₹` etc. from line-breaking away from
1424
- the number they annotate.
1425
-
1426
- - **Trailing collapsible-space reconstruction fixed** — Upstream PR #29 fix (extended in
1427
- v0.0.6-patched.2): a word followed by a space that exactly fills `maxWidth` no longer
1428
- drops the space from line boundary cursors, preventing Arabic/mixed-script text from
1429
- losing inter-word spaces during reconstruction.
1430
-
1431
- ### Changed
1432
-
1433
- - **`@chenglou/pretext` dependency** Bumped from `v0.0.6-patched` to `v0.0.6-patched.2`
1434
- (GitHub fork, 11 upstream PRs total). Adds: CJK overflow prevention (PR #132),
1435
- fit-advance cache fix (PR #161), rich inline stats unification (PR #138),
1436
- chunk layout side table O(1) lookup (PR #140), bidi surrogate handling (PR #3),
1437
- skip no-op merge passes (PR #119), currency stickiness (PR #105),
1438
- German quote fix (PR #165), and trailing-space reconstruction (PR #29).
1439
-
1440
- ---
1441
-
1442
- ## [1.0.6]2026-05-04
1443
-
1444
- Audit bug fixes: validator correctness, internal export hygiene, schema gaps, README accuracy.
1445
-
1446
- ### Fixed
1447
-
1448
- - **lineHeight upper-bound cap removed** — `validate()` no longer rejects `lineHeight > 20`. The
1449
- field is in points (pt), not a multiplier; 36pt is valid for a large heading. The `> 20` cap in
1450
- `paragraph`, `heading`, and `defaultParagraphStyle` validators has been removed. The lower-bound
1451
- check (lineHeight >= fontSize) is preserved.
1452
-
1453
- - **form-field error messages use `${prefix}` format** Error messages from the `form-field` case
1454
- now follow the `content[N] (form-field): ...` format used by all other element types, instead of
1455
- the old `[N] form-field.` prefix.
1456
-
1457
- - **`assertUnknownProps` hint punctuation fixed** — The "unknown property" message previously
1458
- produced `unknown property. did you mean "color"` (period before hint). Fixed to
1459
- `unknown property; did you mean "color"` no period, semicolon separator.
1460
-
1461
- - **British "colour" "color" in JSDoc** Two `QrCodeElement` field comments
1462
- (`foreground`, `background`) and the `ValidationError.path` JSDoc example corrected.
1463
-
1464
- - **`TocEntryElement`, `RichLine`, `RichFragment` removed from public exports** — These types are
1465
- marked `@internal` in `types-public.ts` and should not be part of the npm API surface. Removed
1466
- from `src/index.ts`.
1467
-
1468
- - **Signature error includes original cause** `SIGNATURE_FAILED` now preserves the underlying
1469
- error message: `PDF signing failed: <original message>` instead of a static string.
1470
-
1471
- - **Header-only table now valid** `validate()` previously rejected tables where all rows are
1472
- headers (`headerRowCount === rows.length`). Changed `>=` to `>`: tables where every row is a
1473
- header are valid (useful for column-label-only tables).
1474
-
1475
- - **Dead sub-condition removed in `float-group` floatWidth guard** `fg.floatWidth <= 0` was a
1476
- dead branch (any value `<= 0` is already `< 30`). Removed to clarify intent.
1477
-
1478
- - **`warningCount` JSDoc updated** — Documents that the validator currently only emits errors, so
1479
- `warningCount` is always 0 (reserved for future use).
1480
-
1481
- - **`validateDocument` no longer re-throws unexpected errors** Non-`PretextPdfError` exceptions
1482
- (e.g. circular JSON, unexpected runtime errors) are now caught and returned as a structured
1483
- `ValidationResult` instead of propagating. `validateDocument` now always returns, never throws.
1484
-
1485
- ### Changed (Schema additions `pretext-pdf/schema`)
1486
-
1487
- - `qrCodeSchema`: added `margin` field.
1488
- - `imageSchema`: added `floatFontSize`, `floatFontFamily`, `floatColor` fields.
1489
- - `codeSchema`: added `dir` and `highlightTheme` fields.
1490
- - `tableSchema`: added `dir`, `headerRows`, and cell-level `dir`, `fontFamily`, `fontSize`,
1491
- `tabularNumbers` fields.
1492
-
1493
- ### Docs
1494
-
1495
- - README: `highlight.js` added to optional peer dependencies table.
1496
- - README: `validate_document` added to MCP server tool list.
1497
-
1498
- ---
1499
-
1500
- ## [1.0.5] — 2026-05-04
1501
-
1502
- Schema coverage completion, `ValidationResult.warningCount`, and README API docs.
1503
-
1504
- ### Added
1505
-
1506
- - **`ValidationResult.warningCount`** `validateDocument()` now returns `warningCount` alongside
1507
- `errorCount`. Computed by filtering `errors[]` by `severity === 'warning'`. MCP consumers no
1508
- longer need to derive it client-side.
1509
-
1510
- - **JSON Schema: remaining field coverage** — `src/schema.ts` now covers all previously missing
1511
- fields across 9 element types:
1512
- - `inlineSpanSchema`: `dir`
1513
- - `paragraphSchema`: `columns`, `columnGap`, `tabularNumbers`, `hyphenate`
1514
- - `headingSchema`: `tabularNumbers`, `hyphenate`
1515
- - `blockquoteSchema`: `lineHeight`, `padding`, `paddingH`, `paddingV`, `underline`, `strikethrough`
1516
- - `calloutSchema`: `titleColor`, `fontWeight`, `lineHeight`, `padding`, `paddingH`, `paddingV`
1517
- - `listSchema`: `lineHeight`, `markerWidth`, `itemSpaceAfter`, `nestedNumberingStyle`; nested
1518
- items now carry `dir` and have a typed inner schema
1519
- - `tocSchema`: `titleFontSize`, `levelIndent`, `leader`, `entrySpacing`
1520
- - `formFieldSchema`: `borderColor`, `backgroundColor`, `keepTogether`, `defaultSelected`
1521
- - `richParagraphSchema`: `columns`, `columnGap`, `tabularNumbers`
1522
-
1523
- - **README: `validateDocument` and `pretext-pdf/schema` documented** — both entry points now have
1524
- `### API reference` sections with code examples.
1525
-
1526
- ---
1527
-
1528
- ## [1.0.4] — 2026-05-04
1529
-
1530
- Schema export hardening: post-release audit fixes addressing coverage gaps and a
1531
- malformed dialect URI.
1532
-
1533
- ### Fixed
1534
-
1535
- - **`pretext-pdf/schema`: `$schema` dialect URI corrected** — was
1536
- `https://json-schema.org/draft/2020-12` (not a registered URI), now
1537
- `https://json-schema.org/draft/2020-12/schema`. Strict JSON Schema validators
1538
- (AJV, Smithery, VS Code) will now correctly identify the dialect.
1539
- - **`pretext-pdf/schema`: `hr` element spacing fields** — `spaceAbove` and
1540
- `spaceBelow` (the primary documented fields, default 12) were missing.
1541
- `spaceBefore` and `spaceAfter` are now correctly marked as aliases.
1542
- - **`pretext-pdf/schema`: `float-group` and `chart` element types** — both
1543
- first-class public element types were missing from the `content.items.anyOf`
1544
- list. Schema-driven tooling will now know they exist.
1545
-
1546
- ### Added (schema coverage)
1547
-
1548
- - `pdfDocumentSchema.sections`page-range header/footer overrides
1549
- - `headingSchema.annotation` — annotation field (was already on paragraph)
1550
- - `tableSchema.cellPaddingH` / `cellPaddingV` primary table density controls
1551
- - `imageSchema.floatWidth` / `floatGap` / `floatSpans` — column-layout controls
1552
- for floated images
1553
-
1554
- ---
1555
-
1556
- ## [1.0.3] 2026-05-03
1557
-
1558
- Enhancements: JSON Schema export, simplified marked peer dep range, and internal API polish.
1559
-
1560
- ### Added
1561
-
1562
- - **`pretext-pdf/schema` entry point** — exports `pdfDocumentSchema`, a machine-readable JSON Schema
1563
- object describing the full `PdfDocument` type. Covers all 22 element types and 18 top-level
1564
- document properties. Intended for editor tooling, MCP clients, and Smithery UI form generation.
1565
-
1566
- ```typescript
1567
- import { pdfDocumentSchema } from 'pretext-pdf/schema'
1568
- ```
1569
-
1570
- ### Changed
1571
-
1572
- - **`marked` peer dependency simplified** — `^9.0.0 || ^10.0.0 || ... || ^18.0.0` condensed to
1573
- `>=9.0.0`. Semantically identical, cleaner npm output.
1574
-
1575
- - **`validateDocument` logger option** — `options.logger` now passed to the underlying `validate()`
1576
- call via conditional spread, respecting `exactOptionalPropertyTypes: true` constraints.
1577
-
1578
- ### Fixed
1579
-
1580
- - **`fonts.ts` unsafe cast removed** — `(spec as { style?: string }).style` replaced with direct
1581
- property access on the widened parameter type.
1582
-
1583
- ---
1584
-
1585
- ## [1.0.2] 2026-05-03
1586
-
1587
- ### Added
1588
-
1589
- - `validateDocument(doc, options?)` non-throwing validation API that returns a structured `ValidationResult` with typed `ValidationError[]` instead of throwing. Each error includes `path`, `message`, `code`, `severity`, and `suggestion` fields.
1590
- - `ValidationError` and `ValidationResult` exported from the public API surface.
1591
- - `Logger` interface and `logger?: Logger` in `RenderOptions` — route diagnostic warnings through a custom logger instead of `console.warn`.
1592
- - Inter italic font support (Inter-400-italic, Inter-700-italic) via bundled `@fontsource/inter` — italic markdown and `fontStyle: 'italic'` now work without manual font setup.
1593
-
1594
- ---
1595
-
1596
- ## [1.0.1]2026-05-02
1597
-
1598
- Patch: strict mode correctness fixes. No API changes.
1599
-
1600
- ### Fixed
1601
-
1602
- - **`levenshteinDist` early-exit bug** — per-cell `if (curr[j]! > 2) return 999` inside
1603
- the inner DP loop fired on intermediate cells, causing d=1 pairs like `hrefs→href` and
1604
- `spaceafter→spaceAfter` to incorrectly return 999 instead of 1. Fix: removed the per-cell
1605
- guard; final check only (`prev[n]! > 2 ? 999 : prev[n]!`).
1606
- - **Seven path-prefix annotations** strict-mode error paths had `(type)` suffixes
1607
- (e.g. `doc(table).rows[0]`) that no other validator used and that tests didn't expect.
1608
- All seven removed so paths are plain dot-notation.
1609
- - **`encryption` block not strict-checked** — unknown props inside `doc.encryption`
1610
- were silently accepted in strict mode. Now validated against `ALLOWED_PROPS_SUB['encryption']`.
1611
- - **Root path was `'document'` not `'doc'`** top-level `assertUnknownProps` was called
1612
- with `'document'` as the path prefix, producing paths like `document.content[0]` instead
1613
- of `doc.content[0]`. Corrected to `'doc'`.
1614
- - **Suggestion format mismatched** — `Did you mean 'x'?` → `did you mean "x"` (lowercase,
1615
- double-quotes) to match the format tests asserted.
1616
- - **`formatErrors` missing header** — multi-error output now begins with
1617
- `Strict validation failed (N issues):\n` so callers can detect strict vs. regular errors.
1618
-
1619
- ### Tests
1620
-
1621
- - Added `test/validate-strict.test.ts` (35 tests) to `test:unit` script — these tests were
1622
- written but not wired into CI in v1.0.0.
1623
-
1624
- ---
1625
-
1626
- ## [1.0.0] — 2026-05-02
1627
-
1628
- First stable release. Completes the plugin extension API, closes all v1.0 gate requirements,
1629
- and ships a fully verified public surface with zero breaking changes from 0.9.x.
1630
-
1631
- ### Added
1632
-
1633
- - **Plugin extension API** Register custom element types via `RenderOptions.plugins`.
1634
- Each `PluginDefinition` participates in all four pipeline stages: `validate`, `loadAsset`,
1635
- `measure`, and `render`. Plugins are fully typed and tree-shaken from documents
1636
- that don't use them. See README § Custom element types (plugins) and
1637
- `examples/plugin-custom-element.ts` for a runnable example.
1638
- - **`PluginDefinition`, `PluginMeasureContext`, `PluginMeasureResult`, `PluginRenderContext`**
1639
- exported from `pretext-pdf` public surface (previously internal).
1640
- - **`PdfBuilder` and `PdfBuilderOptions`** exported from `pretext-pdf` (enables type-safe
1641
- builder construction in downstream code without re-declaring the interface).
1642
- - **`TocEntryElement`** exported from `pretext-pdf` public surface (was in the `ContentElement`
1643
- union but not individually importable).
1644
- - **`plugins` option on `createPdf()`** `PdfBuilderOptions.plugins` threads plugins through
1645
- the builder's `build()` call automatically.
1646
- - **`Intl.Segmenter` pre-flight guard** in `render()` throws `RENDER_FAILED` with a clear
1647
- message on Node.js < 16 or runtimes without full-ICU data, instead of silently producing
1648
- incorrect line breaks.
1649
- - **`PluginRenderContext.pageWidth/pageHeight/margins`** — render hooks now receive full page
1650
- geometry for layout calculations (page-relative positioning, bleed boxes, etc.).
1651
- - **`render` context Y-coordinate docs** expanded JSDoc with multi-line text example showing
1652
- how to position text baselines relative to `context.y`.
1653
- - **Benchmark corpora manifest** and **smoke staging** tests wired into `npm test`
1654
- (previously orphaned).
1655
- - **`test/table-determinism.test.ts`** asserts that table pagination produces identical
1656
- layout traces across repeated invocations of `prepareLayoutState`.
1657
- - **`test/validate-strict.test.ts`** (35 tests)comprehensive contract for `strict: true`
1658
- validation covering all element types, nested structures, Levenshtein suggestions, error
1659
- message format, doc-level and sub-structure prop checks. Total test count: 676.
1660
- - `examples/plugin-custom-element.ts` runnable plugin example (`npm run example:plugin`).
1661
-
1662
- ### Fixed
1663
-
1664
- - **`SIGNATURE_CERT_AND_ENCRYPTION` error code** — was declared in the `ErrorCode` union
1665
- but never thrown; validate.ts now uses it correctly when a document specifies both
1666
- signatures and encryption (previously threw a generic `VALIDATION_ERROR`).
1667
- - **Build break under `exactOptionalPropertyTypes: true`** — `PdfBuilder.build()` no longer
1668
- passes `{ plugins: undefined }` to `runPipeline` when no plugins are configured.
1669
- - **Plugin `validate` hook empty-string normalization** `plugin.validate()` returning `''`
1670
- now correctly accepts the element (was previously treated as a rejection message).
1671
- - **`toc` element reaching render default arm** — `render.ts` now has an explicit
1672
- `case 'toc': return` guard before the default arm; TOC elements are pre-processed
1673
- during pagination and should never reach the renderer.
1674
- - **`RichLine` and `RichFragment`** demoted from `@public` to `@internal`; these are
1675
- implementation details of the rich-text pipeline, not intended for external use.
1676
- - **Sentinel value documentation** `MeasuredBlock` comment now explicitly states that
1677
- `lines: []`, `fontSize: 0`, `lineHeight: 0`, `fontKey: ''` applies to spacers, tables,
1678
- images, hr, *and plugin blocks* — not a bug but a documented convention.
1679
-
1680
- ### Internal
1681
-
1682
- - `src/plugin-registry.ts` (new): Pure orchestration helpers for the four plugin injection
1683
- points (`findPlugin`, `runPluginValidate`, `runPluginLoadAsset`, `runPluginMeasure`,
1684
- `runPluginRender`).
1685
- - `src/plugin-types.ts` (new): `PluginDefinition` interface and context/result types.
1686
- - `src/layout-state.ts`: `prepareLayoutState` now accepts `options?: RenderOptions` and
1687
- threads plugins to `stageValidate`, `stageLoadAssets`, and `stageMeasure`.
1688
- - `docs/V1.0-RUNBOOK.md`: Full release runbook with first-principles audit, anti-hallucination
1689
- protocol, verified-facts table, and phase-by-phase plan.
1690
-
1691
- ---
1692
-
1693
- ## [0.9.4] 2026-05-02
1694
-
1695
- > **Note:** This release was never published to npm as a standalone tag. All changes listed
1696
- > here shipped as part of [1.0.0] on the same date.
1697
-
1698
- Architecture hardening + API surface snapshot. No public API changes; internal
1699
- restructuring to eliminate circular dependencies and add drift guards before v1.0 freeze.
1700
-
1701
- ### Added
1702
-
1703
- - **API surface snapshot** (`etc/pretext-pdf.api.md`) checked into source control as
1704
- the v1.0 baseline. The `api:check` CI step will fail on unintentional public-API drift.
1705
- - **`src/layout-state.ts`** `prepareLayoutState()` and `summarizeLayoutState()` extracted
1706
- from the pipeline for testability; `layout-contract` and `hard-text-contract` tests
1707
- wired into `test:unit`.
1708
- - **`src/benchmarks/corpora.ts`**benchmark corpus manifest (`getBenchmarkCorpora()`)
1709
- restored from git history; `benchmark-baseline.test.ts` wired into `test:contract`.
1710
- - **Drift guards** (`test/drift-guards.test.ts`) — asserts that `ELEMENT_TYPES`,
1711
- `ALLOWED_PROPS`, `validate.ts` cases, and `render.ts` cases all agree at test time.
1712
- Catches any future element-type addition that isn't plumbed through all four places.
1713
- - **`render.ts` default arm** unknown element types now throw immediately instead of
1714
- silently producing a blank block.
1715
-
1716
- ### Refactored
1717
-
1718
- - **Circular dependency broken**: `src/post-process.ts` extracted so `builder.ts` and
1719
- `index.ts` no longer form a cycle through each other.
1720
- - **`ELEMENT_TYPES` extracted** to `src/element-types.ts` as single source of truth;
1721
- re-exported from `index.ts`, imported by `validate.ts` — eliminates the previous
1722
- per-file string-literal duplication.
1723
-
1724
- ### Fixed
1725
-
1726
- - `post-process.ts`: drop raw signing library error message from `SIGNATURE_FAILED`
1727
- to avoid leaking certificate or passphrase details in error output.
1728
- - `layout-state.ts`: polyfill install wrapped in try/catch; throws `CANVAS_UNAVAILABLE`
1729
- on failure instead of an untyped exception.
1730
-
1731
- ---
1732
-
1733
- ## [0.9.3] 2026-04-23
1734
-
1735
- Strict validation release. Opt-in property validation to catch unknown properties on elements and sub-structures via typo detection and precise JSONPath error reporting.
1736
-
1737
- ### Added
1738
-
1739
- - **Strict validation mode**: Pass `{ strict: true }` to `render(doc, options)` to reject unknown properties. Non-strict mode (default) remains permissive for backwards compatibility.
1740
- - **`render()` options parameter**: Updated signature to `render(doc: PdfDocument, options?: RenderOptions)` where `RenderOptions = { strict?: boolean }`.
1741
- - **`validate()` public export**: `validate()` is now exported from `pretext-pdf` for standalone validation and testing.
1742
- - **Validation error details**:
1743
- - Unknown properties reported with Levenshtein edit-distance suggestions (distance ≤2) for typo correction.
1744
- - Errors include JSONPath-like paths (`content[3].table.rows[0].cells[1].align`) for precise location reporting.
1745
- - Error accumulation: all violations collected before throwing a single VALIDATION_ERROR with formatted multi-line message.
1746
- - First 20 errors shown; overflow indicator present.
1747
- - **Compile-time drift guards**: `src/allowed-props.ts` uses `Exact<T, Keys>` TypeScript type assertions to catch property definition drift at type-check time. If element types change, `tsc --noEmit` will error if allowed-props lists don't match.
1748
- - **Property allowlists**:
1749
- - `ALLOWED_PROPS`: 22 element types (paragraph, heading, table, image, code, list, etc.)
1750
- - `ALLOWED_PROPS_SUB`: 8 sub-structures (document, metadata, table-row, table-cell, list-item, inline-span, column-def, annotation)
1751
-
1752
- ### Internal
1753
-
1754
- - `src/allowed-props.ts` (new): Central configuration for allowed properties with compile-time assertions.
1755
- - `src/validate.ts` (enhanced): Added `levenshteinDist()`, `closestMatch()`, `assertUnknownProps()`, and `formatErrors()` helpers; threading strict flag through `validateElement()` for nested structure validation (tables, lists, rich-paragraphs, float-groups, annotations).
1756
-
1757
- ---
1758
-
1759
- ## [0.9.2] 2026-04-22
1760
-
1761
- Maintenance release. Engine refresh + repo-hygiene automation. No runtime behavior changes beyond the `@chenglou/pretext` bump.
1762
-
1763
- ### Changed
1764
-
1765
- - **Bumped `@chenglou/pretext` to 0.0.6** (from 0.0.5). Brings two upstream improvements: (a) CJK text followed by opening-bracket annotations now wraps like browsers instead of leaving the opening bracket on the previous line (upstream PR #148), (b) native numeric `letterSpacing` support on `prepare()` and `prepareWithSegments()` (upstream PRs #108/#156). Our manual letterSpacing compensation in `src/measure-blocks.ts` and `src/rich-text.ts` continues to work unchanged — delegating to pretext's native path is tracked as Tier 1 follow-up in `docs/ROADMAP.md`. All 624 tests green, all 5 visual regression baselines green.
1766
-
1767
- ### Fixed
1768
-
1769
- - **README badges matched to reality**: `runtime-deps-7` → `runtime-deps-8` (there are 8 direct `dependencies`, not 7), `tests-600+` `tests-624` (the full `npm test` chain runs 624 tests across 5 subsuites). Drift guarded by a new CI step; see below.
1770
-
1771
- ### Added
1772
-
1773
- - `scripts/verify-badges.js` + CI step — compares README shields.io badge values against `package.json` dep count and `npm test` total. Fails CI on drift. Fast path via `SKIP_TEST_RUN=1` for pre-commit use.
1774
- - `release` job in `ci.yml` on `v*` tag push, auto-extracts the matching `## [X.Y.Z]` section from this file and creates the GitHub release (requires publish to succeed first). Closes the "tag exists but no release page" gap that affected v0.9.1. (Note: originally shipped as `.github/workflows/release-on-tag.yml`; merged into `ci.yml` for dependency ordering in Tier 0.5.)
1775
- - `renovate.json` watches dependencies, auto-merges devDependency bumps that pass CI, opens PRs (without auto-merge) for runtime, peer, and `@chenglou/pretext` engine bumps. Closes the gap that left us one release behind upstream.
1776
-
1777
- ### Removed
1778
-
1779
- - `test/smoke-staging.test.ts` — exercised a non-existent `{ type: 'paragraph', footnote: {...} }` shape that the permissive validator silently accepted. False coverage. A strict validator rollout (rejecting unknown element properties) is the root fix and is tracked as a Tier 1 item in the rewritten `docs/ROADMAP.md`.
1780
- - `src/brain/` — inert auto-logger artifact (34 blank-body entries, no active writer). Never published to npm.
1781
-
1782
- ### Docs
1783
-
1784
- - `docs/ROADMAP.md` — complete rewrite as a living document (Now / Next / Under consideration / Shipped / History + Update discipline). The previous "master remediation plan" with phase-numbered sections was dropped: phases 0–5 all shipped by v0.9.1, and the document had rotted to the point of contradicting `package.json` on dependency pinning and `CHANGELOG.md` on what was live. History section preserves the prior plan's origin date and scope for reference.
1785
-
1786
- ---
1787
-
1788
- ## [0.9.1] 2026-04-21
1789
-
1790
- Bug-fix + hardening release. Ships the callout + rich-text rendering fixes from PR #2 together with PR #3's producer-validator contract around measured blocks.
1791
-
1792
- ### Fixed
1793
-
1794
- - **Rich-paragraph: leading-space tokens stripped after hard break** ([src/rich-text.ts](src/rich-text.ts)). A pre-overflow guard (`isLeadingSpace: currentX === 0 && token.text.trim() === ''`) fired whenever `currentX` was zero — both at block start *and* after a `\n` hard break reset the cursor. Continuation spans beginning with whitespace (e.g. `' · text'`) had their first token silently dropped, causing separator glyphs and indented text to appear mis-positioned. Guard removed; the overflow-wrap skip path that correctly skips trailing spaces after soft wraps is unaffected.
1795
- - **Callout: `spaceAfter` double-applied by paginator** ([src/measure-blocks.ts](src/measure-blocks.ts)). `callout` block measurement included `el.spaceAfter ?? 12` inside `totalHeight` *and* returned the same value as `block.spaceAfter`. `paginate.ts` added `block.spaceAfter` on top of `block.height`, counting it twice and pushing callout content ~12 pt below its intended position. Fixed by removing `spaceAfter` from the `totalHeight` formula; the value is still returned in `block.spaceAfter` for the paginator.
1796
- - **Callout with title: background rect clips title row when split across pages** ([src/paginate.ts](src/paginate.ts)). `splitBlock` did not subtract `calloutData.titleHeight` from `availableForLines` for the first chunk, allowing `floor((titleH + lh) / lh)` extra lines to be placed, leaving no room for the title row. `getCurrentY` also omitted `titleHeight` from `blockBottom`, producing incorrect Y tracking after a split callout. Both fixed: `titleH` is now subtracted from available space on the first chunk only, and added to `blockBottom` when computing the cursor position after the first chunk renders.
1797
-
1798
- ### Added / hardened
1799
-
1800
- - **Producer-validator contract for measured blocks** ([src/paginate.ts](src/paginate.ts)). `validateMeasuredBlocks()` runs at `paginate()` entry in O(n) and throws `PretextPdfError('PAGINATION_FAILED')` if a callout `MeasuredBlock` is missing `calloutData` or any of `titleHeight` / `paddingV` / `paddingH` is non-finite — same for blockquote padding/border fields. Surfaces producer bugs directly instead of as downstream NaN arithmetic or `PAGE_LIMIT_EXCEEDED`.
1801
- - **Narrowed internal types** `MeasuredCalloutBlock` / `MeasuredBlockquoteBlock` (intersection types in [src/types.ts](src/types.ts)) consumed by `calloutTitleHeight` + `verticalPadding` helpers in `paginate.ts`. No defensive runtime checks downstream.
1802
- - **Extracted `CalloutData` interface** from the previously-inline shape on `MeasuredBlock.calloutData`. Measurer constructs it as a typed literal, so TypeScript enforces the full contract at the producer site.
1803
- - **Zero-width non-whitespace tokens preserved**: the rich-text post-soft-wrap guard only skips tokens where `text.trim() === ''`. ZWJ (U+200D), combining marks, and other zero-width non-whitespace characters pass through so emoji / CJK shaping stays intact pinned by a regression test.
1804
- - **Extracted `LINK_COLOR_DEFAULT`** constant in `src/rich-text.ts`.
1805
-
1806
- ### Tests
1807
-
1808
- - `test/rich-text.test.ts` 20 → 23 (+3): block-start leading whitespace preserved; leading whitespace after hard break preserved; ZWJ preservation.
1809
- - `test/phase-8d-callout.test.ts` 12 19 (+7): callout `spaceAfter` double-count regression, titled split line count, untitled split, continuation chunk `yFromTop === 0`, mid-page split entry, validator rejection on missing `calloutData`, validator rejection on partial `calloutData` (non-finite fields), validator rejection on partial blockquote padding, non-callout-document early-return.
1810
- - Full suite: 624 tests, 100% pass.
1811
-
1812
- ### Chore / docs
1813
-
1814
- - Removed `brain/learnings/*.md`, `docs/PLAN-v0.6-v1.0.md`, `test/paginate.test.ts.archive` — internal dev artifacts not for the public repo.
1815
- - Stripped `Phase N:` nomenclature from `src/` comments (pure rename — no logic delta).
1816
- - Added `demo/stackblitz/.stackblitzrc`, `docs/articles/pretext-pdf-vs-pdfmake-2026.md` (draft).
1817
- - Added `examples/visual-pr2-bug1-separator.ts` + `examples/visual-pr2-bug3-callout-split.ts` plus 4 reference PNGs under `docs/visuals/pr2/` for bug-reproduction demonstrations.
1818
- - README test badge corrected `650+ → 600+` (verified: 624 tests total).
1819
-
1820
- ---
1821
-
1822
- ## [0.9.0] 2026-04-20
1823
-
1824
- Three additive enhancements that broaden the package's surface without growing its mandatory dependency footprint.
1825
-
1826
- ### Added
1827
-
1828
- - **CLI binary** `pretext-pdf` is now a `bin` entry. `pretext-pdf doc.json out.pdf`, `cat doc.json | pretext-pdf > out.pdf`, `echo '{...}' | pretext-pdf -o out.pdf`. Supports stdin/stdout and file arguments. `--markdown` flag converts Markdown input to PDF in one step (requires the `marked` peer dep). See [src/cli.ts](src/cli.ts).
1829
- - **`pretext-pdf/compat` entry point** — `fromPdfmake(pdfmakeDoc)` translates pdfmake document descriptors into `PdfDocument` so existing pdfmake codebases can switch with a one-line change at the entry point. Covers strings, `text` nodes (with `style`/`bold`/`italics`/`color`/`fontSize`/`alignment`/`font`), `ul`/`ol`, `table` (with `widths` + `headerRows`), `image`, `qr`, `pageBreak` (`before`/`after`), `stack`, `pageSize`/`pageOrientation`/`pageMargins`, `defaultStyle`/`styles`, `info` → metadata, and string-form `header`/`footer`. Default style-name → heading mapping is configurable via `headingMap` option.
1830
- - **Markdown: GFM tables** ([src/markdown.ts](src/markdown.ts)) — `markdownToContent()` now recognises GFM tables and translates them to `TableElement`, including column alignment from `:---:` / `---:` markers. Ragged rows are padded with empty cells.
1831
- - **Markdown: GFM task lists** — `- [x] done` and `- [ ] todo` render with ☑ / ☐ Unicode markers prepended to the item text.
1832
-
1833
- ### Tests
1834
-
1835
- - New `test/v0.9.0-features.test.ts` (21 tests): markdown table + task list, full CLI exec coverage (stdin, file, `--markdown`, error paths), and pdfmake compat (strings, headings, rich-paragraphs, lists, tables, images, QR, `pageBreak`, `stack`, `pageSize`/`pageMargins`, end-to-end render of a translated document).
1836
-
1837
- ### Notes
1838
-
1839
- - Zero new mandatory dependencies. The CLI uses only Node built-ins. The compat shim is pure TypeScript. Markdown additions ride on the existing optional `marked` peer.
1840
- - `dist/cli.js` is wired through `package.json#bin.pretext-pdf` — `npm install -g pretext-pdf` makes the CLI globally available; `npx pretext-pdf` works without install.
1841
-
1842
- ---
1843
-
1844
- ## [0.8.3] 2026-04-20
1845
-
1846
- ### Security
1847
-
1848
- - **SSRF IPv4-mapped IPv6 bypass** ([src/assets.ts](src/assets.ts) `assertSafeUrl`). Pre-0.8.3 the private-IP guard checked the parsed hostname against dotted-decimal regexes only. WHATWG `URL` normalizes `[::ffff:127.0.0.1]` to `[::ffff:7f00:1]` (hex IPv4-in-IPv6), so attacker-supplied URLs of the form `https://[::ffff:127.0.0.1]/admin` slipped past every `^127\.`/`^10\.`/etc. check and reached localhost or RFC 1918 ranges. Patched by detecting both the dotted (`::ffff:127.0.0.1`) and hex-compressed (`::ffff:7f00:1`) IPv4-mapped forms and decoding the embedded IPv4 before regex matching. Also explicitly blocks the IPv6 unspecified address `::`.
1849
- - **SSRF redirect-following bypass** ([src/assets.ts](src/assets.ts) `fetchWithTimeout`). The previous implementation used the default `redirect: 'follow'`, so a public URL could `302` to `http://127.0.0.1:8080/internal` and the library would happily fetch the private target despite the upfront `assertSafeUrl` check on the *initial* URL. Patched to use `redirect: 'manual'` and re-validate every `Location` hop with `assertSafeUrl`, capped at 3 redirects. Browser opaqueredirect responses are rejected with a clear error.
1850
-
1851
- ### Fixed
1852
-
1853
- - **`createGstInvoice` amount-in-words double space for sub-rupee totals** ([src/templates.ts](src/templates.ts)). An invoice whose total was less than ₹1 (e.g. ₹0.50) produced `"Rupees and Fifty Paise Only"` (two spaces after "Rupees") because the rupee-words branch resolved to an empty string. Now uses an explicit `"Zero"` when there are no rupees: `"Rupees Zero and Fifty Paise Only"`.
1854
- - **Markdown deeper-than-2-level lists silently dropped** ([src/markdown.ts](src/markdown.ts) `convertListItem`). Pre-0.8.3 the converter only created text-only leaves for nested lists, so `- A\n - B\n - C` lost C entirely. Now recursive — preserves arbitrary nesting depth in the resulting `ListItem` tree.
1855
- - **Markdown list items with paragraph-typed content** ([src/markdown.ts](src/markdown.ts)). When list items were separated by blank lines, marked emits `paragraph` tokens (not `text` tokens) for the item content. The converter only handled `text`, silently dropping the item text. Now also handles `paragraph` tokens.
1856
-
1857
- ### Tests
1858
-
1859
- - New `test/v0.8.3-ssrf.test.ts` covers 11 IPv4-mapped IPv6 bypass cases, IPv6 unspecified/loopback regressions, and HTTP rejection.
1860
- - Extended `test/phase-10c-markdown.test.ts` with regressions for 3-level nesting and paragraph-typed list items.
1861
- - Extended `test/phase-10d-templates.test.ts` with the sub-rupee amount-in-words case.
1862
-
1863
- ---
1864
-
1865
- ## [0.8.2] 2026-04-20
1866
-
1867
- ### Fixed
1868
-
1869
- - **Rich-paragraph whitespace collapse** — multi-span `rich-paragraph` content rendered with adjacent words overlapping (e.g. `"Founder & CEO" + " — Antigravity Systems"` displayed as `"Founder& CEO—AntigravitySystems"`). Root cause: pretext's `layoutWithLines` follows CSS-like behavior and excludes trailing whitespace from line widths, so tokens like `"Hello "` or `" "` measured to width 0 and downstream fragments overlapped the previous one. `measureTokenWidth` in [src/rich-text.ts](src/rich-text.ts) now uses a sentinel-character technique (append non-whitespace `\u2588`, measure combined string, subtract sentinel width) to recover the true rendered width whenever a token has trailing whitespace. Sentinel width is cached per font config.
1870
- - The fast path (no trailing whitespace) is unchanged single pretext call. Slow path adds two pretext calls per affected token, with one cached.
1871
-
1872
- ### Tests
1873
-
1874
- - Added 3 regression tests in `test/rich-text.test.ts` under `whitespace preservation (v0.8.2 fix)` covering trailing whitespace inside spans, whitespace-only separator spans, and the exact `"Founder & CEO" → "Antigravity Systems"` resume-preset scenario.
1875
-
1876
- ---
1877
-
1878
- ## [0.8.1]2026-04-20
1879
-
1880
- ### Fixed
1881
-
1882
- - **Browser support** — `pretext-pdf` now imports cleanly in browsers. Module-init in `src/fonts.ts` previously called `fileURLToPath(import.meta.url)` and `createRequire(import.meta.url)` eagerly, which threw `"The URL must be of scheme file"` whenever the module was loaded from a non-`file://` URL (esm.sh, jsdelivr, Vite dev server). Both calls are now gated on a runtime `IS_NODE` check, and the bundled-Inter `BUNDLED_INTER_PATHS` arrays are constructed only in Node.
1883
- - **Browser font-loading errors** `loadFontBytes` now throws clear `FONT_LOAD_FAILED` messages when bundled Inter or string font paths are requested in a browser, pointing the consumer at the correct workaround (supply `Uint8Array` bytes via `doc.fonts`).
1884
-
1885
- ### Notes for browser users
1886
-
1887
- - Always supply Inter (or your default font) explicitly via `doc.fonts: [{ family: 'Inter', weight: 400, src: <Uint8Array> }, { family: 'Inter', weight: 700, src: <Uint8Array> }]`. The library cannot read local font files in the browser.
1888
- - SVG / chart / qr-code / barcode elements still depend on `@napi-rs/canvas` at runtime; in the browser, the native `OffscreenCanvas` is used instead and the polyfill is skipped automatically.
1889
-
1890
- ---
1891
-
1892
- ## [0.8.0] — 2026-04-19
1893
-
1894
- ### Added
1895
-
1896
- - **`qr-code` element** — generate QR codes as inline PDF content using the `qrcode` optional peer dependency. Supports `data`, `size`, `errorCorrectionLevel` (L/M/Q/H), `foreground`/`background` hex colours, `margin`, `align`, `spaceBefore`/`spaceAfter`. Fully serverless pure JS, no canvas required.
1897
- - **`barcode` element** — generate 100+ barcode symbologies (EAN-13, Code128, PDF417, QR, DataMatrix, etc.) via the `bwip-js` optional peer dependency. Supports `symbology`, `data`, `width`, `height`, `includeText`, `align`, `spaceBefore`/`spaceAfter`. Pure JS, Lambda/Edge safe.
1898
- - **`chart` element** — embed Vega-Lite charts as vector SVG using `vega` + `vega-lite` optional peer deps. Accepts any Vega-Lite `spec`, `width`, `height`, `caption`, `align`. Rendered with `renderer: 'none'` — zero canvas/puppeteer dependency.
1899
- - **`pretext-pdf/markdown` entry point** — `markdownToContent(md, options?)` converts a Markdown string to `ContentElement[]`. Requires optional `marked` peer dep. Supports headings, bold/italic/links (→ rich-paragraph), lists (2 levels), blockquotes, code blocks, and HR.
1900
- - **`pretext-pdf/templates` entry point** — three typed template functions with zero extra dependencies: `createInvoice(data)` (generic invoice with currency, tax, discount, QR payment), `createGstInvoice(data)` (GST-compliant Indian tax invoice with IGST/CGST+SGST, UPI QR, bank details, amount in words), `createReport(data)` (structured business report with optional TOC).
1901
- - **New error codes** `QR_DEP_MISSING`, `QR_GENERATE_FAILED`, `BARCODE_DEP_MISSING`, `BARCODE_GENERATE_FAILED`, `BARCODE_SYMBOLOGY_INVALID`, `CHART_DEP_MISSING`, `CHART_SPEC_INVALID`, `CHART_RENDER_FAILED`, `MARKDOWN_DEP_MISSING`.
1902
-
1903
- ---
1904
-
1905
- ## [0.7.2] — 2026-04-20
1906
-
1907
- Phase 11 cross-cutting enhancements. Retroactively attributed to 0.7.2; these features were
1908
- originally left as `[Unreleased]` and published out of chronological order after 0.7.1.
1909
-
1910
- ### Added
1911
-
1912
- - **`floatSpans` on image elements** — rich-text alternative to plain `floatText`. Accepts `InlineSpan[]` for mixed bold/italic/color/link captions beside float images. Mutually exclusive with `floatText` (validated).
1913
- - **2-level list nesting** `ListItem.items` now supports one further level of nesting (depth 0 → 1 → 2). Unordered marker: `▪`. Ordered: inherits parent counter or restarts via `nestedNumberingStyle: 'restart'`.
1914
- - **Table `rowspan`** — `TableCell.rowspan` spans a cell across multiple rows. Works alongside `colspan`. Origin cell draws background over full span height; continuation rows automatically receive placeholder cells.
1915
- - **`onFormFieldError` callback** — `doc.onFormFieldError: (name, err) => 'skip' | 'throw'` mirrors `onImageLoadError`. Controls render behaviour when a form field fails.
1916
- - **`createFootnoteSet(defs)`** — helper exported from `pretext-pdf` that generates footnote definition/reference pairs with globally unique IDs. Returns `Array<{ id, def }>`.
1917
- - **`renderDate` field** — `doc.renderDate: Date | string` overrides the PDF creation date. Useful for reproducible builds and testing.
1918
- - **`{{date}}` and `{{author}}` tokens** in header/footer textjoin existing `{{pageNumber}}` / `{{totalPages}}`. `{{date}}` resolves from `renderDate`; `{{author}}` resolves from `doc.metadata.author`.
1919
- - **`tabularNumbers`** on `rich-paragraph` — digits rendered at uniform slot width (widest digit in font), so columns of numbers align without OpenType TNUM feature.
1920
- - **`smallCaps` + `letterSpacing` per span** — `InlineSpan.smallCaps` and `InlineSpan.letterSpacing` now respected in `rich-paragraph` rendering.
1921
- - **Per-span `fontSize`** — `InlineSpan.fontSize` overrides the element-level font size for that span. Enables mixed-size text in a single paragraph.
1922
-
1923
- ### Fixed
1924
-
1925
- - `resolveTokens()` used `.replace()` (replaces first occurrence only) — changed to `.replaceAll()` for all four tokens.
1926
- - Table span grid: continuation-row cursor was advancing by 1 instead of `colspan` when skipping a spanned column now advances by full span width.
1927
- - Font family names now validated for safe characters (`/^[a-zA-Z0-9 _-]+$/`) in `requireFamily()` — rejects null bytes and control characters.
1928
- - Annotation `color` and `author` fields now validated in `validateElement()` for both `paragraph` and `heading` annotations.
1929
- - `buildOutlineTree` memoizes `parentIdxOf()` into a pre-computed array — eliminates O(n²) scan for documents with large heading counts.
1930
- - Table grid-line renderer pre-computes active boundary set eliminates O(rows × cols) inner loop for large tables.
1931
- - `addLinkAnnotation()` re-validates URL scheme at render time (defense-in-depth; `validate.ts` is the primary gate).
1932
-
1933
- ---
1934
-
1935
- ## [0.7.1] 2026-04-19
1936
-
1937
- ### Changed
1938
-
1939
- - **Upstream pretext pinned to `f2014338487a`** — picks up unreleased CJK opening-bracket annotation fix, Hangul jamo line-walker alignment fix, and two internal line-object churn reductions. No public API changes.
1940
-
1941
- ### Fixed
1942
-
1943
- - **List nesting depth enforced at validation** — `ListItem.items` (2nd-level items) now correctly rejects any further `.items` property, matching the documented 2-level maximum. Previously the validation silently passed 3-level data which could cause undefined render behaviour.
1944
- - **3 phase-11 list tests corrected** test data incorrectly contained 3-level nesting while named "2-level"; data trimmed to match documented contract.
1945
-
1946
- ---
1947
-
1948
- ## [0.7.0]2026-04-17
1949
-
1950
- ### Added
1951
-
1952
- - **6 production templates** (`templates/`) — GST invoice, international invoice, resume, multi-section report, NDA, and meeting minutes. Each is a self-contained `.ts` file outputting a valid PDF. Smoke-tested in Phase 2F Block D.
1953
- - **StackBlitz live demo** (`demo/stackblitz/`) 4-tab UI (Invoice, Report, Resume, Custom) backed by a Node.js render server. Edit JSON and generate PDFs instantly, no install required. Accessible at the StackBlitz link in the README.
1954
- - **`## Performance` section in README** — measured render times and PDF sizes for 1-page, 10-page, and mixed-element documents. Font subsetting behaviour documented.
1955
- - **Stress tests and benchmarks** (`test/phase-2f-stress.test.ts`) 32 tests across 4 blocks: large document stress (400-element, 200-row table), edge case stress (CJK, RTL, empty arrays, extreme sizes), timing benchmarks (1-page < 500 ms, 10-page < 5,000 ms), and template smoke tests.
1956
- - **Error code coverage** — new tests for `COLUMN_WIDTH_TOO_NARROW`, `IMAGE_LOAD_FAILED`, `SVG_LOAD_FAILED`, and `ASSEMBLY_FAILED`. 16 of 19 error codes now have direct test coverage.
1957
-
1958
- ### Changed
1959
-
1960
- - **`as any` audit** — eliminated 10 casts in `validate.ts` by introducing a typed `FormFieldElement` local binding. The remaining 8 instances (pdf-lib interop, dynamic import, internal back-references) are now documented with one-line comments explaining the constraint.
1961
- - **Comparison article** (`docs/articles/pretext-pdf-vs-pdfmake-2026.md`) 2,200-word draft covering feature matrix, typography quality, API design, performance, and migration quick-start. Marked `published: false` pending live demo.
1962
- - **Migration guide** (`docs/MIGRATION_FROM_PDFMAKE.md`) 30+ pdfmake pretext-pdf mappings, complete before/after invoice example, and a quick-start checklist. Linked from README.
1963
-
1964
- ### Fixed
1965
-
1966
- - **Phase 2F test types** — `fontWeight: 700 as 700` cast in pre-constructed rows array; removed non-existent `creationDate` from `DocumentMetadata`; replaced `allowCopying: false` with correct `encryption: { permissions: { copying: false } }`.
1967
- - **StackBlitz integration**added `.stackblitzrc` so WebContainer auto-runs `npm start` and opens the browser preview on port 3000.
1968
-
1969
- ---
1970
-
1971
- ## [0.5.3] — 2026-04-16
1972
-
1973
- ### Changed
1974
-
1975
- - **Upgraded `@chenglou/pretext` from 0.0.3 to 0.0.5** picks up improved text analysis accuracy (~35% larger analysis module), better measurement precision, extracted bidi-data module for cleaner tree-shaking, and new `rich-inline` export (not yet used by pretext-pdf). No breaking changes — `prepareWithSegments()` and `layoutWithLines()` APIs are unchanged. All 223 tests pass, 3 example PDFs visually verified (RTL, TOC, hyperlinks).
1976
-
1977
- ---
1978
-
1979
- ## [0.5.2] 2026-04-13
1980
-
1981
- ### Added
1982
-
1983
- - **`onImageLoadError` callback on `PdfDocument`** — gives callers control over image load failures. Return `'skip'` to silently omit the image (preserves existing default behavior). Return `'throw'` to abort rendering with the original error. Previously, all image failures were silently downgraded to `console.warn` with no way to detect them programmatically.
1984
-
1985
- ```typescript
1986
- await render({
1987
- content: [...],
1988
- onImageLoadError: (src, error) => {
1989
- myLogger.warn('Image skipped', { src, error })
1990
- return 'skip' // or 'throw' to abort
1991
- }
1992
- })
1993
- ```
1994
-
1995
- ---
1996
-
1997
- ## [0.4.0] — 2026-04-08
1998
-
1999
- ### Breaking Changes
2000
-
2001
- - **Migrated from `pdf-lib` to `@cantoo/pdf-lib`**`@cantoo/pdf-lib` is now a direct `dependency` (always installed). Previously it was an optional peer dependency required only for encryption. This removes the `ENCRYPTION_NOT_AVAILABLE` error code and the separate `npm install @cantoo/pdf-lib` installation step. Encryption now works out of the box.
2002
- - **`ENCRYPTION_NOT_AVAILABLE` error code removed** — encryption is now always available. Update any `switch` statements that handled this code.
2003
-
2004
- ### Why this change
2005
-
2006
- `pdf-lib` (the original) has not received a meaningful commit since November 2021. `@cantoo/pdf-lib` is the actively maintained fork (v2.6.5, 107+ releases, MIT license). pretext-pdf was already using `@cantoo/pdf-lib` for encryption — this commit makes it the single source of truth for all PDF operations.
2007
-
2008
- ### Added
2009
-
2010
- - `test/pretext-api-contract.test.ts` — canary test that asserts `@chenglou/pretext` exports the exact functions pretext-pdf depends on. Breaks loudly if pretext changes its API.
2011
- - `docs/ROADMAP.md` — public multi-phase development plan
2012
-
2013
- ### Changed
2014
-
2015
- - `@chenglou/pretext` version pinned to exact `0.0.3` (no caret) prevents surprise breaking changes from upstream auto-updates
2016
- - `test:contract` script added — runs the pretext API contract test before the full test suite
2017
- - All internal comments updated from `pdf-lib` to `@cantoo/pdf-lib`
2018
-
2019
- ---
2020
-
2021
- ## [0.3.1] — 2026-04-08
2022
-
2023
- ### Fixed
2024
-
2025
- - **Critical: Font resolution when installed as npm package** — `@fontsource/inter` is now resolved via `createRequire(import.meta.url)` instead of a hardcoded relative path. Previously, `path.join(__dirname, '..', 'node_modules', '@fontsource', 'inter', ...)` failed when npm hoisted the dependency to the consumer's top-level `node_modules`, causing `FONT_LOAD_FAILED` on every install. Now resolves correctly regardless of npm hoisting behavior.
2026
-
2027
- ---
2028
-
2029
- ## [0.3.0] — 2026-04-08
2030
-
2031
- ### Added (Phase 8BInteractive Forms)
2032
-
2033
- - New `form-field` element type — creates interactive AcroForm fields in PDFs
2034
- - `fieldType: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'button'`
2035
- - `label` renders above the field as static text
2036
- - Text fields: `defaultValue`, `multiline`, `placeholder`, `maxLength`
2037
- - Checkboxes: `checked` initial state
2038
- - Radio groups and dropdowns: `options` array, `defaultSelected`
2039
- - `doc.flattenForms: true` — bakes all fields into static content
2040
- - Custom `borderColor`, `backgroundColor`, `width`, `height`, `fontSize` per field
2041
- - New error codes: `FORM_FIELD_NAME_DUPLICATE` (duplicate `name` across fields), `FORM_FLATTEN_FAILED`
2042
- - Post-render `form.updateFieldAppearances()` ensures proper display in all PDF readers
2043
- - 10 comprehensive tests covering all form field types
2044
-
2045
- ### Added (Phase 8E Signature Placeholder)
2046
-
2047
- - `doc.signature` — visual signature box drawn on a specified page
2048
- - Fields: `signerName`, `reason`, `location`, `x`, `y`, `width`, `height`, `page`, `borderColor`, `fontSize`
2049
- - Draws signature line, date line, and optional text inside a bordered rectangle
2050
- - `page` is 0-indexed, defaults to last page, clamps gracefully if out of range
2051
- - 6 comprehensive tests
2052
-
2053
- ### Added (Phase 8D — Callout Boxes)
2054
-
2055
- - New `callout` element type — styled highlight box with optional title
2056
- - Preset styles: `style: 'info'` (blue), `'warning'` (amber), `'tip'` (green), `'note'` (gray)
2057
- - Optional `title` rendered bold above content with left border accent
2058
- - Fully customizable: `backgroundColor`, `borderColor`, `color`, `titleColor`, `padding`
2059
- - Paginates correctly across pages (reuses blockquote pagination logic)
2060
- - 8 comprehensive tests
2061
-
2062
- ### Added (Phase 8F — Document Metadata Extensions)
2063
-
2064
- - `doc.metadata.language` — sets PDF `/Lang` catalog entry (BCP47 tag e.g. `'en-US'`, `'hi'`)
2065
- - `doc.metadata.producer` sets PDF producer field (e.g. `'MyApp v2.1'`)
2066
- - Both fields validate as non-empty strings
2067
- - 5 comprehensive tests
2068
-
2069
- ---
2070
-
2071
- ## [0.2.0] — 2026-04-08
2072
-
2073
- ### Added (Phase 8HInline Formatting)
2074
-
2075
- - `verticalAlign: 'superscript' | 'subscript'` on `InlineSpan` in rich-paragraphs
2076
- - Superscript renders at 65% font size, baseline shifted up by 40% of font size
2077
- - Subscript renders at 65% font size, baseline shifted down by 20% of font size
2078
- - `letterSpacing?: number` on `ParagraphElement`, `HeadingElement`, `RichParagraphElement` — extra pt between characters
2079
- - `smallCaps?: boolean` on those same three element types simulated via uppercase + 80% fontSize
2080
- - Character-by-character rendering for letterSpacing (pdf-lib has no native spacing param)
2081
- - 8 comprehensive tests covering all inline formatting functionality
2082
-
2083
- ### Added (Phase 8A Annotations/Comments)
2084
-
2085
- - New `comment` element type — sticky note annotation at position in document
2086
- - `annotation?: AnnotationSpec` on `ParagraphElement` and `HeadingElement` — attach note to element
2087
- - Supports: `contents`, `author`, `color` (hex), `open` (popup default state)
2088
- - Uses PDF `Subtype: 'Text'` annotation (sticky note icon in PDF viewers)
2089
- - 8 comprehensive tests covering all annotation functionality
2090
-
2091
- ### Added (Phase 8C Document Assembly)
2092
-
2093
- - New `merge(pdfs: Uint8Array[])` exported function combine pre-rendered PDFs
2094
- - New `assemble(parts: AssemblyPart[])` exported function — mix rendered docs + existing PDFs
2095
- - `AssemblyPart` interface: `{ doc?: PdfDocument, pdf?: Uint8Array }`
2096
- - New error codes: `ASSEMBLY_EMPTY`, `ASSEMBLY_FAILED`
2097
- - 8 comprehensive tests covering all assembly functionality
2098
-
2099
- ### Fixed
2100
-
2101
- - **CI case-sensitivity bug**: `test/phase-7-integration.test.ts` used `'en-US'` (uppercase) for hyphenation language. On Linux CI (case-sensitive filesystem) this failed with `UNSUPPORTED_LANGUAGE`. Changed to `'en-us'` to match package name `hyphenation.en-us`.
2102
-
2103
- ---
2104
-
2105
- ## [0.1.1] 2026-04-08
2106
-
2107
- ### Added
2108
-
2109
- - **Phase 8G: Hyperlinks** — Complete link annotation support:
2110
- - `paragraph.url` for external URI links on paragraphs
2111
- - `heading.url` for external URI links on headings
2112
- - `heading.anchor` for named PDF destinations (internal cross-references)
2113
- - `InlineSpan.href` for external and internal `#anchorId` links in rich-paragraphs
2114
- - `mailto:` scheme support for email links
2115
- - GoTo annotations for internal anchor references
2116
- - 9 comprehensive tests covering all hyperlink functionality
2117
-
2118
- ### Fixed
2119
-
2120
- - **Memory leak in test suite**: Removed module-level `_hypherCache` in `src/measure.ts` that accumulated ~188KB per language across 255+ test runs. Changed from cached Hypher instances to fresh instances per call (negligible performance impact, massive memory savings).
2121
- - **Node.js version compatibility**: Replaced `--experimental-strip-types` with `tsx` runner to support Node.js 18.x, 20.x, and 22.x in CI
2122
- - **Broken CI examples**: Removed references to non-existent Phase 8 example scripts from GitHub Actions workflow
2123
- - **README examples mismatch**: Updated Examples section to only list 5 existing Phase 7 examples (watermark, bookmarks, toc, rtl, encryption)
2124
- - **Test suite OOM issues**: Split large test files (paginate.test.ts) into paginate-basic.test.ts to work around Node.js `--experimental-strip-types` heap exhaustion bug on files >17KB
2125
-
2126
- ### Changed
2127
-
2128
- - `test:unit` now runs only `test/paginate-basic.test.ts` (fast, no canvas overhead)
2129
- - Reorganized test scripts: `test:unit`, `test:validate`, `test:e2e`, `test:phases` for better memory management
2130
- - Moved internal planning documentation to archive (preserved, not published)
2131
- - `devDependencies`: Added `@napi-rs/canvas` explicitly (was missing, causing CI failures)
2132
-
2133
- ### Added
2134
-
2135
- - `CONTRIBUTING.md`: Development setup, TDD workflow, PR process guide
2136
- - `CHENG_LOU_EMAIL_DRAFT.md`: Template for requesting endorsement from pretext creator
2137
- - `examples/comparison-pdfmake.ts`: pdfmake version of invoice for typography comparison
2138
-
2139
- ---
2140
-
2141
- ## [0.1.0] — 2026-04-07
2142
-
2143
- ### Added (Phase 7G Encryption)
2144
-
2145
- - `doc.encryption` configuration for password-protecting PDFs
2146
- - User password and owner password support
2147
- - Granular permission restrictions: printing, copying, modifying, annotating
2148
- - Lazy-loads `@cantoo/pdf-lib` (optional peer dependency) — zero cost when not used
2149
- - Error code: `ENCRYPTION_NOT_AVAILABLE` when encryption is requested but dependency not installed
2150
-
2151
- ### Added (Phase 7F — RTL Text Support)
2152
-
2153
- - Right-to-left text support for Arabic, Hebrew, and other RTL languages
2154
- - Unicode bidirectional text algorithm via `bidi-js`
2155
- - `dir` attribute on text elements: `'ltr'` | `'rtl'` | `'auto'` for per-element control
2156
- - RTL text works with headings, paragraphs, lists, tables, and all text elements
2157
- - Automatic detection of mixed LTR/RTL content
2158
-
2159
- ### Added (Phase 7E SVG Support)
2160
-
2161
- - `{ type: 'svg', svg: '<...' }` element for embedding SVG graphics
2162
- - SVG rasterization via `@napi-rs/canvas`
2163
- - ViewBox auto-sizing: automatic height calculation from viewBox aspect ratio
2164
- - Explicit sizing: `width` and `height` parameters for precise control
2165
- - Alignment options: `align: 'left' | 'center' | 'right'`
2166
- - Multi-page support: SVGs paginate correctly across page breaks
2167
- - Error code: `SVG_RENDER_FAILED` for SVG rasterization errors
2168
-
2169
- ### Added (Phase 7D Table of Contents)
2170
-
2171
- - `{ type: 'toc' }` element for automatic TOC generation
2172
- - Two-pass rendering pipeline ensures accurate page numbers
2173
- - Configurable: `title`, `showTitle`, `minLevel`/`maxLevel`, dot leaders, level indentation
2174
- - Auto-indexed from heading structure (H1, H2, H3, etc.)
2175
- - Supports custom formatting via `fontSize`, `color`, `spaceAfter` parameters
2176
-
2177
- ### Added (Phase 7C Hyphenation)
2178
-
2179
- - Automatic word hyphenation for better justified text layout
2180
- - `doc.hyphenation: { language: 'en-US' }` for document-level config
2181
- - Liang's algorithm via `hypher` package for accurate break points
2182
- - Configurable: `minWordLength`, `leftMin`, `rightMin`, per-element `hyphenate: false` opt-out
2183
- - Language support: includes `hyphenation.en-us` (additional languages via npm packages)
2184
- - Error code: `UNSUPPORTED_LANGUAGE` when language not available
2185
-
2186
- ### Added (Phase 7B — Watermarks)
2187
-
2188
- - `doc.watermark` for text or image watermarks on every page
2189
- - Text watermarks: `text`, `fontSize`, `fontWeight`, `color`, `opacity`, `rotation`
2190
- - Image watermarks: `image` (Uint8Array), `opacity`, `rotation`, `color` (tint)
2191
- - Watermarks render behind content (lower z-index)
2192
- - Rotation bounds: -360 ≤ rotation ≤ 360 degrees
2193
- - Validation: must provide either text or image, never both required
2194
-
2195
- ### Added (Phase 7A Bookmarks / PDF Outline)
2196
-
2197
- - PDF sidebar bookmarks auto-generated from heading structure
2198
- - Enabled by default: `bookmarks: true` or `bookmarks: { minLevel: 1, maxLevel: 3 }`
2199
- - Level filtering: include/exclude heading levels from outline
2200
- - Per-heading opt-out: `bookmark: false` on heading elements
2201
- - Keyboard navigation: Cmd/Ctrl+Opt/Alt+O in PDF readers to toggle bookmark sidebar
2202
-
2203
- ### Added (Phase 6 Advanced Features)
2204
-
2205
- - Header and footer support with {{pageNumber}} and {{totalPages}} tokens
2206
- - Text decoration: strikethrough, underline
2207
- - Text alignment: left, center, right, justify
2208
- - Line height control: custom line-height multipliers
2209
- - Column layout with multi-column content flow
2210
- - Tables with colspan/rowspan support
2211
-
2212
- ### Added (Phase 5 Rich Text / Builder API)
2213
-
2214
- - Fluent builder API for programmatic document construction
2215
- - Rich text element with nested formatting (bold, italic, links)
2216
- - Inline code and code blocks with syntax highlighting
2217
- - Block quotes with custom styling
2218
- - Horizontal rules (hr element)
2219
- - Numbered and bulleted lists with nesting
2220
-
2221
- ### Added (Phases 1–4 Core Engine)
2222
-
2223
- - Core PDF generation via `pdf-lib`
2224
- - Element types: paragraph, heading, table, image, list, code, blockquote
2225
- - Font support: Inter bundled, custom TTF embedding
2226
- - Document metadata: title, author, subject, keywords, created date
2227
- - Page sizing: A4, A3, A5, Letter, Legal, or custom dimensions
2228
- - Margins: top, bottom, left, right per page
2229
- - Multi-page pagination with orphan/widow control
2230
- - Image formats: PNG, JPG, WebP
2231
- - Table features: custom column widths, cell padding, borders, header styling
2232
- - Colors: hex color codes throughout (text, backgrounds, borders)
1
+ # Changelog
2
+
3
+ <!-- markdownlint-disable MD024 -->
4
+
5
+ All notable changes to pretext-pdf are documented here.
6
+ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
7
+
8
+ ---
9
+
10
+ ## [2.1.1] — 2026-07-20
11
+
12
+ Dependency security fix.
13
+
14
+ ### Security
15
+
16
+ - **`undici` bumped from `^6.25.0` to `^6.27.0`** — resolves 4 HIGH-severity advisories in this direct production dependency: HTTP header injection via Set-Cookie percent-decoding, WebSocket client DoS via fragment count bypass, HTTP response queue poisoning via keep-alive socket reuse, and Set-Cookie SameSite downgrade via permissive substring matching.
17
+ - Dev-only transitive advisories (`markdown-it`/`linkify-it` via `typedoc`) resolved via `npm audit fix`. `esbuild` (via `tsx`, dev-only, Windows dev-server file read) remains at low severity below the CI `--audit-level=high` gate, tracked in ROADMAP.
18
+
19
+ ### Testing
20
+
21
+ - Full suite re-verified after the bump: 340/340 unit tests, 61/61 security tests (SSRF, IPv4-bypass, scheme blocklist) passing.
22
+
23
+ ---
24
+
25
+ ## [2.1.0] — 2026-06-01
26
+
27
+ Developer experience improvements: auto-coercion for intuitive-but-incorrect patterns, better error messages with structure hints, and deprecation pathway for v3.0 breaking changes.
28
+
29
+ ### Added
30
+
31
+ - **Auto-coercion: List items** — strings now auto-convert to `{text: 'string'}` objects, allowing intuitive `items: ['a', 'b']` syntax alongside `items: [{text: 'a'}, {text: 'b'}]`
32
+ - **Auto-coercion: Fonts** — single font object auto-converts to array, allowing `fonts: {family: 'Inter'}` alongside `fonts: [{family: 'Inter'}]`
33
+ - **Auto-coercion: Table structure** — pdfmake-style `{headers, rows}` tables auto-adapt to pretext `{columns, rows[{cells}]}` schema for smoother migration from other PDF libraries
34
+ - **Better error messages** — validation errors now include expected structure hints: list items show `{text: 'string'}` format, tables show `{columns: [...], rows: [{cells: [...]}]}` format, helping agents and humans understand the correct schema
35
+
36
+ ### Changed
37
+
38
+ - **Deprecation: Callout `content` field** `content` will be renamed to `text` in v3.0 for consistency with paragraph/heading/blockquote. v2.1 emits a console warning when `content` is used.
39
+ - **Table error messages enhanced** — columns and rows validation now include structure hints in error output
40
+
41
+ ### Fixed
42
+
43
+ - **Headers-only table validation** — tables with headers but no data rows now properly trigger VALIDATION_ERROR instead of being silently adapted
44
+ - **Mixed row format corruption prevention** — table adapter now validates row shape (array vs object) before attempting pdfmake-to-pretext conversion, preventing silent data corruption when rows use inconsistent formats
45
+ - **Strict-mode false positives in table adapter** — removed pdfmake-style field properties cleanly with `delete` instead of setting to undefined, eliminating spurious unknown-property warnings in strict validation mode
46
+ - **Callout deprecation warning condition** — fixed logic inversion so warning fires only when `content` field is used without `text` field
47
+
48
+ ### Testing
49
+
50
+ - Added 49 new corpus tests covering list auto-coercion, font coercion, and table adapter edge cases
51
+ - Added 4 new corpus JSON files for agent edge-case testing (list-string-coercion, table-pdfmake, callout-old-field, fonts-object)
52
+ - Added deprecation warning routing tests to verify logger.warn receives v3.0 migration warnings
53
+ - All validation changes verified with pixel-perfect rendering equivalence tests
54
+ - Total test count: 340/340 passing
55
+
56
+ ---
57
+
58
+ ## [2.0.14]2026-05-30
59
+
60
+ Vendor snapshot updated to pretext v0.0.7-patched.1 (upstream v0.0.7 + 11 unique patches).
61
+
62
+ ### Changed
63
+
64
+ - **Vendored pretext layout engine upgraded from v0.0.6-patched.2 to v0.0.7-patched.1.**
65
+ Incorporates upstream v0.0.7 improvements: keep-all mixed-script grouping, punctuation chain wrapping, opening punctuation line breaks, soft-hyphen break preservation, terminal letter spacing, numeric affix stickiness, rich inline boundary overflow prevention, browser-like dash/symbol runs, and line streaming performance optimizations.
66
+ Plus fork-specific patches: German low-opening-quote fix, bidi surrogate handling, trailing collapsible space reconstruction, stream-friendly chunk layout optimization, and no-op merge pass skipping.
67
+
68
+ ---
69
+
70
+ ## [2.0.13] — 2026-05-29
71
+
72
+ Security hardening and coverage improvements from final review pass.
73
+
74
+ ### Security
75
+
76
+ - **Symlink escape closed in `assertPathAllowed`** — `path-allowlist.ts` now calls `realpathSync` before the prefix comparison, so a symlink placed inside an allowed directory cannot dereference to an arbitrary path outside the allowlist (F-1).
77
+ - **Chart `spec` scanned for prototype-pollution keys** — `validateChart` in `media.ts` now rejects any `spec` object containing `__proto__`, `constructor`, or `prototype` keys at any nesting depth before passing it to the Vega renderer (F-9).
78
+ - **`http://` URLs rejected at loader entry points** — `images.ts` and `resolve-content.ts` now throw immediately on `http://` src values instead of entering the fetch path and relying on the inner SSRF guard (F-4).
79
+
80
+ ### Fixed
81
+
82
+ - **`stageInit` metadata fallback logs message only** — `pipeline.ts` no longer serializes the full error object into `console.warn`; only `err.message` is included, preventing internal API names and stack frames from appearing in library stderr (F-3).
83
+
84
+ ### Tests Added
85
+
86
+ - `test/compat-normalize.test.ts` — 62 tests covering all exported functions in `src/compat/normalize.ts` directly via tsx (previously only reachable through the dist/ barrel).
87
+
88
+ ---
89
+
90
+ ## [2.0.12] 2026-05-29
91
+
92
+ Multi-perspective review fixes: security, correctness, silent failures, dead code, and test coverage.
93
+
94
+ ### Security
95
+
96
+ - **`allowedFileDirs` array now copied in `fromPdfmake()`** — Previously assigned by reference, allowing the caller to mutate the allowlist after translation and silently expand path-traversal permissions mid-render. Now copied with `[...arr]` (SEC-1).
97
+
98
+ ### Fixed
99
+
100
+ - **`content` field is now `readonly ContentElement[]` on `PdfDocument`** — The last mutable public field. Prevents callers from `.push()`-ing into a document after passing it to `render()`, which could corrupt multi-stage async pipeline results. `builder.toDocument()` now snapshots the content array at call time (H-1).
101
+ - **`buildFootnoteNumbering` and `runFootnoteTwoPass` parameter widened to `readonly ContentElement[]`** — Required by the `content` readonly change; no semantic change.
102
+ - **Footnote sentinel `?? 0` replaced with `PretextPdfError`** — A missing footnote number now throws `PAGINATION_FAILED` with a clear message instead of silently rendering footnote `0` (M-3).
103
+ - **`onUnsupported` default now logs a one-time warning** The previous default was a silent no-op, contradicting its own JSDoc. Default is now `console.warn('[pretext-pdf/compat] Unsupported pdfmake feature skipped: <feature>')` with per-call deduplication (M-1).
104
+ - **`stageInit` metadata catch block now logs the error** The fallback path for `getInfoDict()` unavailability no longer silently swallows the root cause (M-2).
105
+ - **`FONT_EMBED_FAILED` error message is user-facing** Removed "this is a bug" from the message; replaced with actionable guidance (M-5).
106
+ - **Internal exports removed from `translate.ts`** `translateNodeInner`, `applyStyleToParagraph`, `collectSpans` are no longer exported from the sub-module (M-4).
107
+ - **`_require` unexported from `bundled-paths.ts`** — Was exported with a misleading `_` prefix but only used internally (L-1).
108
+ - **`fonts.ts` header comment includes `collect-text.ts`** — Fourth sub-module was omitted (L-2).
109
+ - **`translate.ts` uses named `ListItem` import** Replaced inline `import('../types.js').ListItem` expression (L-3).
110
+
111
+ ### Tests Added
112
+
113
+ - `test/compat/normalize.test.ts` — 28 direct unit tests for `mergeStyles`, `normalizePageSize`, `normalizeMargins`, `normalizeHeaderFooter`, `normalizeStyleNames`
114
+ - `test/fonts/collect-text.test.ts` — 9 direct unit tests for `collectTextByFont` across paragraph, heading, rich-paragraph, header/footer, and defaultFont override
115
+ - `test/compat.test.ts` 3 isolation tests for `allowedFileDirs` array copy (SEC-1)
116
+ - `test/builder.test.ts` — 2 snapshot isolation tests for `toDocument()` content copy (H-1)
117
+
118
+ ### Changed
119
+
120
+ - **`pdfmake-types.ts`: `allowedFileDirs` is now `readonly string[]`** — Type made symmetric with `PdfDocument.allowedFileDirs`
121
+
122
+ ## [2.0.11] — 2026-05-29
123
+
124
+ Phase 6a: `readonly` on all scalar public interface fields (non-breaking).
125
+
126
+ ### Changed
127
+
128
+ - **All scalar fields on public interfaces marked `readonly`** — `PdfDocument`, `DocumentMetadata`,
129
+ `Margins`, `FontSpec`, `HeaderFooterSpec`, `WatermarkBase`/`WatermarkSpec`, `EncryptionSpec`,
130
+ `SignatureSpec`, `BookmarkConfig`, `HyphenationConfig`, `AnnotationSpec`.
131
+
132
+ Array fields (`content`, `fonts`, `keywords`, `sections`, `allowedFileDirs`) are intentionally
133
+ left mutable marking those `readonly` is a breaking change deferred to v3.
134
+
135
+ This is non-breaking for all callers: TypeScript allows assigning to `readonly` fields in
136
+ object literal initializers, and the runtime behavior is identical.
137
+
138
+ - **`compat.ts`**: Internal construction variables use `Mutable<T>` to allow incremental property
139
+ assignment, cast to the full interface at return. Externally the API is unchanged.
140
+
141
+ ---
142
+
143
+ ## [2.0.10] — 2026-05-29
144
+
145
+ Sprint 5B: Split `compat.ts` (613L) and `fonts.ts` (530L) into focused sub-directories.
146
+
147
+ ### Changed
148
+
149
+ - **`src/compat.ts` split into `src/compat/` (3 files)**
150
+ - `compat/pdfmake-types.ts` (76L) — `PdfmakeDocument`, `PdfmakeObjectNode`, `PdfmakeStyle`, `CompatOptions`, `TranslateCtx`, `DEFAULT_HEADING_MAP`
151
+ - `compat/translate.ts` (155L) — `translateNode`, `translateNodeInner`, `translateTextNode`, `applyStyleToParagraph`, `collectSpans`, `pdfmakeNodeToListItem`
152
+ - `compat/normalize.ts` (115L) `extractFlatText`, `translateTable`, `mergeStyles`, `normalizeStyleNames`, `pdfmakeAlignToPretext`, `normalizePageSize`, `normalizeMargins`, `normalizeHeaderFooter`
153
+ - `compat.ts` (85L) — `fromPdfmake` (the only public export) + type re-exports
154
+ Circular-dependency between translate↔normalize avoided by moving `extractFlatText` to `normalize.ts`.
155
+
156
+ - **`src/fonts.ts` split into `src/fonts/` (4 files)**
157
+ - `fonts/bundled-paths.ts` (50L) — `IS_NODE`, `resolveInterFile`, all four `BUNDLED_INTER_*_PATHS` arrays
158
+ - `fonts/load-bytes.ts` (80L) — `loadFontBytes` (file / Uint8Array / bundled-Inter async loader)
159
+ - `fonts/collect-needed.ts` (75L) — `collectNeededFonts` (document font-variant scanner)
160
+ - `fonts/collect-text.ts` (120L) — `collectTextByFont` (glyph subsetting text collector)
161
+ - `fonts.ts` (70L) — `loadFonts` + `collectTextByFont` re-export (both public exports intact)
162
+
163
+ The public API is unchanged. All existing test and source imports continue to work.
164
+
165
+ ---
166
+
167
+ ## [2.0.9] — 2026-05-29
168
+
169
+ Sprint 5A: dead-export removal, ts-prune scan.
170
+
171
+ ### Changed
172
+
173
+ - **`addGoToAnnotation` removed from `src/render-utils.ts`** — The function was exported but
174
+ never called anywhere in the codebase. GoTo annotation support (internal document links)
175
+ was prepared prematurely; the implementation is preserved in git history and can be
176
+ restored when anchor-link rendering is actually wired up.
177
+
178
+ - **`RTL_REGEX` removed from `src/validate/helpers.ts`** — Exported constant never imported
179
+ anywhere. The regex pattern is preserved in git history.
180
+
181
+ - **`PDFRef` import removed from `render-utils.ts`** — Only needed for the now-deleted
182
+ `addGoToAnnotation` signature.
183
+
184
+ ---
185
+
186
+ ## [2.0.7]2026-05-28
187
+
188
+ Sprint 4: Split `schema.ts` (907 LOC) into focused `src/schema/` modules.
189
+
190
+ ### Changed
191
+
192
+ - **`src/schema.ts` split into `src/schema/` (6 focused files)** — The 907-line monolith is
193
+ replaced by a 19-line barrel re-export and six cohesive sub-files, each under 300 lines:
194
+ - `schema/shared.ts` (30L) — atomic sub-schemas: align, color, fontWeight, dir, space, inlineSpan
195
+ - `schema/elements-text.ts` (270L) — paragraph, heading, blockquote, code, callout, richParagraph, list, toc, footnoteDef
196
+ - `schema/elements-media.ts` (87L) — image, svg, qrCode, barcode, chart
197
+ - `schema/elements-block.ts` (164L) spacer, hr, pageBreak, comment, formField, floatGroup
198
+ - `schema/elements-table.ts` (60L) table
199
+ - `schema/document.ts` (293L) top-level pdfDocumentSchema (assembles all element schemas)
200
+ The public API (`import { pdfDocumentSchema } from 'pretext-pdf/schema'`) is unchanged.
201
+
202
+ ---
203
+
204
+ ## [2.0.6] 2026-05-28
205
+
206
+ Post-sprint audit fixes: /Lang encoding, BCP47 regex, test correctness.
207
+
208
+ ### Fixed
209
+
210
+ - **`/Lang` catalog entry now uses `PDFString.of()` (correct literal string)** Previously
211
+ used `PDFHexString.of()` which stored raw ASCII bytes as invalid hex. `/Lang` is a
212
+ PDF text string per spec §14.9.2; `PDFString` (literal string) is correct and matches
213
+ pdf-lib's own `setLanguage()`. BCP47 tags are ASCII-only (validated by regex) so
214
+ literal-string injection risk is zero.
215
+
216
+ - **BCP47 regex widened to accept `i-` and `x-` prefixed tags** — `/^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/`
217
+ now allows the single-char leading subtag required by grandfathered (`i-klingon`) and
218
+ private-use (`x-custom-app`) tags per RFC 5646 §2.2.7. Previously `{2,8}` incorrectly
219
+ rejected valid language codes.
220
+
221
+ ### Tests
222
+
223
+ - **H6 (signerName truncation)** strengthened replaced the vacuous 5% byte-size ratio with
224
+ a `deepEqual(pdf100, pdf101)` identity check: a 100-char and 101-char signerName must produce
225
+ byte-identical PDFs, directly testing that `slice(0, 100)` is applied.
226
+
227
+ - **M5 (/TU encoding)** — added `FEFF` BOM assertion to the radio and dropdown loop,
228
+ making it consistent with the checkbox/button loop.
229
+
230
+ - **M6 (bookmark Title)** — replaced the `FEFF0043` prefix check (too broad) with the full
231
+ 6-character "Chapte" UTF-16BE hex prefix for an unambiguous unique match.
232
+
233
+ - **M7 (signing field escaping)** — clarified test scope: the visual placeholder path uses
234
+ `drawText()` (CIDFont glyph encoding — no injection risk). The `escapePdfLit` contract is
235
+ tested in `signatures-validation.test.ts` which covers the crypto signing code path.
236
+
237
+ ---
238
+
239
+ ## [2.0.5] — 2026-05-28
240
+
241
+ Sprint 3 audit fixes: annotation array safety, test coverage gaps closed.
242
+
243
+ ### Fixed
244
+
245
+ - **`addStickyNoteAnnotation` now has `instanceof PDFArray` guard** — the last remaining
246
+ `as any` annotation-push in `render-utils.ts`. All three annotation functions
247
+ (`addLinkAnnotation`, `addGoToAnnotation`, `addStickyNoteAnnotation`) now safely fall back
248
+ to creating a new array if the existing `Annots` entry is not a direct `PDFArray`.
249
+
250
+ ### Tests
251
+
252
+ - **H6** (`test/signatures-visual.test.ts`): `signerName` longer than 100 chars renders to
253
+ the same byte-size PDF as a 100-char name — confirms the `slice(0, 100)` truncation is applied.
254
+ - **M5** (`test/forms.test.ts`): `/TU` encoding verified for checkbox, radio, dropdown, and
255
+ button field types not just `text`.
256
+ - **M6** (`test/bookmarks.test.ts`): Bookmark `Title` with special chars (parentheses) appears
257
+ as UTF-16BE hex in the PDF bytes raw literal form `(Chapter (1): ...)` does not appear.
258
+ - **M7** (`test/signatures-visual.test.ts`): Backslash and parentheses in `signerName`,
259
+ `reason`, and `location` render without throwing (escaping confirmed).
260
+
261
+ ---
262
+
263
+ ## [2.0.4] 2026-05-28
264
+
265
+ Sprint 2 audit fixes: type-safety improvements in render-utils.ts and vendor declarations.
266
+
267
+ ### Changed
268
+
269
+ - **`pdf-lib-augment.d.ts` expanded** — Declares `PdfFontEmbedder` interface covering the
270
+ private `embedder.font` (underlinePosition, underlineThickness, xHeight) and `embedder.scale`
271
+ fields accessed for text decoration metrics. The `(pdfFont as any).embedder` cast in
272
+ `render-utils.ts` is now `(pdfFont as unknown as { embedder?: PdfFontEmbedder }).embedder`,
273
+ eliminating untyped `any` propagation into font-metric arithmetic.
274
+
275
+ ---
276
+
277
+ ## [2.0.3] — 2026-05-28
278
+
279
+ Sprint 1 audit fixes: GoTo annotation guard, BCP47 metadata validation, dependency security.
280
+
281
+ ### Fixed
282
+
283
+ - **`addGoToAnnotation` now has `instanceof PDFArray` guard** — the same fix applied to
284
+ `addLinkAnnotation` and `addStickyNoteAnnotation` in v2.0.2 was missed for GoTo annotations.
285
+ Existing `Annots` entries that are not a direct `PDFArray` now fall back to creating a new
286
+ array instead of silently calling `.push()` on an `any`-cast value.
287
+
288
+ - **`metadata.language` validated against `LANGUAGE_TAG_REGEX`** — previously only checked for
289
+ non-empty string and max length. Now enforces the same BCP47 regex (`/^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$/`)
290
+ used for `hyphenation.language`, closing an inconsistency in the validation layer.
291
+
292
+ - **Dependency audit** — `npm audit fix` resolved `fast-uri <=3.1.1` CVE (GHSA-v39h-62p7-jpjc,
293
+ GHSA-q3j6-qgpj-74h6) in the dev dependency chain.
294
+
295
+ ---
296
+
297
+ ## [2.0.2] — 2026-05-28
298
+
299
+ Second post-release audit patch: PDF injection hardening (metadata, AcroForm names), PDFHexString encoding correctness, signing field escaping, annotation array safety.
300
+
301
+ ### Fixed
302
+
303
+ - **Metadata Info dict fields now use `PDFHexString.fromText()` (correct UTF-16BE encoding)** —
304
+ The v2.0.1 fix used `PDFHexString.of()` which stored raw bytes between `<>` delimiters (not
305
+ valid hex). `fromText` correctly produces `<FEFF...>` UTF-16BE hex, the same encoding used by
306
+ pdf-lib's built-in `setTitle()` etc. Injection protection is now both correct AND effective.
307
+
308
+ - **Form field `name` and option `value` restricted to AcroForm-safe characters** — Field names
309
+ and radio/dropdown option export values are written as PDF literal strings by pdf-lib. Characters
310
+ like `)`, `\`, and null bytes that could corrupt the AcroForm `/T` dictionary are now rejected
311
+ at validation time with a clear error message. Allowed: `[a-zA-Z0-9_.@-]+` (`HIGH-2`).
312
+
313
+ - **Signing placeholder fields escaped for PDF literal strings** — `sig.reason`, `sig.location`,
314
+ `sig.contactInfo`, and `sig.signerName` are passed to `@signpdf/placeholder-pdf-lib` which
315
+ writes them as PDF literal strings. Backslashes and parentheses are now escaped with `\` before
316
+ passing, so values like "New York (USA)" are preserved correctly without breaking the dict.
317
+
318
+ - **Annotation array push guarded with `instanceof PDFArray`** — The previous `as any` cast on
319
+ `pdfDoc.context.lookup(existingAnnots)` allowed silent no-ops if the value wasn't a PDFArray.
320
+ All three annotation functions now check `instanceof PDFArray` and fall back to creating a new
321
+ array rather than silently dropping the annotation.
322
+
323
+ - **Bookmark `Title` and AcroForm `/TU` use `PDFHexString.fromText()`** — Same `of()` → `fromText()`
324
+ correction applied to bookmark headings and all five AcroForm `/TU` (accessibility tooltip) writes.
325
+
326
+ - **Sticky note `Contents` and author `T` use `PDFHexString.fromText()`**Human-readable text in
327
+ sticky note annotations now uses proper UTF-16BE encoding.
328
+
329
+ - **Signature placeholder `signerName` truncated to 100 chars** — Prevents glyph overflow outside
330
+ the visual signature box for very long signer names.
331
+
332
+ - **Schema `content.items` changed from `anyOf` to `oneOf`** — JSON Schema validators and AI agent
333
+ code generators now get exclusive-match semantics, preventing multi-schema ambiguity.
334
+
335
+ ### Tests
336
+
337
+ - Added T6: `/TU` accessibilityLabel byte-level injection guard in `test/forms.test.ts`.
338
+ - Added T7: metadata title/author/accessibility UTF-16BE hex-encoding byte checks in `test/metadata.test.ts`.
339
+ - Added field name and option value AcroForm-safety tests in `test/forms.test.ts`.
340
+
341
+ ---
342
+
343
+ ## [2.0.1] — 2026-05-28
344
+
345
+ Post-release patch: audit-driven hardening of every change introduced in v2.0.0.
346
+
347
+ ### Fixed
348
+
349
+ - **SVG sanitizer: `on*` handlers with whitespace in attribute name now stripped** — Attackers
350
+ could inject `on\nload=` or `on\tclick=` to bypass the previous `\w+` name regex. The pattern
351
+ now uses `[\w\r\n\t ]+` for the attribute name portion (`H3`).
352
+
353
+ - **SVG sanitizer: `expression()` strips arguments with nested parens** — `expression(alert(1))`
354
+ and `expression(eval(x))` are now stripped in a single pass. The inner-parens pattern
355
+ `(?:[^()]*|\([^()]*\))*` handles one level of argument nesting; multi-pass unwinds deeper
356
+ nesting (`M6`).
357
+
358
+ - **SVG sanitizer: size/element-count guards now throw `PretextPdfError('SVG_LOAD_FAILED')`** —
359
+ Previously both guards returned the raw unstripped SVG on overflow, silently bypassing
360
+ script/event stripping. Now both throw, so callers always receive a typed error (`H1`).
361
+
362
+ - **Form-field strict mode no longer flags cross-variant props as unknown** `ALLOWED_PROPS`
363
+ entry for `form-field` now covers the union of all variant-specific keys so the first strict
364
+ check never fires false positives. Per-variant narrowing still runs inside `validateFormField`
365
+ to catch cross-contamination (`B1`).
366
+
367
+ - **Bookmark `Title` and AcroForm `/TU` now use `PDFHexString`** — Previously `PDFString` was
368
+ used, which is vulnerable to unbalanced-parenthesis injection in user-controlled strings. All
369
+ five `/TU` writes and the outline `Title` write now use the hex-encoded form (`B3`).
370
+
371
+ - **Link URI annotation and sticky-note `Contents`/`T` use `PDFHexString`** Same injection
372
+ guard extended to `addLinkAnnotation` and `addStickyNoteAnnotation` (`B3`).
373
+
374
+ - **`getInfoDict()` private-API call is now try/caught** — Accessibility and semantic metadata
375
+ are silently omitted (rather than throwing) if the `@cantoo/pdf-lib` internal API is removed
376
+ in a future library version (`M7`).
377
+
378
+ - **`SignPdf`/`P12Signer` dynamic imports typed via local interfaces** — Removes `any` casts
379
+ on the signpdf module destructuring, catching future API drift at compile time (`M4`).
380
+
381
+ - **Signing error message scrubbed of certificate details** — `SIGNATURE_FAILED` no longer
382
+ leaks P12 structural details or ASN.1 internals; only the first 120 chars of the underlying
383
+ error message are included (`F9`).
384
+
385
+ - **`FORM_FIELD_VARIANT_PROPS` type narrowed** The export type is now
386
+ `Record<'text'|'checkbox'|'radio'|'dropdown'|'button', ReadonlySet<string>>` so callers that
387
+ iterate the map get exhaustive narrowing rather than a plain string index (`M1`).
388
+
389
+ - **`fieldType` validation is now derived from `FORM_FIELD_VARIANT_PROPS`** — The allowed-values
390
+ list and the dispatch map are now the same object, removing the risk of one going stale (`M2`).
391
+
392
+ - **Form-field options arrays are structurally validated** — Each item in `options` for `radio`
393
+ and `dropdown` is now checked to be a `{value: string, label: string}` object. Invalid items
394
+ throw `VALIDATION_ERROR` with an indexed path like `options[1].value` (`H2`).
395
+
396
+ - **`accessibilityLabel` validated as non-empty string** Empty or non-string values now
397
+ throw `VALIDATION_ERROR` rather than embedding an empty `/TU` annotation (`H2`).
398
+
399
+ - **`signing/placeholder.ts` uses `buildFontKey()` instead of hardcoded literal** — Keeps the
400
+ font lookup in sync with the canonical key format if the naming convention ever changes (`L1`).
401
+
402
+ - **`PluginMeasureResult.spaceBefore/spaceAfter` marked readonly** Plugins cannot mutate the
403
+ pipeline's spacing side-table; callers can still construct the object with literal values (`L2`).
404
+
405
+ - **`package.json` `mcpCompat` range updated** Was `>=1.4.0 <2.0.0` (excluded v2.0.0 itself);
406
+ now `>=1.5.0 <3.0.0` (`B2`).
407
+
408
+ - **`pdfDocumentSchema`: `form-field` now a `oneOf` discriminated union** The flat single-object
409
+ schema was replaced with five variant schemas keyed by `fieldType: { const: '...' }`, with
410
+ correct per-variant `required` arrays (radio/dropdown require `options`) (`M3`).
411
+
412
+ ### Tests
413
+
414
+ - Added T1: `on\nload` and `on\tclick` attribute-name injection regression guard in
415
+ `test/svg-sanitizer.test.ts`.
416
+ - Added T2: Per-variant strict-mode unknown-props tests for form-field in
417
+ `test/validate-strict.test.ts` (Group 8).
418
+ - Added T3: `./signing` entry point added to `test/public-api-surface.test.ts` tripwire.
419
+ - Added T4: `MAX_SVG_ELEMENTS` boundary tests (at-limit sanitizes, over-limit throws) in
420
+ `test/svg-sanitizer.test.ts`.
421
+ - Added T5: `fieldType: 'textarea'` (unknown value) test triggering `VALIDATION_ERROR`.
422
+
423
+ ---
424
+
425
+ ## [2.0.0] — 2026-05-28
426
+
427
+ Major release. Three breaking changes, six improvements, and two security fixes.
428
+
429
+ ### Breaking Changes
430
+
431
+ - **`FormFieldElement` is now a discriminated union** Replace the single flat interface with
432
+ five per-variant types (`TextFormField | CheckboxFormField | RadioFormField | DropdownFormField |
433
+ ButtonFormField`) that narrow available properties by `fieldType`. The `BaseFormField` interface
434
+ holds all shared fields and is also exported. Callers that assign into `FormFieldElement` without
435
+ narrowing will need to specify the concrete variant. **Migration:** narrow on `fieldType` or
436
+ import the specific variant type.
437
+
438
+ - **`HorizontalRuleElement.spaceAbove` / `spaceBelow` removed** Use `spaceBefore` / `spaceAfter`
439
+ (stable since v0.9). The deprecated aliases were present in v1.x with `@deprecated` JSDoc.
440
+ **Migration:** replace `spaceAbove` → `spaceBefore`, `spaceBelow` → `spaceAfter`.
441
+
442
+ - **`ValidationResult.warningCount` removed** The field was always `0` in v1.x (validator emits
443
+ errors only). **Migration:** use `result.errors.filter(e => e.severity === 'warning').length` if
444
+ you need a count returns `0` until warning-severity issues are introduced.
445
+
446
+ ### Added
447
+
448
+ - **`./signing` export** New subpath export `pretext-pdf/signing` exposes `applySignature`,
449
+ `applyEncryption`, `applyPostProcessing`, and `renderSignaturePlaceholder` as a standalone
450
+ signing module. Useful for post-render pipeline composition without importing the full library.
451
+
452
+ - **`MAX_SVG_ELEMENTS` constant** — Exported from `dist/assets.js` alongside `SVG_MAX_BYTES`.
453
+ Heuristic element-count guard (5,000 open tags) that skips sanitization of pathologically deep
454
+ SVGs to prevent memory exhaustion during rasterization.
455
+
456
+ - **Per-variant `accessibilityLabel` wired to AcroForm `/TU`** — `accessibilityLabel` on form
457
+ fields is now written to the PDF annotation `/TU` (tooltip/alt-text) dictionary entry.
458
+ Screen readers and assistive technology that consume AcroForm annotations will pick it up.
459
+
460
+ - **`metadata.accessibility` / `metadata.semantic` written to Info dict** — Both reserved fields
461
+ are now serialized as JSON strings into the PDF Info dictionary (`Accessibility` and `Semantic`
462
+ keys). Previously they were accepted but ignored at render time.
463
+
464
+ ### Changed
465
+
466
+ - **`ValidationError` and `ValidationResult` fields are `readonly`** — All fields on both
467
+ interfaces are now `readonly`. This is technically breaking for callers that mutate validation
468
+ results directly (which should be none in practice).
469
+
470
+ - **`PluginMeasureResult.height` is `readonly`** — Prevents accidental mutation of the measured
471
+ height by render hooks.
472
+
473
+ ### Fixed (security)
474
+
475
+ - **SVG `on*` handler regex now catches newline-injected attributes** — The previous pattern
476
+ `\bon\w+\s*=` missed attributes like `on\nload=`. Fixed to `\bon\w+[\s\n\r]*=`.
477
+
478
+ - **`allowed-props` strict mode now covers `accessibilityLabel`, `accessibility`, `semantic`** —
479
+ These fields were missing from their respective key arrays; strict-mode validation would
480
+ incorrectly report them as unknown properties. Fixed.
481
+
482
+ ---
483
+
484
+ ## [1.9.0] — 2026-05-28
485
+
486
+ Additive release: error categorisation, typed plugin generics, beta graduations, soft deprecations, and a 100 MB output size guard.
487
+
488
+ ### Added
489
+
490
+ - **`PretextPdfError.category`** — New readonly field on every thrown error. Groups all 50+
491
+ error codes into 8 high-level categories (`'validation' | 'font' | 'image' | 'layout' |
492
+ 'security' | 'dependency' | 'signature' | 'render'`). Lets callers branch on class of
493
+ failure without exhaustive switches. Also exported as the `ErrorCategory` type.
494
+
495
+ - **`LEGACY_ERROR_CODE_MAP`** — `Record<string, ErrorCode>` exported from the main entry
496
+ point. Documents the canonical code for any renamed or aliased error strings. Currently
497
+ maps `FONT_ENCODE_FAILFONT_ENCODE_FAIL` (no renames have occurred yet); future renames
498
+ will be recorded here before the old string is removed.
499
+
500
+ - **`PluginDefinition<T>` generic** `PluginDefinition`, `PluginMeasureResult`, and
501
+ `PluginRenderContext` are now generic over a `T` type parameter (default `unknown`).
502
+ `pluginData` in `measure` and `render` is now typed as `T` instead of `unknown`. Fully
503
+ backward compatible existing plugins without a type argument continue to compile.
504
+
505
+ - **`MAX_PDF_BYTES` constant** — Exported from the main entry point. Value: `100 * 1024 * 1024`
506
+ (100 MB). `render()` now throws `RENDER_FAILED` if the serialized PDF exceeds this limit,
507
+ preventing runaway memory use from pathological documents.
508
+
509
+ ### Changed (promotions — no breaking changes)
510
+
511
+ - **`PluginDefinition`, `PluginMeasureContext`, `PluginMeasureResult`, `PluginRenderContext`**
512
+ Promoted from `@beta` to `@public`. The plugin API is stable.
513
+
514
+ - **`RenderOptions.plugins`** `@beta` tag removed. Stable.
515
+
516
+ - **`createFootnoteSet()`** `@beta` tag removed. Stable.
517
+
518
+ ### Deprecated
519
+
520
+ - **`HorizontalRuleElement.spaceAbove`** and **`.spaceBelow`**Use `spaceBefore` and
521
+ `spaceAfter` instead (consistent with paragraph/heading naming). Both aliases remain
522
+ functional in v1.x. Will be removed in v2.0.
523
+
524
+ - **`ValidationResult.warningCount`** — Always `0` in v1.x (the validator emits errors
525
+ only). Will be removed in v2.0. Use
526
+ `result.errors.filter(e => e.severity === 'warning').length` directly when
527
+ warning-severity items are introduced.
528
+
529
+ ---
530
+
531
+ ## [1.8.0] — 2026-05-28
532
+
533
+ Additive release: type narrowing, shared leaf modules, 5 new test coverage points, and a Windows font-path validation bugfix.
534
+
535
+ ### Added
536
+
537
+ - **`ColumnDef.width` narrowed** — `number | string` → `number | \`${number}*\` | '*' | 'auto'`. The `as unknown as number` cast in `compat.ts` that faked `'auto'` as a number is now removed; `'auto' as const` is the direct type. No behavior change — accepted inputs are identical.
538
+
539
+ - **`DocumentMetadata.accessibility?` and `DocumentMetadata.semantic?` reserved** (`Record<string, unknown>`, `@alpha`) — Forward-reserved slots for PDF/UA accessibility metadata and document-structure annotations. No render-time effect in v1.x; schema allows them so tooling can start populating them. Will be wired in v2.0.
540
+
541
+ - **`FormFieldElement.accessibilityLabel?` reserved** (`string`, `@alpha`) — Reserved slot for AcroForm `/TU` tooltip/alt-text annotation. Stored but not written to the PDF in v1.x. Will be wired in v2.0.
542
+
543
+ - **`src/url-utils.ts` leaf module** — `SAFE_URL_SCHEME` regex consolidated into a single shared module. Previously duplicated in `render-utils.ts` and `validate/helpers.ts` (v1.7.2 added dedup comments). Both sites now import from `url-utils.ts`; `validate/helpers.ts` re-exports it for downstream consumers.
544
+
545
+ - **`src/font-key.ts` leaf module** — `buildFontKey` extracted from `measure.ts` to break the `measure.ts → measure-blocks/index.ts → measure.ts` circular dependency. All 9 former `./measure.js` importers now import directly from `./font-key.js`. `measure.ts` re-exports `buildFontKey` for backward compatibility.
546
+
547
+ - **`test/error-coverage.test.ts`** — Fills 5 coverage holes identified in the v1.7.2 audit:
548
+ 1. `FONT_LOAD_FAILED` relative font path rejection
549
+ 2. `FONT_LOAD_FAILED` — nonexistent absolute font path
550
+ 3. `SVG_LOAD_FAILED` local mock server returning HTTP error (via `fetchWithTimeout` directly)
551
+ 4. `CHART_LOAD_FAILED` silent path — chart failure warns, not throws (contract guard)
552
+ 5. `RTL_REORDER_FAILED` error code round-trips correctly; RTL Arabic renders without error
553
+
554
+ ### Fixed
555
+
556
+ - **`validate/helpers.ts — validateFontSpec` Windows path false positive** — Windows absolute paths like `F:\path\font.ttf` were being parsed by `new URL()` as `f:` protocol URLs, causing valid font paths to throw `VALIDATION_ERROR: 'src' contains a URL`. Fixed by skipping the URL check for paths that `isAbsolutePath()` identifies as OS-native absolute paths (POSIX `/` prefix or Windows `[A-Z]:\` prefix).
557
+
558
+ ---
559
+
560
+ ## [1.7.2] 2026-05-28
561
+
562
+ Internal hardening. No public API changes.
563
+
564
+ ### Fixed (silent failures observable warnings)
565
+
566
+ - **`render.ts` `getForm()` bare-catch now logs a warning** previously the error was swallowed silently with a comment. Now routes through the existing `warn()` bridge so structured-logger callers can observe it.
567
+
568
+ - **`render-blocks/footnote.ts` — silent footnote drop on font miss now logs** `if (!pdfFont) continue` now emits a `[pretext-pdf] footnote #N: font "key" not in fontMap footnote skipped` warning to stderr before skipping.
569
+
570
+ - **`measure-blocks/image.ts` — non-null assertion on `floatText` replaced with guard** — `element.floatText!` → `element.floatText !== undefined` guard. If neither `floatSpans` nor `floatText` is present, `textLines` stays empty and the float renders image-only (valid case) instead of crashing with `Cannot read properties of undefined`.
571
+
572
+ ### Fixed (code correctness)
573
+
574
+ - **`compat.ts` — proto-pollution defense restored** — `copySafeStyleProperties` used `key in source` (traverses prototype chain) instead of `Object.hasOwn`. Replaced with `Object.hasOwn` and narrowed the `source` parameter from `any` to `unknown`, removing the unsafe `as any` cast at the assignment site.
575
+
576
+ ### Improved (exhaustiveness + type safety)
577
+
578
+ - **`render-extras.ts` switch(el.fieldType)** — added `default` arm with runtime warning and TypeScript `never` guard. New `fieldType` values added without updating this switch will produce a compile error.
579
+
580
+ - **`render-utils.ts` switch(align)** added `default` arm with `satisfies never` guard and `startX` safe fallback. The `'left' | 'center' | 'right'` union is already exhaustive; the guard prevents silent breakage if the union ever widens.
581
+
582
+ - **`fonts.ts` switch(el.type)** added explicit `default: break` arm with a comment listing intentionally-unhandled types. Previously the fall-through silence could hide newly-added element types that do carry text (and would need glyph collection).
583
+
584
+ ### Fixed (stale comments)
585
+
586
+ - **`types-public/render-options.ts`** corrected false claim that bidi-js fallback warnings go to `console.warn` directly; they've been routed through `setBidiWarnFn` / the logger bridge since v1.3.x.
587
+
588
+ - **`render-utils.ts` and `validate/helpers.ts`** — annotated the duplicate `SAFE_URL_SCHEME` regex with a `// NOTE: duplicated` comment pointing to the other location. Consolidation to a shared `url-utils.ts` leaf is deferred to v1.8.
589
+
590
+ - **`assets/loaders/images.ts`** corrected "still in assets.ts for this commit" actual location `assets/loaders/orchestrator.ts`.
591
+
592
+ - **`measure-text.ts`** — corrected "measure-blocks.ts call sites" → "call sites in measure-blocks/" (the file was split into a directory in v1.6.0).
593
+
594
+ - **`fonts.ts`** — corrected inline comment `(assets.ts)` → `(assets/security/path-allowlist.ts)`.
595
+
596
+ - **`measure.ts`** corrected inline comment `(assets.ts)` `(assets/loaders/orchestrator.ts)`.
597
+
598
+ ### Tests rewritten (theatrical → meaningful)
599
+
600
+ - **`test/signatures-crypto.test.ts`** `'SIGNATURE_DEP_MISSING when @signpdf/signpdf not installed'` renamed and rewritten as `'invisible-only signature: renders without p12, no crypto deps needed'`. The old test body had nothing to do with `SIGNATURE_DEP_MISSING`; the new body verifies that invisible signing without a `p12` path produces a valid PDF that does NOT contain a `/ByteRange` dict (confirming no PKCS#7 signing ran).
601
+
602
+ - **`test/validate-concurrent.test.ts`** — The `'8 parallel validates'` test was vacuous: since `validateDocument` is synchronous, `parallel[i]` trivially equals `sequential[i]`. Replaced with a content-specific isolation test: 5 intentionally-broken docs are validated, and each result's error messages are asserted to mention a keyword specific to THAT doc's element type (e.g. paragraph errors mention "text", table errors mention "columns"). This catches module-level state leakage without relying on async interleaving.
603
+
604
+ ---
605
+
606
+ ## [1.7.1] 2026-05-27
607
+
608
+ Security hardening for SVG sanitizer + schema accuracy fix for signing fields.
609
+
610
+ ### Security
611
+
612
+ - **SVG `<style>` block: strip `@import` rules** `sanitizeSvg` now removes `@import` directives before any CSS can be used to trigger an outbound network request from the rasterizer. Previously only `expression()` was stripped from inline `<style>` blocks; `@import url('https://attacker.example/track.css')` would survive sanitization.
613
+
614
+ - **SVG `<style>` block: strip `url(javascript:|vbscript:|data:)` values** — Removes URL function calls using JS/vbscript/data schemes from style content, closing an injection path that bypasses the existing `<a href>` and `<image href>` filters (those only target *attribute values*, not *CSS property values*).
615
+
616
+ - **SVG `<style>` block: strip `url(https?://...)` values (defense-in-depth)** — SVGs embedded in PDFs have no legitimate reason to hot-link external stylesheets or background images. Removes all HTTP/HTTPS `url()` references from style blocks so the rasterizer cannot be coerced into outbound connections via crafted input.
617
+
618
+ ### Fixed
619
+
620
+ - **`schema.ts` missing signing fields** — `signature` schema block was missing `p12`, `passphrase`, and `contactInfo` that were added to `SignatureSpec` in v1.7.0. JSON schema consumers (e.g. editors, the MCP generate_pdf tool) now see all valid signing inputs. The TypeScript type was always correct; this was a schema-only gap.
621
+
622
+ ### Changed
623
+
624
+ - **`schema.ts` signature description** updated from "Visual signature placeholder drawn on a specified page" to reflect that providing `p12` enables PKCS#7 cryptographic signing; without `p12` a visual-only placeholder box is drawn.
625
+
626
+ ---
627
+
628
+ ## [1.7.0] — 2026-05-25
629
+
630
+ Signing path repaired. **No public API changes.** The cryptographic signing pipeline has been architecturally broken end-to-end since v1.3.6 — calling `render({ signature: { p12, passphrase } })` would fail with `SIGNATURE_FAILED: PDF signing failed: No ByteRangeStrings found within PDF buffer`. v1.7.0 fixes it with a surgical change inside `applySignature`.
631
+
632
+ ### Fixed (critical)
633
+
634
+ - **Signing path was architecturally broken since v1.3.6**. Root cause: `applySignature` loaded the placeholder doc via `@cantoo/pdf-lib`'s `PDFDocument.load`, then handed it to `@signpdf/placeholder-pdf-lib.pdflibAddPlaceholder`, which internally builds `/ByteRange` using **upstream** `pdf-lib`'s `PDFArray`/`PDFNumber`/`PDFName` classes (it imports them directly from `"pdf-lib"`). Cantoo's serializer doesn't recognize upstream's class instances and emitted a malformed `/ByteRange` dict; `@signpdf/utils.findByteRange` then aborted parsing with `No ByteRangeStrings found within PDF buffer`. Fix: load the doc via **upstream `pdf-lib`** for the placeholder hop only. Encryption stays on `@cantoo/pdf-lib` in `applyEncryption` — the two paths are mutually exclusive via the existing `SIGNATURE_CERT_AND_ENCRYPTION` guard, so we never need both pdf-libs active simultaneously. The previously KNOWN-BROKEN `test/signatures-crypto.test.ts → P12 signature verifies cryptographically (real CMS verify)` is now unskipped and green.
635
+
636
+ ### Added
637
+
638
+ - **AcroForm regression assertion** inside `test/signatures-crypto.test.ts`. Any valid signed PDF must carry `/AcroForm`, `/Fields [...]`, `/SigFlags 3`, and a `/Type /Sig` object — these four properties are asserted on the signed bytes so a future regression that loses AcroForm structure surfaces immediately.
639
+ - **Signature path snapshot tripwire** (`test/signatures-snapshot.test.ts` + `test/data/signatures-snapshot.json`). Captures the categorical structural shape of a signed PDF (presence of `/ByteRange`, `/AcroForm`, `/SigFlags` value, etc.) rather than byte offsets (which are document- and randomness-dependent). Wired into `test:phases`. Regenerate with `UPDATE_SNAPSHOT=1`.
640
+
641
+ ### Changed
642
+
643
+ - **`pdf-lib` declared as an explicit optional peer dependency** (`^1.17.1`). It was previously only present transitively via `@signpdf/placeholder-pdf-lib`'s `dependencies`. Now it's a documented peer with `peerDependenciesMeta.pdf-lib.optional: true`, mirroring the existing `@signpdf/*` pattern. Users with `@signpdf/*` already installed need no action — npm satisfies the new peer from the existing transitive install.
644
+ - **`SIGNATURE_DEP_MISSING` error message** now lists `pdf-lib` alongside the three `@signpdf/*` packages and drops the "currently non-functional due to fork incompatibility" disclaimer that was added in v1.3.6.
645
+
646
+ ### Migration
647
+
648
+ None. Same `signature: { p12, passphrase, reason, contactInfo, signerName, location, page, invisible }` config. Same error codes (`SIGNATURE_DEP_MISSING`, `SIGNATURE_P12_LOAD_FAILED`, `SIGNATURE_FAILED`, `SIGNATURE_CERT_AND_ENCRYPTION`). One error message string changed: `SIGNATURE_DEP_MISSING` now mentions `pdf-lib` and no longer carries the "non-functional" disclaimer.
649
+
650
+ ### Verification
651
+
652
+ - All 456 tests pass (was 454 pass + 1 skipped pre-fix; now 455 pass after unskip + 1 new snapshot test = 456 total).
653
+ - Encryption-after-signing path still rejected with `SIGNATURE_CERT_AND_ENCRYPTION` (regression-tested).
654
+ - All 7 v1.6.0 verification gates (G1–G7) still pass.
655
+
656
+ ---
657
+
658
+ ## [1.6.0]2026-05-25
659
+
660
+ Internal restructuring + SVG sanitizer hardening. **No public API changes.** The previously-monolithic `src/assets.ts` (961 lines pre-sprint) has been split into 10 focused files under `src/assets/`. A 14-line back-compat shim at `src/assets.ts` re-exports the barrel so every existing consumer (internal modules, public API, and direct test imports via `dist/assets.js`) keeps working unchanged.
661
+
662
+ ### Security
663
+
664
+ - **SVG sanitizer hardening** `sanitizeSvg` now strips three additional payload classes that survived the previous regex chain:
665
+ - **`<foreignObject>` blocks** the only XML-in-SVG construct that can host arbitrary HTML/XML namespaces. Both self-closing and paired forms are removed wholesale; sibling SVG primitives (`<rect/>`, `<path/>`, etc.) are preserved.
666
+ - **`javascript:` / `vbscript:` / `data:` hrefs on `<a>` elements** — previously only `<image>`/`<use>` hrefs were filtered. Only the dangerous href attribute is dropped, so the `<a>` element's text children still render.
667
+ - **CSS `expression(...)` calls inside `<style>` blocks** — legacy IE XSS vector. Only the `expression(...)` call site is excised; the surrounding stylesheet remains parseable.
668
+
669
+ Coverage: new `test/svg-sanitizer.test.ts` (10 cases) plus expanded MA-4 / MA-5 fixtures in `test/data/assets-split-tripwire.json` (30 fixtures total).
670
+
671
+ ### Changed
672
+
673
+ - **`src/assets.ts` split into 10 files** under `src/assets/` (see Internal). No public API change — the file at `src/assets.ts` is now a 14-line re-export shim that aggregates the new barrel `src/assets/index.ts`. Consumers importing from `dist/assets.js` continue to resolve every previously-public symbol (`loadImages`, `assertPathAllowed`, `sanitizeSvg`, `assertSafeUrl`, `resolveAndValidateUrl`, `normalizeIpv4Hostname`, `fetchWithTimeout`, `redactPath`, `VECTOR_RASTER_CONCURRENCY`, plus the `ResolvedSafeUrl` type) at the same module path.
674
+ - **`PretextPdfError` constructor signature snapshot refreshed** — `etc/pretext-pdf.api.md` now reflects the `options?: ErrorOptions` parameter that was added in v1.2.1 but never re-snapshotted. No code change — the parameter has been live for several releases. The snapshot was stale; this release reconciles it.
675
+
676
+ ### Internal
677
+
678
+ - **New `src/assets/` directory layout:**
679
+ ```
680
+ src/assets/
681
+ ├── index.ts # internal barrel
682
+ ├── util/
683
+ │ └── redact-path.ts # commit 4
684
+ ├── security/
685
+ │ ├── path-allowlist.ts # commit 5
686
+ │ ├── ipv4-normalize.ts # commit 6
687
+ │ ├── url-validation.ts # commit 7
688
+ │ └── fetch.ts # commit 8 (undici Agent stays lazy)
689
+ ├── svg/
690
+ │ ├── sanitize.ts # commit 9 (+ SVG_MAX_BYTES)
691
+ │ ├── dimensions.ts # commit 10
692
+ │ ├── resolve-content.ts # commit 10
693
+ │ └── rasterize.ts # commit 11 (@napi-rs/canvas dynamic)
694
+ ├── generators/
695
+ │ ├── qr.ts # commit 12 (qrcode dynamic)
696
+ │ ├── barcode.ts # commit 12 (bwip-js dynamic)
697
+ │ └── chart.ts # commit 12 (vega/vega-lite dynamic)
698
+ └── loaders/
699
+ ├── images.ts # commit 13
700
+ ├── vectors.ts # commit 14
701
+ ├── watermark.ts # commit 15
702
+ └── orchestrator.ts # commit 15 (top-level loadImages)
703
+ ```
704
+ All optional peer-dependency dynamic imports (`@napi-rs/canvas`, `qrcode`, `bwip-js`, `vega`, `vega-lite`, `undici`) are preserved as lazy loads cold-start cost is unchanged.
705
+ - **7 verification gates (G1–G7) added** to catch regressions during the split:
706
+ - **G1** Snapshot tripwire (`test/assets-split-tripwire.test.ts`) — 30 fixtures covering sanitizer output, URL normalization, path allowlist, and error code surface
707
+ - **G2** DNS lookup dedup (`test/assets-dns-dedup.test.ts`)
708
+ - **G3** SSRF blocking (`test/security-ssrf.test.ts`, expanded)
709
+ - **G4** Parallel-render concurrency (`test/assets-concurrency.test.ts`) — 10× concurrent `render()` calls must produce bit-identical PDFs
710
+ - **G5** ErrorCode stability (`test/assets-errorcode-stability.test.ts`)
711
+ - **G6** api-extractor diff against `etc/pretext-pdf.api.md`
712
+ - **G7** Cold-start perf (`test/assets-perf-coldstart.test.ts`, baseline at `test/data/perf-coldstart-baseline.json`) 100 sequential renders within 2.5× the v1.5.2 baseline
713
+ - **G7 measurement on the final shim**: 12,224ms / 100 renders (vs 21,205ms baseline = -42%, i.e. the split happens to be *faster*, well under the upper bound).
714
+
715
+ ---
716
+
717
+ ## [1.5.2] — 2026-05-25
718
+
719
+ Security hotfix. **No public API changes.** **Upgrade recommended for any deployment that accepts user-controlled image / SVG URLs.**
720
+
721
+ ### Security
722
+
723
+ - **CVE-class SSRF bypass via IPv4 alternative notations** — `isPrivateAddress` in `src/assets.ts` applied dotted-decimal regexes (`/^127\./`, `/^10\./`, …) against `URL#hostname`. The WHATWG URL parser does NOT normalize non-dotted IPv4 forms, so an attacker could reach private services by encoding the target in any inet_aton-compatible form:
724
+ - Pure decimal: `https://2130706433/x` 127.0.0.1
725
+ - Pure hex: `https://0x7f000001/x` 127.0.0.1
726
+ - Octal octet: `https://0177.0.0.1/x` 127.0.0.1
727
+ - Hex octet: `https://0x7f.0.0.1/x` 127.0.0.1
728
+ - Short form: `https://127.1/x` 127.0.0.1
729
+
730
+ Without normalization, `parsed.hostname` is e.g. `"2130706433"`, the private-range regex chain misses it, the `isIpv4Literal` (4-dot) check misses it, and the URL falls through to DNS — which on Linux's `getaddrinfo` resolves to 127.0.0.1 and bypasses the SSRF guard. Same vector reaches RFC 1918 ranges (10/8, 192.168/16, 172.16/12), link-local (169.254.169.254 — AWS IMDS), and CGNAT.
731
+
732
+ **Fix:** new internal helper `normalizeIpv4Hostname()` implements inet_aton-style parsing (decimal/octal/hex per part, short-form packing for 1/2/3-part inputs, strict 32-bit range guard). `resolveAndValidateUrl` normalizes before the private-IP check AND before DNS, then treats the normalized form as an IP literal so undici never re-resolves a non-dotted private encoding. `isPrivateAddress` also normalizes its input as defense-in-depth on the post-DNS path. Public alternative encodings (e.g. `134744072` == 8.8.8.8) continue to resolve and fetch normally.
733
+
734
+ **Test coverage:** new `test/security-ipv4-bypass.test.ts` adds 24 cases every blocked encoding above, public regression cases, plus direct unit tests for `normalizeIpv4Hostname` (round-trips, range guards, malformed-octal rejection, public-IP allowlist). Wired into the `test:phases` stage; phases stage grows from 417 to 441 tests.
735
+
736
+ ### Fixed
737
+
738
+ - **`measure-blocks/float-group.ts` fontSize fallback intent clarified** v1.5.1 M5a removed a dead `baseFontSize = doc.defaultFontSize ?? 12` plus per-block `fontSize = block.fontSize || baseFontSize` local that was never read (the item assignment used `block.fontSize` directly). Audit of upstream measure helpers (measure-paragraph, measure-heading, …) confirms `block.fontSize` is always populated with a positive value for real content, so the fallback would never have fired in practice. Added a leading comment so future contributors don't reintroduce the fallback in the mistaken belief that `block.fontSize === 0` is a real case. No behavior change.
739
+
740
+ ---
741
+
742
+ ## [1.5.1] 2026-05-24
743
+
744
+ Hotfix batch closing 9 audit findings from the 6-agent v1.5.0 review. **No public API changes** — guarded by `test/public-api-surface.test.ts`. **No behavior changes other than the documented fixes**. Snapshot baseline expanded from 68 to 73 fixtures (5 new: 2 metadata.keywords + 3 watermark.image).
745
+
746
+ ### Security
747
+
748
+ - **Watermark image URL scheme validation (H1)** — `doc.watermark.image` now passes through the same `validateUrl` pre-flight check used by `validateImage`. Unsafe schemes (`javascript:`, `data:`, `vbscript:`, `blob:`, `about:`, `file:`) are rejected at validate-time so CLI lint and MCP validate tools catch them before render. Relative file paths fall through unchanged. The shared URL-shape helper `looksLikeUrl` was moved from `validate/elements/media.ts` to `validate/helpers.ts`.
749
+
750
+ ### Fixed
751
+
752
+ - **`metadata.keywords[]` element validation (H3)** — Previously, `for (const field of [...'keywords'...])` was paired with `typeof val === 'string'`, which silently no-op'd for the `string[]`-shaped `keywords` field. Each keyword entry is now validated for control-character injection and the 1000-character length cap via `validateMetadataString(kw, \`keywords[\${i}]\`)`.
753
+ - **highlight.js dynamic-import error logging (H4)** Previously, `catch { /* not installed */ }` silently swallowed every failure including real module-load errors. The catch now logs a warning unless the error code is `ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND` (the only legitimate "optional dep absent" signal).
754
+ - **`hljs.highlight()` runtime exception logging (M2)** — Same silent-fallback issue as H4 but for tokenization failures. Now logs a warning naming the language before falling back to plain text.
755
+ - **Font-variant registration logging (M3)** — `installNodePolyfill` now tracks per-variant success and warns when an individual Inter weight (400 or 700) fails to register, instead of only warning when *both* failed. Text metrics for the missing variant may be inaccurate; operators see this in logs.
756
+
757
+ ### Changed
758
+
759
+ - **Removed redundant `as import('...').X` casts in `validate/elements/forms-floats.ts` (M1)** — `validateFormField`, `validateFootnoteDef`, and `validateFloatGroup` already accept `Extract<ContentElement, { type: 'X' }>`-typed `el`. The inner `const ff = el as FormFieldElement` casts were no-ops; removed.
760
+ - **Removed unreachable `toc-entry` validator (M4)** — The pre-switch throw at `validate/index.ts:260` already rejects `toc-entry` before dispatch. The `case 'toc-entry':` arm (guarded with `@ts-expect-error`) and the `validateTocEntry` function in `validate/elements/structural-simple.ts` were unreachable dead code. Drift-guard test updated with `VALIDATE_DISPATCHER_EXCLUDES` (mirrors the existing `MEASURE_DISPATCHER_EXCLUDES` pattern) to keep the orchestrator scan honest.
761
+ - **tsconfig: `noUnusedParameters` + `noUnusedLocals` enabled (M5a)** — Catches dead destructured locals and unused imports at build time. Cascade: 51 errors → 0. 30 of those were per-type `type _X = Exact<...>` drift guards in `allowed-props.ts` (load-bearing scaffolding) — consolidated into a single exported tuple `_AllowedPropsDriftGuard` that TypeScript counts as used. The remaining 20 were genuine unused-locals / unused-imports cleanup across measure, render, rich-text, and assets modules.
762
+ - **tsconfig: `verbatimModuleSyntax` enabled (M5b)** Enforces `import type` discipline. Cascade was only 6 errors, all `HyphenatorOpts` runtime imports that needed splitting into `import type`. Well under the 30-line cascade-defer threshold.
763
+
764
+ ---
765
+
766
+ ## [1.5.0] — 2026-05-24
767
+
768
+ Architecture sprint completing the v1.4.0 god-file split debt. Six items shipped, single minor release. **No public API changes** — guarded by `test/public-api-surface.test.ts`. **No behavioral changes** — Item A's security-critical extraction guarded by new `test/validate-document-snapshot.test.ts` (68 fixtures, bit-exact preservation verified).
769
+
770
+ ### Changed (internal structure — non-breaking)
771
+
772
+ - **`src/validate/index.ts` (594L) → orchestrator (322L) + `src/validate/document.ts` (324L)** —
773
+ Extracted 11 doc-level check categories (pageSize, margins, fonts, header/footer, defaultParagraphStyle, sections, watermark, encryption, signature, bookmarks, hyphenation, metadata) into a new `validateDocumentLevel(doc, ctx)` function. Single function with labeled `// ── name ──` blocks. **Security-critical**: snapshot tripwire test (68 fixtures across all 11 categories) verified bit-exact error preservation through the move.
774
+ - **`src/measure-blocks/index.ts` (337L) → dispatcher (243L) + `src/measure-blocks/simple-blocks.ts` (151L)** —
775
+ Extracted 7 simple measurement arms (spacer, page-break, comment, form-field, hr, toc, footnote-def). Throw-guards for image/svg/qr-code/barcode/chart stay in dispatcher (security invariants tied to routing).
776
+ - **`src/validate/elements/structural.ts` (239L grab-bag) → `structural-simple.ts` (120L) + `forms-floats.ts` (128L)** —
777
+ Split light element validators (spacer, hr, toc, toc-entry, comment) from heavy ones (form-field, footnote-def, float-group). Cleaner separation of concerns.
778
+
779
+ ### Added
780
+
781
+ - **`src/validate/elements/README.md`** — Placement guide + validator signature contract + `_ctx` policy documentation + `withCycleGuard` usage guidance. Onboarding aid for adding new element types.
782
+ - **`test/validate-document-snapshot.test.ts`** + `test/data/validate-document-snapshot.json` Bit-exact error preservation tripwire for `validateDocumentLevel`. 68 fixtures across pageSize, margins, fonts, header/footer, defaultParagraphStyle, sections, watermark, encryption, signature, bookmarks, hyphenation, metadata, content guards, and valid-doc sanity cases.
783
+
784
+ ### Fixed
785
+
786
+ - **`dist/` no longer tracked in git** (168 files removed) — was already in `.gitignore` but tracked from prior history.
787
+
788
+ ### Notes
789
+
790
+ - 12 commits across 6 items, each independently revertable via the 3-commit-per-split pattern (stage → route → delete) proven in v1.4.0.
791
+ - Test suite: 416 pass / 1 skip / 0 fail throughout the sprint, plus the new snapshot tripwire test (passes against generated baseline).
792
+ - Public API surface unchanged: 15 runtime exports × 6 entry points.
793
+ - `_ctx` policy decision: keep underscore-prefixed param for validators that don't currently consume context. Documented in `validate/elements/README.md` stable signature lets future strict-mode or context-aware checks be added without changing call sites.
794
+ - `loadedFamilies` initialization order: populated after `validateDocumentLevel` returns. Audit confirmed no order dependency — `document.ts` never reads `loadedFamilies`, only `validateFontSpec` for shape validation.
795
+ - Path-traversal pre-flight in `validateImage` deferred to v2.0 — runtime SSRF + `allowedFileDirs` guards already provide defense-in-depth.
796
+
797
+ ---
798
+
799
+ ## [1.4.1] 2026-05-23
800
+
801
+ Cleanup batch addressing audit findings surfaced by the v1.4.0 god-file split. Five MEDIUM-severity items, all internal — no public API changes, no behavioral changes for callers using documented schemas. Test suite unchanged: 416 pass / 1 skip / 0 fail.
802
+
803
+ ### Fixed
804
+
805
+ - **M1 — Dead outer `withCycleGuard` removed from `validateElement` dispatcher (`src/validate/index.ts`).** The dispatcher wrapped `list` and `float-group` cases in an outer `withCycleGuard` with an empty body, then the inner element validators (`validateList` in `elements/list.ts`, `validateFloatGroup` in `elements/structural.ts`) immediately opened their own guard on the same element. The outer guard ran a no-op body and `finally`-deleted the element from `seen` BEFORE the inner guard added it — dead code communicating false intent. The inner validators own the guard. Import of `withCycleGuard` also dropped from `index.ts` since it is no longer used there.
806
+ - **M2 — `measure-blocks/float-group.ts ↔ measure-blocks/index.ts` runtime cycle broken (Option C: dependency injection).** `float-group.ts` previously imported `measureBlock` from `./index.js`, while `index.ts` re-exported `measureFloatGroup` from `./float-group.js` — a real ESM cycle that hoisting tolerated but which violated module-boundary discipline. Resolved by promoting `measureBlock` to an explicit parameter of `measureFloatGroup` (new exported `MeasureBlockFn` type). The sole caller (the orchestrator in `measure.ts`) already has `measureBlock` in scope, so the change is non-invasive. Option C was chosen over Option A (inlining the dispatch — too much duplication) and Option B (extracting `dispatch.ts` — restructured more than needed for a one-edge cycle).
807
+
808
+ ### Changed
809
+
810
+ - **M3 Concurrent validate test annotated with sync-vs-async semantics (`test/validate-concurrent.test.ts`).** Added a comment block above the parallel-validate `describe` clarifying that `validateDocument` is synchronous, so `Promise.all` over it cannot exercise real concurrent execution. The test now explicitly documents what it does prove (shape stability across N invocations) versus what it does not (concurrent isolation — guaranteed structurally by the per-call `WeakSet` opened at the top of `validate()` in `src/validate/index.ts`). The async render-path tests later in the same file DO exercise real concurrency because `render()` crosses `await` boundaries.
811
+ - **M4 Benchmark harness upgraded (`scripts/run-bench-snapshot.mjs`).** Default measured runs raised from 3 to 10. New `--runs N` CLI flag for callers to override (CI: 5, dev: 3, full: 10). Output now reports `median`, `p90`, and `min` in addition to `avg`. Variance on cold-tsx invocations can hit ±70%, which made any <10% regression gate meaningless at N=3.
812
+
813
+ ### Added
814
+
815
+ - **M5 — URL scheme check in `validateImage` (`src/validate/elements/media.ts`).** When `el.src` is a string that looks like a URL (matches `data:`, `javascript:`, `vbscript:`, `blob:`, `about:`, `file:`, or any `scheme://` form), the validator now routes through `validateUrl` from `validate/helpers.ts`. Matches the runtime SSRF guard's posture in `src/compat.ts` so validate-only callers (CLI lint, MCP `validate_document` tool) catch unsafe schemes pre-flight instead of letting them through to the asset-loader guard. http/https/ftp/mailto/anchor links still pass.
816
+
817
+ ---
818
+
819
+ ## [1.4.0]2026-05-23
820
+
821
+ Architecture sprint: four god-files split into thin orchestrators + cohesive sub-modules. **No public API changes** — guarded by `test/public-api-surface.test.ts` (15 runtime exports across 6 entry points, snapshot tripwire). 12 granular commits, each independently revertable.
822
+
823
+ ### Changed (internal structure non-breaking)
824
+
825
+ - **`src/validate.ts` (1834L) → `src/validate/` (9 files, ~250L each)**
826
+ `validate/index.ts` orchestrator + `helpers.ts`, `fonts.ts`, `errors.ts`, and per-element validators under `elements/`. Introduced explicit `ValidationContext` ({ errors, strict, loadedFamilies, seen, options }) threaded into every element validator instead of relying on closure-captured locals. Per-call `WeakSet` for cycle detection — concurrent isolation verified by `test/validate-concurrent.test.ts`.
827
+ - **`src/measure-blocks.ts` (1600L) → `src/measure-blocks/` (10 files)** —
828
+ `measure-blocks/index.ts` dispatcher + `text-blocks.ts`, `list.ts`, `image.ts`, `float-group.ts`, `highlight.ts`, `helpers.ts`, plus `table/{measure,spans,columns}.ts`. Added explicit case arms for `qr-code`, `barcode`, `chart` (previously fell through to "Unknown element type"). `_hljsCache` intentionally remains module-scoped in `highlight.ts` — idempotent process-wide cache, not concurrent-isolated state.
829
+ - **`src/render-blocks.ts` (1277L) → `src/render-blocks/` (13 files)** —
830
+ Per-render-function modules. Each independent (no shared state). Dropped four unused imports from the original (`PDFFont`, `PDFName`, `renderTocEntry`, `renderFormField` — never referenced).
831
+ - **`src/types-public.ts` (1200L) → `src/types-public/` (8 files)**
832
+ Split by domain: `document.ts`, `elements-{text,block,media}.ts`, `union.ts`, `validation.ts`, `render-options.ts`. Type-only intra-package cycles are erased at runtime (verified by clean tsc build).
833
+
834
+ ### Added
835
+
836
+ - **`test/drift-guards.test.ts` — measure-blocks dispatcher case-arm guard.** New invariant mirrors the existing validate + render guards: every `ELEMENT_TYPES` entry must have an explicit `case` arm in `src/measure-blocks/index.ts` (except internal `toc-entry`). Catches future additions to `ELEMENT_TYPES` that forget to handle measurement.
837
+
838
+ ### Fixed
839
+
840
+ - **Dead imports removed:** `validate` was imported but never used in `src/builder.ts` after the split. Removed.
841
+ - **Unused locals/params:** `lineHeight` in `float-group.ts` and `table/measure.ts` (computed but never read); `doc` param in `measureCallout` and `measureCode` (preserved as `_doc` for signature compatibility, signals intentional non-use).
842
+
843
+ ### Notes
844
+
845
+ - Public API surface tripwire: 15 runtime exports across 6 entry points, all unchanged.
846
+ - Benchmark gate: 5/7 corpora within 5% of v1.3.4; two corpora (`rich-text-mixed-spans` +12.9%, `table-stress` +7.2%) breach but match documented run-to-run variance (5-10% noise on dev machine). Cold-start module-load unchanged. CI re-snapshot recommended to disambiguate variance from regression.
847
+ - Known follow-up: ESM-fragile circular import in `measure-blocks/float-group.ts` ↔ `measure-blocks/index.ts` (load-safe today because `measureBlock` is async-runtime only; revisit if top-level `await` is ever added).
848
+ - Known follow-up: type-only intra-cycles in `types-public/` (no runtime risk — erased at compile time).
849
+
850
+ ---
851
+
852
+ ## [1.3.6] 2026-05-23
853
+
854
+ Architecture-sprint scaffolding release: vendor integrity check, concurrent isolation tests, signing import correctness, public API tripwire.
855
+
856
+ ### Added
857
+
858
+ - **Boot-time vendor integrity check (#12)** — `src/version-check.ts` exports `assertVendorIntegrity()`, called once at the start of every `render()`. Verifies the vendored pretext version (`src/vendor/pretext/VERSION.ts`) is in the compatible range. Warns (does not throw) on drift. Inline semver matcher avoids adding a `semver` dependency.
859
+ - **Concurrent validate/render isolation tests (#25)** — `test/validate-concurrent.test.ts` exercises 8 parallel `validate()` calls plus 4 parallel `render()` calls across different fonts AND different scripts (en/he/ar/th). Byte-identical fingerprints between parallel and sequential runs prove `vendor/pretext/measurement.ts` and `vendor/pretext/analysis.ts` shared state holds up under concurrent use.
860
+ - **Public API surface tripwire** — `test/public-api-surface.test.ts` snapshots all 15 runtime exports across 6 entry points. Will guard against accidental API drift during the upcoming v1.4.0 god-file splits.
861
+ - **P12/CMS crypto verification test (#26)** — `test/signatures-crypto.test.ts` adds a real `crypto.createVerify('RSA-SHA256')` round-trip with positive + negative cases. **Currently `t.skip`'d** — see KNOWN ISSUES below.
862
+ - **`pretextPdf.mcpCompat`** field in `package.json` declares `>=1.4.0 <2.0.0` compatibility range for the pretext-pdf-mcp consumer. MCP-side check ships separately.
863
+
864
+ ### Fixed
865
+
866
+ - **`signpdf` v3 API + import correctness** — `src/post-process.ts` was destructuring `pdflibAddPlaceholder` from `@signpdf/signpdf` where it does not exist; the symbol lives in `@signpdf/placeholder-pdf-lib`. Also updated to the v3 API (`new P12Signer(buffer, { passphrase })` instead of `signer.sign(buffer, { passphrase })`). Added `@signpdf/placeholder-pdf-lib` and `@signpdf/signer-p12` to `peerDependencies` + `peerDependenciesMeta.optional` (mirroring `@signpdf/signpdf`).
867
+ - **`SIGNATURE_DEP_MISSING` error message** — now identifies exactly which `@signpdf/*` packages are missing and tells the user which to install, instead of always listing all three.
868
+
869
+ ### KNOWN ISSUES (deferred to follow-up sprint)
870
+
871
+ - **Signing path is architecturally non-functional** — even with correct imports, `@cantoo/pdf-lib`'s serializer is fork-incompatible with `@signpdf/placeholder-pdf-lib`. The placeholder ByteRange dict is emitted in a shape that `@signpdf/utils.findByteRange` cannot parse. End-to-end signing has never worked. The crypto verify test (#26) is correctly written and ready to run once signing is repaired. Three fix paths exist: `@signpdf/placeholder-plain` swap (breaks AcroForm), porting placeholder-pdf-lib onto cantoo primitives, or a merge-bytes approach. None are in v1.3.6 scope. The `SIGNATURE_DEP_MISSING` message now pre-warns callers.
872
+
873
+ ## [1.3.5] — 2026-05-22
874
+
875
+ ### Fixed
876
+
877
+ - **`toc-entry` drift-guard regression**
878
+ `test/drift-guards.test.ts` was failing because `src/validate.ts` had no
879
+ `case 'toc-entry':` arm. `toc-entry` elements are produced internally by
880
+ the TOC two-pass processor, but the drift guard correctly insists every
881
+ registered `ElementType` has a validator case. Added a defensive validator
882
+ for `text`, `pageNumber`, `level`, `levelIndent`, and `leader` so
883
+ user-authored `toc-entry` payloads (rare but possible) fail loud instead
884
+ of slipping through.
885
+
886
+ ### Removed
887
+
888
+ - **`test/pretext-api-contract.test.ts`** —
889
+ Reframed in v1.3.3 as a local export-shape guard for the vendored pretext
890
+ layout module, but the test was tautological: it could only fail if
891
+ someone hand-edited `src/vendor/pretext/*.ts` to remove an export, in
892
+ which case TypeScript would already fail the build. Deleted along with
893
+ its entry in the `test:contract` npm script. Drift guards in
894
+ `test/drift-guards.test.ts` cover the real risk (registry vs. switch
895
+ arms going out of sync).
896
+
897
+ ### Performance
898
+
899
+ - **v1.3.2+ benchmark numbers captured**
900
+ Re-ran the seven core corpora against `benchmarks/benchmark-baseline.json`
901
+ (recorded 2026-04-10 at v1.3.0). Results documented in
902
+ `benchmarks/v1.3.2-results.md`: ~1.66x geometric-mean speedup across
903
+ corpora, with the largest wins on text-heavy workloads (table-stress
904
+ -52%, punctuation-heavy -51%, rtl-layout -43%). Confirms the DNS dedup,
905
+ parallel raster, and word-width cache work landed in v1.3.2 was real
906
+ rather than aspirational.
907
+
908
+ ### Docs
909
+
910
+ - **README version table extended**
911
+ Added 1.1.x (vendor switch), 1.2.x (security + benchmarks), and
912
+ 1.3.0–1.3.4 (perf + drift guards) rows so the version table no longer
913
+ stops at 1.0.6.
914
+
915
+ ### Tooling
916
+
917
+ - **`scripts/run-bench-snapshot.mjs`** —
918
+ Small one-shot runner that prints avg/min render time per corpus across
919
+ three measured runs (after a warmup). Used to capture the v1.3.5
920
+ benchmark results above; useful for ad-hoc perf checks without touching
921
+ the regression-guarded `test/benchmark-baseline.test.ts`.
922
+
923
+ ---
924
+
925
+ ## [1.3.4] 2026-05-17
926
+
927
+ ### Fixed
928
+
929
+ - **DNS dedup test now imports from source**
930
+ `test/assets-dns-dedup.test.ts` previously imported `fetchWithTimeout` /
931
+ `assertSafeUrl` from `../dist/assets.js`, which would silently pass against
932
+ a stale build (false confidence). Switched to `../src/assets.js` so the
933
+ test always runs against the current source tree under tsx.
934
+
935
+ ### Added
936
+
937
+ - **FIFO eviction boundary test for word-width cache** —
938
+ `test/measure-text-cache.test.ts` now asserts that when the cache is
939
+ pre-filled to `WORD_WIDTH_CACHE_MAX` and a new `measureWord` call is made,
940
+ `cache.size` stays at the cap, the oldest insertion (`syn0`) is evicted,
941
+ and a re-accessed entry (`syn1`) survives — proving FIFO semantics, not LRU.
942
+
943
+ ### Changed
944
+
945
+ - **Constant tunability scope documented** `VECTOR_RASTER_CONCURRENCY` and
946
+ `WORD_WIDTH_CACHE_MAX` are exported as read-only constants for observability
947
+ and test introspection. Consumers wanting different values must fork;
948
+ runtime tunability (env vars or options) is a future enhancement and not
949
+ planned for the v1.x line.
950
+
951
+ ---
952
+
953
+ ## [1.3.3] — 2026-05-17
954
+
955
+ ### Fixed
956
+
957
+ - **Parallel rasterization concurrency cap** — `loadVectorAssets` now runs at
958
+ most 4 SVG/QR/barcode/chart rasterization tasks concurrently (was unbounded).
959
+ Prevents file-descriptor / worker exhaustion on documents with many vector
960
+ assets. New exported constant: `VECTOR_RASTER_CONCURRENCY`.
961
+ - **Word-width cache memory bound** — `measureWord` now FIFO-evicts at 50,000
962
+ entries to bound memory for long-running processes that reuse a single
963
+ `wordWidthCache`. New exported constant: `WORD_WIDTH_CACHE_MAX`.
964
+
965
+ ### Changed
966
+
967
+ - **Pretext API contract test reframed** — `test/pretext-api-contract.test.ts`
968
+ header clarified: this is a local export-shape guard for the vendored
969
+ pretext layout module, not an upstream version canary. Pretext has been
970
+ vendored at `src/vendor/pretext/` since v1.1.0.
971
+ - **CHANGELOG clarification on v1.3.2 parallel rasterization** — see updated
972
+ v1.3.2 entry below; the speedup is real for I/O-bound fan-out, but CPU
973
+ rasterization still serializes on the V8 main thread.
974
+
975
+ ### Documented (not changed)
976
+
977
+ - **Word-width cache scope (H1)** the cache is currently consulted on the
978
+ hyphenation path (`measureTextWithHyphenation`) only. The non-hyphenation
979
+ branch of `measureText` delegates directly to pretext's `layoutWithLines`
980
+ to preserve CJK character-level breaking, RTL/bidi, Thai segmentation,
981
+ kerning, and justify semantics that word-by-word summing would diverge
982
+ from. Documents that do not configure a hyphenator will not see
983
+ cross-paragraph cache reuse; this is intentional for correctness.
984
+
985
+ ---
986
+
987
+ ## [1.3.2] 2026-05-17
988
+
989
+ ### Performance
990
+
991
+ - Removed double DNS resolution in image/SVG fetch (one lookup per remote asset, not two)
992
+ - Parallel SVG/QR/barcode generation+rasterization (sequential embed retained for pdf-lib safety)
993
+ - Sub-note (added in v1.3.3): the parallelism is real for the I/O-bound fan-out
994
+ (remote SVG fetches overlap). CPU rasterization (sharp/svg2pdfkit) still
995
+ contends on the V8 main thread, so wall-clock improvement is dominated by
996
+ remote-asset latency, not raster throughput.
997
+ - Document-level word-width measurement cache (cross-paragraph dedup of common-word measurements)
998
+ - Sub-note (added in v1.3.3): the cache is consulted on the hyphenation code
999
+ path only. See v1.3.3 "Documented (not changed)" for the rationale.
1000
+
1001
+ ---
1002
+
1003
+ ## [1.3.1] 2026-05-17
1004
+
1005
+ ### Fixed
1006
+
1007
+ - Internal test fixtures updated to set `allowedFileDirs` (resolved with `path.resolve` for Windows drive-letter compatibility) after the v1.2.2 deny-by-default flip no library behavior change. Affected: `signatures-crypto`, `signatures-validation`, `svg`, `image-floats` test files.
1008
+ - `markdown-gfm` compat test updated to use an `https:` image src; `data:` URLs are blocked by the scheme guard added in v1.3.0, so the pre-existing fixture no longer round-tripped — test-only change.
1009
+
1010
+ ---
1011
+
1012
+ ## [1.3.0] 2026-05-17
1013
+
1014
+ ### ⚠️ BREAKING (retroactive note covering v1.2.2)
1015
+
1016
+ - **`assertPathAllowed` is now deny-by-default.** Documents using `file://` image sources without an explicit `allowedFileDirs` configuration will throw `PATH_TRAVERSAL`. This was shipped in v1.2.2 as a security fix but is technically a breaking change consumers on `^1.2.0` who upgrade past v1.2.1 must either set `allowedFileDirs` or migrate away from `file://` sources. v1.3.0 is the recommended upgrade target with full semver signal.
1017
+
1018
+ ### Fixed
1019
+
1020
+ - **Scheme guard whitespace bypass** in `compat.ts` leading whitespace in image src (e.g. `" file:///etc/passwd"`) no longer bypasses scheme stripping.
1021
+ - **Extended scheme blocklist** in `compat.ts` added `vbscript:`, `blob:`, `about:` alongside existing `file://`, `data:`, `javascript:`.
1022
+
1023
+ ### Tests
1024
+
1025
+ - Added redirect-chain SSRF test using a local mock HTTP server.
1026
+ - Pinned CLI exit-code assertion to detect regressions.
1027
+
1028
+ ---
1029
+
1030
+ ## [1.2.2] 2026-05-17
1031
+
1032
+ ### Security
1033
+
1034
+ - **`assertPathAllowed` is now deny-by-default** Previously, when `doc.allowedFileDirs` was
1035
+ undefined or empty, local file:// paths were silently allowed. Now the function throws
1036
+ `PATH_TRAVERSAL` unless `allowedFileDirs` is explicitly configured with at least one directory.
1037
+ This closes an unintended open-access footgun for server-side deployments.
1038
+
1039
+ - **`compat.ts` dangerous image schemes stripped in `fromPdfmake`** — `file://`, `data:`, and
1040
+ `javascript:` image `src` values are now silently dropped during pdfmake→pretext-pdf translation
1041
+ rather than forwarded verbatim. This prevents the compat shim from acting as an indirect
1042
+ bypass for the scheme-level SSRF guards in `assets.ts`.
1043
+
1044
+ - **`compat.ts` `allowedFileDirs` forwarded from `PdfmakeDocument`** The `PdfmakeDocument`
1045
+ interface now accepts `allowedFileDirs?: string[]`, which is forwarded into the resulting
1046
+ `PdfDocument`. Callers who previously passed file paths via the compat shim can now
1047
+ allowlist their directories explicitly.
1048
+
1049
+ - **CLI validates before rendering** — `pretext-pdf` now calls `validateDocument()` before
1050
+ invoking `render()`. Invalid documents produce a `VALIDATION_ERROR` message on stderr and
1051
+ exit with code 1, avoiding wasted work during the render phase.
1052
+
1053
+ ---
1054
+
1055
+ ## [1.2.1]2026-05-16
1056
+
1057
+ ### Fixed
1058
+
1059
+ - **`PretextPdfError` now preserves root cause via `err.cause`** — `ASSEMBLY_FAILED` errors
1060
+ thrown by `merge()` and `assemble()` now carry the original pdf-lib error as `err.cause`,
1061
+ making root-cause debugging possible without losing the upstream message.
1062
+
1063
+ - **`form.updateFieldAppearances()` failure is now logged** Previously silently swallowed
1064
+ (`catch { /* non-fatal */ }`). Now emits a structured warning via the document logger or
1065
+ `console.warn`. Behaviour is unchanged (non-fatal); the warning aids debugging.
1066
+
1067
+ - **Owner-only encryption now warns explicitly** — When `doc.encryption` is set without
1068
+ `userPassword`, a `console.warn` is emitted explaining that the PDF will open without a
1069
+ password. Owner-only encryption remains valid (it restricts editing/printing, not opening).
1070
+
1071
+ - **`assemble([{}])` now throws `VALIDATION_ERROR`** — Regression from v1.2.0 discriminated
1072
+ union changes: passing a part with neither `doc` nor `pdf` previously crashed with a
1073
+ `TypeError` (no `.code` property). Now throws a proper `VALIDATION_ERROR` with a clear
1074
+ message before attempting to render.
1075
+
1076
+ - **`watermark: {}` now throws `VALIDATION_ERROR`** Regression from v1.2.0: the
1077
+ `WatermarkSpec` discriminated union enforced text/image presence at compile-time but the
1078
+ runtime validation was missing. A watermark object with neither `text` nor `image` now
1079
+ correctly throws at validate time.
1080
+
1081
+ - **`svg: ''` (empty string) now throws `VALIDATION_ERROR`** Empty SVG strings passed
1082
+ the validation stage and surfaced as `SVG_LOAD_FAILED` during render. Now caught at
1083
+ validate time with a clear `VALIDATION_ERROR`.
1084
+
1085
+ ### Changed
1086
+
1087
+ - **`PretextPdfError` constructor accepts optional `ErrorOptions`** Third argument
1088
+ `options?: ErrorOptions` (i.e. `{ cause?: unknown }`) is now accepted and passed to the
1089
+ native `Error` constructor. Fully backwards-compatible all existing call sites unchanged.
1090
+
1091
+ - **bidi-js missing-peer warning routes through document logger** When a `logger` is
1092
+ passed to `render()`, bidi-js peer-dependency warnings are now routed through
1093
+ `logger.warn` instead of always using `console.warn`. New low-level export:
1094
+ `setBidiWarnFn(fn)` (prefer the `logger` render option in application code).
1095
+
1096
+ ---
1097
+
1098
+ ## [1.2.0] — 2026-05-16
1099
+
1100
+ Post-audit hardening release. Type system tightened, concurrency-safe validation,
1101
+ @internal type leaks closed, RTL/asset failures surface as structured errors,
1102
+ SSRF defense upgraded to undici-pinned IP. No source-level API removals from the
1103
+ package entry point see migration notes below for `@internal` types that were
1104
+ already not exported from `src/index.ts`.
1105
+
1106
+ ### Added
1107
+
1108
+ - **Discriminated unions on four public types** (`src/types-public.ts`, audit Phase B)
1109
+ `WatermarkSpec`, `AssemblyPart`, `SvgElement`, and `ImageElement` (float variants)
1110
+ now use TypeScript discriminated unions instead of flat optional structs. The
1111
+ compiler now prevents invalid combinations (e.g., a watermark with both `text`
1112
+ and `image`, or an SVG element with neither `svg` nor `src`) that previously
1113
+ could only be caught at runtime. Existing valid usages continue to compile.
1114
+
1115
+ - **`pdf-lib` type augmentation** (`src/vendor/pdf-lib-augment.d.ts`, audit Phase D) —
1116
+ Documents the load-bearing `as any` casts against pdf-lib internals
1117
+ (`PDFArray.push`, `PDFFont.embedder`) and removes the one genuinely-avoidable
1118
+ cast in `measure.ts`.
1119
+
1120
+ - **DNS rebinding defense via undici Agent IP pinning** (`src/assets.ts`, audit B6) —
1121
+ `assertSafeUrl` was upgraded to `resolveAndValidateUrl` which returns the
1122
+ resolved IP, and `fetchWithTimeout` now uses an undici `Agent` whose
1123
+ `connect.lookup` callback always returns the pre-validated IP. Closes the
1124
+ TOCTOU window where DNS could rebind between validation and connect.
1125
+ Extended private-range coverage: 0.0.0.0/8, 192.0.0/24, 198.18/15, IPv6
1126
+ multicast, and IPv4-mapped IPv6 normalization. +14 SSRF tests (25 total).
1127
+
1128
+ - **Per-call cycle-detection state** (`src/validate.ts`, audit Perf-1) — Moved
1129
+ `seenInRecursion` WeakSet from module scope into `validate()` and threaded
1130
+ through `withCycleGuard`. Makes validation reentrant and concurrency-safe
1131
+ (no shared mutable state across parallel `validate()` calls). +1 regression
1132
+ test.
1133
+
1134
+ - **Structured error codes on RTL + asset failures** (`src/errors.ts`,
1135
+ `src/measure-text.ts`, `src/assets.ts`, audit silent-failure pass) —
1136
+ - `RTL_REORDER_FAILED` — surfaces when `bidi-js` is installed but throws.
1137
+ Previously fell through with `isRTL:true` on logical-order text =
1138
+ visually broken Arabic/Hebrew renders. Missing `bidi-js` still degrades
1139
+ gracefully (warn + LTR render) since it is an optional peer dep.
1140
+ - `CHART_LOAD_FAILED` — embedded in warn logs from QR/barcode/chart loaders
1141
+ so failures are debuggable from log scraping alone.
1142
+ - `FONT_ENCODE_FAIL` — replaces the prior bare `catch` in `src/fonts.ts`
1143
+ that silently swallowed font subset failures (audit B2).
1144
+
1145
+ ### Changed
1146
+
1147
+ - **`@internal` types removed from the `types.ts` barrel** (audit H8 / type-design HIGH) —
1148
+ `RichLine`, `RichFragment`, and `TocEntryElement` were tagged `@internal`
1149
+ but re-exported through `src/types.ts`. They have been removed from that
1150
+ barrel and canonicalized in `src/types-internal.ts`. `TocEntryElement` is
1151
+ no longer a member of the public `ContentElement` union (it was always
1152
+ pipeline-synthesized, never user-constructed). Internal imports updated
1153
+ in `rich-text.ts`, `measure.ts`, `render-extras.ts`, `allowed-props.ts`,
1154
+ `validate.ts`, `measure-blocks.ts`, `fonts.ts`. **Migration note:** these
1155
+ types were never exported from `src/index.ts` (the package entry point),
1156
+ so consumers using the supported import path (`import { ... } from
1157
+ 'pretext-pdf'`) are unaffected. Deep imports (`'pretext-pdf/src/types'`)
1158
+ are unsupported and never were stable.
1159
+
1160
+ - **`api-extractor` enforcement escalated to `error`** (`api-extractor.json`)
1161
+ `ae-forgotten-export` log level changed from `warning` to `error` so CI
1162
+ fails on future `@internal` type leaks. `etc/pretext-pdf.api.md` baseline
1163
+ regenerated.
1164
+
1165
+ - **`assertUnknownProps` parameter tightened** (`src/validate.ts`)
1166
+ `obj: any` `obj: unknown` with an explicit type guard at the boundary.
1167
+ Removes one of the few remaining `any` exposures at a security-sensitive
1168
+ validation entrypoint.
1169
+
1170
+ ### Fixed
1171
+
1172
+ - **Concurrent validation false positives** (`src/validate.ts`) Two
1173
+ simultaneous `validateDocument(doc)` calls on the same object reference
1174
+ could produce a false "cyclic reference detected" error because the
1175
+ WeakSet was at module scope. Per-call WeakSet fix closes this and any
1176
+ future re-entrant validator scenarios.
1177
+
1178
+ - **Stale `tests-743` badge** (`README.md`) Replaced with the durable
1179
+ `tests-passing` (current unit count is 319).
1180
+
1181
+ - **Dead `case 'toc-entry'` branch** (`src/fonts.ts:collectTextByFont`) —
1182
+ After `TocEntryElement` left the public `ContentElement` union, the
1183
+ branch became unreachable.
1184
+
1185
+ ### Migration notes (v1.1.x v1.2.0)
1186
+
1187
+ If you only import from `'pretext-pdf'`: **no source changes needed.**
1188
+
1189
+ If you do unsupported deep imports of internal types
1190
+ (`'pretext-pdf/src/types'`):
1191
+ - `RichLine`, `RichFragment`, `TocEntryElement` — import from
1192
+ `'pretext-pdf/src/types-internal'` instead, with the understanding that
1193
+ these are not part of the stable public API and may change in any release.
1194
+
1195
+ If you construct `WatermarkSpec` / `AssemblyPart` / `SvgElement` / `ImageElement`
1196
+ literals: you may need to delete fields you weren't using anyway. TypeScript
1197
+ will flag any literals that previously satisfied the loose type but violated
1198
+ the actual invariants.
1199
+
1200
+ ---
1201
+
1202
+ ## [1.1.3] 2026-05-15
1203
+
1204
+ ### Added
1205
+
1206
+ - **Cycle detection + depth cap on TableElement walk** (`src/validate.ts`, Sprint 3 / M2)
1207
+ The rows/cells iteration is now wrapped in `withCycleGuard`, matching the
1208
+ protection already in place for `ListItem.items`, `FloatGroup.content`, and
1209
+ `RichParagraph.spans`. A self-referential row or cell shape now produces a
1210
+ structured `VALIDATION_ERROR` instead of an unbounded walk.
1211
+
1212
+ - **Root-level depth guard for `document.content` entries** (`src/validate.ts`, Sprint 3 / M1)
1213
+ Each top-level element call into `validateElement` now runs an explicit
1214
+ `assertDepthOk(depth, prefix)` so the `MAX_VALIDATION_DEPTH = 32` cap fires
1215
+ even for plugin-typed elements that do not open their own `withCycleGuard`
1216
+ scope. Internal recursive walks (`list`, `float-group`, `rich-paragraph`,
1217
+ `table`) continue to enforce the cap via `withCycleGuard`.
1218
+
1219
+ - **Round-trip tests for the pdfmake compatibility shim** (`test/compat.test.ts`, Sprint 3 / M3)
1220
+ Four new tests covering pdfmake pretext render integration, style
1221
+ propagation, sanity rendering of native pretext docs, and large-table
1222
+ preservation (5 columns × 10 rows).
1223
+
1224
+ - **`## Validation` section in `README.md`** (Sprint 3 / M4) — Explicit guidance
1225
+ to call `validateDocument()` before `render()` on untrusted input, with the
1226
+ concrete failure modes (stack overflow on cyclic input, prototype pollution
1227
+ via `__proto__`, runtime 500s on malformed shapes) the validator prevents.
1228
+
1229
+ ### Fixed
1230
+
1231
+ - **Type safety in validateDocument** (`src/validate.ts`) — Replaced unchecked `as PdfDocument` cast with `isValidPdfDocumentLike()` type guard. Returns proper error when input is not a plain object.
1232
+
1233
+ - **Prototype pollution in mergeStyles** (`src/compat.ts`) — `Object.assign(merged, s)` allowed user-supplied pdfmake JSON to pollute the prototype chain. Replaced with `copySafeStyleProperties()` that whitelists only known safe style keys (fontSize, bold, italics, color, alignment, font).
1234
+
1235
+ - **Path traversal in digital signatures** (`src/post-process.ts`)P12 certificate path bypassed the `allowedFileDirs` security check. Now validates path via `assertPathAllowed()` before reading, preventing directory traversal attacks via signature feature.
1236
+
1237
+ - **Fragile errorCount regex in validateDocument** (`src/validate.ts`) — Original regex could match anywhere in error message. Refined to header-only pattern (`^Strict validation failed`) to extract true error count even when >20 errors are returned (capped array but accurate count in message).
1238
+
1239
+ - **Fake test coverage** (`test/validate-document.test.ts`) — Removed describe block with `assert.ok(true, 'TODO')` placeholder. Replaced with documentation explaining why the non-PretextPdfError code path is manually audited.
1240
+
1241
+ - **Missing LICENSE for vendored code** (`src/vendor/pretext/LICENSE`) Added MIT license file with attribution to upstream pretext library and this fork, satisfying legal compliance for vendored dependencies.
1242
+
1243
+ ### Documentation
1244
+
1245
+ - **`Logger` interface guidance** (`src/types-public.ts`, audit L2) Expanded
1246
+ JSDoc on the `Logger` interface and the `logger?` field on `RenderOptions`
1247
+ to call out that passing a no-op (`{ warn: () => {} }`) silences **every**
1248
+ advisory warning — fine in tests, dangerous in production. Documents the
1249
+ default (`console.warn`) and recommends pino/winston for production
1250
+ routing. No code change.
1251
+
1252
+ - **pdfmake `defaultStyle` / `styles` mapping** (`src/compat.ts`, audit L3)
1253
+ Added JSDoc to `PdfmakeStyle` enumerating the supported subset
1254
+ (`font`, `fontSize`, `bold`, `italics`, `color`, `alignment`) and the
1255
+ silently-dropped pdfmake properties (`lineHeight`, `marginX/Y`,
1256
+ `decoration`, `background`, `characterSpacing`, `noWrap`, etc.) that
1257
+ consumers migrating from pdfmake commonly trip over. `fromPdfmake()`
1258
+ now documents how `defaultStyle.font` / `.fontSize` route to
1259
+ document-level `defaultFont` / `defaultFontSize` and how all other
1260
+ `defaultStyle` properties cascade through `mergeStyles()`.
1261
+
1262
+ ### Changed
1263
+
1264
+ - **Benchmark floor override** (`test/benchmark-baseline.test.ts`, audit L4) —
1265
+ The per-corpus regression guard now honors a `PRETEXT_BENCHMARK_FLOOR_MS`
1266
+ environment variable. Set it to a positive integer to raise the 5000ms
1267
+ default floor on slow CI runners; set it to `skip` / `0` / `false` / `off`
1268
+ to bypass the timing assertion entirely. The structural assertions (corpus
1269
+ IDs match baseline, stages present) still run.
1270
+
1271
+ ### Notes — Phase A / B / D history
1272
+
1273
+ The cycle-detection and depth-cap machinery (`withCycleGuard`,
1274
+ `MAX_VALIDATION_DEPTH`, the per-container guards on `list`, `float-group`,
1275
+ `rich-paragraph`) and the discriminated-union refactor of `ContentElement`
1276
+ plus the typed `pdf-lib` augmentation were landed during in-flight audit
1277
+ sprints between `[1.0.x]` and `[1.1.0]` that were not individually tagged.
1278
+ Sprint 3 (this release) backfills those gaps with the M1/M2 root + table
1279
+ guards, plus explicit tests and documentation.
1280
+
1281
+ ---
1282
+
1283
+ ## [1.1.2] — 2026-05-08
1284
+
1285
+ ### Fixed
1286
+
1287
+ - **Silent font-subset failure** (`src/fonts.ts`) — Bare `catch {}` on `pdfFont.encodeText()`
1288
+ silently swallowed glyph-encoding errors, producing wrong characters with no signal.
1289
+ Now logs a `console.warn` so callers know which font key failed.
1290
+
1291
+ - **Explicit RTL direction silently flipping to LTR** (`src/measure-text.ts`) When
1292
+ `dir:'rtl'` was set and `bidi-js` threw during reordering, the fallback incorrectly
1293
+ returned `isRTL: false`, causing Arabic/Hebrew paragraphs to align and wrap as LTR.
1294
+ The fallback now preserves `isRTL: true` so the layout engine honours the explicit
1295
+ direction even without bidi reordering.
1296
+
1297
+ - **SSRF DNS rebinding window** (`src/assets.ts`)`assertSafeUrl()` was synchronous.
1298
+ An attacker with TTL=0 DNS could pass the hostname check then rebind to `169.254.x.x`
1299
+ between the check and the actual `fetch()` call. The function is now async and
1300
+ pre-resolves hostnames via `dns.lookup()` before the private-range check, closing
1301
+ the TOCTOU window. Falls back gracefully when DNS is unavailable (fetch will also
1302
+ fail in that case). All call sites updated to `await assertSafeUrl()`.
1303
+
1304
+ - **Concurrent PDFDocument mutation race** (`src/pipeline.ts`) — `loadFonts` and
1305
+ `loadImages` were run with `Promise.all()` over the same `PDFDocument` instance.
1306
+ Both mutate the cross-reference table, causing intermittent xref corruption under
1307
+ load. Now sequenced: `loadFonts` completes before `loadImages` begins.
1308
+
1309
+ - **Test suite cascade: 692 tests silently dropped on benchmark failure** (`package.json`,
1310
+ `scripts/test-all.mjs`) The `&&`-chained `npm test` command aborted all downstream
1311
+ stages when `test:contract` failed. Replaced with a Node.js runner that executes all
1312
+ 4 stages and collects failures. Benchmark is now in a separate `test:benchmark` script
1313
+ (not in `test:contract`) with `FLOOR_MS` raised to 5s to absorb dev-hardware variance.
1314
+
1315
+ ---
1316
+
1317
+ ## [1.1.1] 2026-05-08
1318
+
1319
+ ### Fixed
1320
+
1321
+ - **`validateDocument` fallback parser: path extraction** — `parseValidationErrorsStructured`
1322
+ now correctly falls back to `path: "document"` for single-throw errors whose message
1323
+ contains a sentence (e.g. `"margins.left must be a non-negative finite number. Got: -1"`).
1324
+ Previously the heuristic accepted any text-before-colon that started with a letter,
1325
+ producing a corrupted path like `"margins.left must be a non-negative finite number. Got"`.
1326
+ Fix: reject candidates that contain `". "` (period + space), which only appears in
1327
+ prose sentences, never in path expressions like `content[0] (paragraph) spans[0].href`.
1328
+
1329
+ - **README `runtime%20deps` badge** Updated from `8` to `7` to reflect the removal
1330
+ of `@chenglou/pretext` from `dependencies` in v1.1.0.
1331
+
1332
+ - **`SECURITY.md` personal email removed** Replaced `akashchikara1998@gmail.com`
1333
+ with the GitHub private vulnerability reporting URL.
1334
+
1335
+ ### Changed
1336
+
1337
+ - **CI matrix: Node 18.x removed** Node 18 reached End of Life in April 2025.
1338
+ The CI matrix now targets Node 20.x and 22.x only. The `engines.node` field in
1339
+ `package.json` is updated to `>=20.0.0`. The Node 18 matrix slot was causing
1340
+ flaky benchmark failures (EOL runners are slower) that killed the `&&` test chain
1341
+ and caused the badge verifier to see a truncated test count.
1342
+
1343
+ ---
1344
+
1345
+ ## [1.1.0] 2026-05-07
1346
+
1347
+ Vendor `@chenglou/pretext` source directly into the package, eliminating the
1348
+ GitHub URL dependency and all associated install risks (mutable tags, npm audit
1349
+ gaps, network-only install, no SRI).
1350
+
1351
+ ### Changed
1352
+
1353
+ - **`@chenglou/pretext` is now vendored** — The upstream text-layout engine
1354
+ (`src/vendor/pretext/`) is compiled as part of pretext-pdf itself. Consumers
1355
+ no longer need to install `@chenglou/pretext`; the GitHub URL dependency has
1356
+ been removed from `package.json`. The vendored snapshot is pinned to
1357
+ `v0.0.6-patched.2` (commit `658edfec`) with 9 upstream PRs cherry-picked on
1358
+ top of the `v0.0.6` release. See `UPSTREAM.md` for the full patch inventory
1359
+ and upgrade procedure.
1360
+
1361
+ ### Added
1362
+
1363
+ - **`UPSTREAM.md`** Authoritative attribution and upgrade guide for the
1364
+ vendored `@chenglou/pretext` source. Documents provenance, the 9 cherry-picked
1365
+ upstream PRs (#3, #29, #105, #119, #132, #138, #140, #161, #165), which
1366
+ commits are excluded from vendoring (fork infra), and the procedure for
1367
+ updating when upstream publishes a new release.
1368
+
1369
+ ### Removed
1370
+
1371
+ - **`@chenglou/pretext` dependency** — Removed from `dependencies`. The library
1372
+ source is now bundled inside the package at `dist/vendor/pretext/`. No runtime
1373
+ behavior change; the same patched code is used.
1374
+
1375
+ ---
1376
+
1377
+ ## [1.0.9] — 2026-05-06
1378
+
1379
+ Test coverage Phase 2: filling blind spots in the CLI, the pdfmake compat shim, and
1380
+ the performance regression guard. Adds c8 coverage tooling for measurability.
1381
+
1382
+ ### Added
1383
+
1384
+ - **`test/cli.test.ts`** (+13 tests) End-to-end coverage for the `pretext-pdf` CLI binary,
1385
+ spawning `dist/cli.js` as a subprocess. Covers argument parsing (`--version`, `--help`,
1386
+ `-i/-o/--markdown/--code-font`, positional fallback, unknown flags), JSON and Markdown
1387
+ input modes, stdin/stdout piping, and exit codes 0/1/2.
1388
+
1389
+ - **`test/compat.test.ts`** (+34 tests) Coverage for the pdfmake pretext-pdf
1390
+ translation shim (`fromPdfmake`). Covers page setup (pageSize string and object,
1391
+ pageMargins scalar/2-tuple/4-tuple, orientation), styles (defaultStyle, named styles,
1392
+ headingMap override), all content node types (string, `text`, `ul`/`ol` with nesting,
1393
+ `table` with header rows, `image`, `qr`, `pageBreak`, `stack`), header/footer string
1394
+ forms, integration render, and unsupported nodes (`columns`, `canvas`).
1395
+
1396
+ - **c8 coverage tooling** `npm run coverage` (text + lcov reporters) and
1397
+ `npm run coverage:check` (75/65/75 thresholds, non-blocking in CI initially).
1398
+ Configuration in `.c8rc.json` excludes type-only files and CLI from instrumentation.
1399
+ Coverage step added to CI as `continue-on-error: true` while baseline thresholds
1400
+ are calibrated.
1401
+
1402
+ ### Fixed
1403
+
1404
+ - **`test/benchmark-baseline.test.ts`: regression guard now actually guards**
1405
+ Replaced the prior "TODO: enable when baseline is calibrated" stub (which collected
1406
+ timings but asserted nothing) with a real 3x-baseline-with-500ms-floor budget per
1407
+ corpus. Missing corpora in the baseline JSON now `assert.fail()` instead of silently
1408
+ defaulting to a zero budget that would mask any regression.
1409
+
1410
+ - **CONTRIBUTING.md: removed stale "(676 tests)" annotation** — Test count drift bait;
1411
+ the README badge already auto-verifies via `verify:badges`.
1412
+
1413
+ ### Changed
1414
+
1415
+ - **Test runner now builds first** — Added `pretest:unit: npm run build` so contributors
1416
+ running `npm run test:unit` always get a fresh dist; the new CLI tests spawn the
1417
+ compiled binary and would otherwise fail with a confusing module-not-found error.
1418
+
1419
+ ---
1420
+
1421
+ ## [1.0.8] — 2026-05-06
1422
+
1423
+ Public API contract integrity: the `RenderOptions.logger` option now actually does what
1424
+ its JSDoc has always promised, and `@napi-rs/canvas` no longer auto-installs.
1425
+
1426
+ ### Fixed
1427
+
1428
+ - **`RenderOptions.logger` now routes warnings from asset loading and rendering**
1429
+ Previously only validation warnings respected the `logger` option. Now all advisory
1430
+ warnings from `loadImages` (image load, image embed, QR/barcode/chart skipped, plugin
1431
+ loadAsset failed, watermark image skipped — 7 call sites) and `renderDocument` (form
1432
+ field render failure) flow through `logger.warn` when one is provided. Bidi-js fallback
1433
+ warnings from RTL reordering remain on `console.warn`; the JSDoc on `RenderOptions.logger`
1434
+ has been updated to document the actual scope honestly.
1435
+
1436
+ - **Missing `[pretext-pdf]` log prefix on bidi-js error path** One `console.warn` in
1437
+ `measure-text.ts` was logging without the canonical `[pretext-pdf]` prefix, making it
1438
+ hard to identify the library as the source in consumer logs. Now consistent.
1439
+
1440
+ ### Changed
1441
+
1442
+ - **`@napi-rs/canvas` removed from `optionalDependencies`** Was double-listed in both
1443
+ `peerDependencies` (with `optional: true`) and `optionalDependencies`. The latter caused
1444
+ npm to attempt installing the native canvas binary on every install, including in
1445
+ edge/serverless environments where the platform may not be supported and the dep is
1446
+ not needed. Now only listed under `peerDependencies` — install it explicitly when you
1447
+ need SVG/QR/barcode/chart rasterization in Node.
1448
+
1449
+ ### Documentation
1450
+
1451
+ - **README security callout for `allowedFileDirs`** — Added a prominent callout in the
1452
+ Quick Start section. The default behavior allows `image.src` and `svg.src` to read any
1453
+ absolute file path, which is a path-traversal vector when document JSON originates from
1454
+ user input or an LLM. The callout now appears immediately after the first `render()`
1455
+ example.
1456
+
1457
+ ---
1458
+
1459
+ ## [1.0.7]2026-05-05
1460
+
1461
+ Picks up pretext fork v0.0.6-patched.2: 8 additional upstream PRs (11 total).
1462
+
1463
+ ### Fixed
1464
+
1465
+ - **German low opening quote `„` no longer breaks at line-start on hyphenation path**
1466
+ `KINSOKU_START_FORBIDDEN` in `src/measure-text.ts` now includes U+201E (`„`), matching
1467
+ pretext PR #165 which fixed the non-hyphenation path. Previously `„` could appear
1468
+ at the start of a wrapped line when hyphenation was active.
1469
+
1470
+ - **Currency symbols stay glued to adjacent numbers** — Upstream PR #105 (cherry-picked in
1471
+ `v0.0.6-patched.2`) prevents `$`, `€`, `£`, `₹` etc. from line-breaking away from
1472
+ the number they annotate.
1473
+
1474
+ - **Trailing collapsible-space reconstruction fixed** — Upstream PR #29 fix (extended in
1475
+ v0.0.6-patched.2): a word followed by a space that exactly fills `maxWidth` no longer
1476
+ drops the space from line boundary cursors, preventing Arabic/mixed-script text from
1477
+ losing inter-word spaces during reconstruction.
1478
+
1479
+ ### Changed
1480
+
1481
+ - **`@chenglou/pretext` dependency** Bumped from `v0.0.6-patched` to `v0.0.6-patched.2`
1482
+ (GitHub fork, 11 upstream PRs total). Adds: CJK overflow prevention (PR #132),
1483
+ fit-advance cache fix (PR #161), rich inline stats unification (PR #138),
1484
+ chunk layout side table O(1) lookup (PR #140), bidi surrogate handling (PR #3),
1485
+ skip no-op merge passes (PR #119), currency stickiness (PR #105),
1486
+ German quote fix (PR #165), and trailing-space reconstruction (PR #29).
1487
+
1488
+ ---
1489
+
1490
+ ## [1.0.6] 2026-05-04
1491
+
1492
+ Audit bug fixes: validator correctness, internal export hygiene, schema gaps, README accuracy.
1493
+
1494
+ ### Fixed
1495
+
1496
+ - **lineHeight upper-bound cap removed** — `validate()` no longer rejects `lineHeight > 20`. The
1497
+ field is in points (pt), not a multiplier; 36pt is valid for a large heading. The `> 20` cap in
1498
+ `paragraph`, `heading`, and `defaultParagraphStyle` validators has been removed. The lower-bound
1499
+ check (lineHeight >= fontSize) is preserved.
1500
+
1501
+ - **form-field error messages use `${prefix}` format** — Error messages from the `form-field` case
1502
+ now follow the `content[N] (form-field): ...` format used by all other element types, instead of
1503
+ the old `[N] form-field.` prefix.
1504
+
1505
+ - **`assertUnknownProps` hint punctuation fixed** — The "unknown property" message previously
1506
+ produced `unknown property. did you mean "color"` (period before hint). Fixed to
1507
+ `unknown property; did you mean "color"` no period, semicolon separator.
1508
+
1509
+ - **British "colour" → "color" in JSDoc** — Two `QrCodeElement` field comments
1510
+ (`foreground`, `background`) and the `ValidationError.path` JSDoc example corrected.
1511
+
1512
+ - **`TocEntryElement`, `RichLine`, `RichFragment` removed from public exports** — These types are
1513
+ marked `@internal` in `types-public.ts` and should not be part of the npm API surface. Removed
1514
+ from `src/index.ts`.
1515
+
1516
+ - **Signature error includes original cause** `SIGNATURE_FAILED` now preserves the underlying
1517
+ error message: `PDF signing failed: <original message>` instead of a static string.
1518
+
1519
+ - **Header-only table now valid** `validate()` previously rejected tables where all rows are
1520
+ headers (`headerRowCount === rows.length`). Changed `>=` to `>`: tables where every row is a
1521
+ header are valid (useful for column-label-only tables).
1522
+
1523
+ - **Dead sub-condition removed in `float-group` floatWidth guard** — `fg.floatWidth <= 0` was a
1524
+ dead branch (any value `<= 0` is already `< 30`). Removed to clarify intent.
1525
+
1526
+ - **`warningCount` JSDoc updated** — Documents that the validator currently only emits errors, so
1527
+ `warningCount` is always 0 (reserved for future use).
1528
+
1529
+ - **`validateDocument` no longer re-throws unexpected errors** — Non-`PretextPdfError` exceptions
1530
+ (e.g. circular JSON, unexpected runtime errors) are now caught and returned as a structured
1531
+ `ValidationResult` instead of propagating. `validateDocument` now always returns, never throws.
1532
+
1533
+ ### Changed (Schema additions — `pretext-pdf/schema`)
1534
+
1535
+ - `qrCodeSchema`: added `margin` field.
1536
+ - `imageSchema`: added `floatFontSize`, `floatFontFamily`, `floatColor` fields.
1537
+ - `codeSchema`: added `dir` and `highlightTheme` fields.
1538
+ - `tableSchema`: added `dir`, `headerRows`, and cell-level `dir`, `fontFamily`, `fontSize`,
1539
+ `tabularNumbers` fields.
1540
+
1541
+ ### Docs
1542
+
1543
+ - README: `highlight.js` added to optional peer dependencies table.
1544
+ - README: `validate_document` added to MCP server tool list.
1545
+
1546
+ ---
1547
+
1548
+ ## [1.0.5]2026-05-04
1549
+
1550
+ Schema coverage completion, `ValidationResult.warningCount`, and README API docs.
1551
+
1552
+ ### Added
1553
+
1554
+ - **`ValidationResult.warningCount`** — `validateDocument()` now returns `warningCount` alongside
1555
+ `errorCount`. Computed by filtering `errors[]` by `severity === 'warning'`. MCP consumers no
1556
+ longer need to derive it client-side.
1557
+
1558
+ - **JSON Schema: remaining field coverage** `src/schema.ts` now covers all previously missing
1559
+ fields across 9 element types:
1560
+ - `inlineSpanSchema`: `dir`
1561
+ - `paragraphSchema`: `columns`, `columnGap`, `tabularNumbers`, `hyphenate`
1562
+ - `headingSchema`: `tabularNumbers`, `hyphenate`
1563
+ - `blockquoteSchema`: `lineHeight`, `padding`, `paddingH`, `paddingV`, `underline`, `strikethrough`
1564
+ - `calloutSchema`: `titleColor`, `fontWeight`, `lineHeight`, `padding`, `paddingH`, `paddingV`
1565
+ - `listSchema`: `lineHeight`, `markerWidth`, `itemSpaceAfter`, `nestedNumberingStyle`; nested
1566
+ items now carry `dir` and have a typed inner schema
1567
+ - `tocSchema`: `titleFontSize`, `levelIndent`, `leader`, `entrySpacing`
1568
+ - `formFieldSchema`: `borderColor`, `backgroundColor`, `keepTogether`, `defaultSelected`
1569
+ - `richParagraphSchema`: `columns`, `columnGap`, `tabularNumbers`
1570
+
1571
+ - **README: `validateDocument` and `pretext-pdf/schema` documented** — both entry points now have
1572
+ `### API reference` sections with code examples.
1573
+
1574
+ ---
1575
+
1576
+ ## [1.0.4] 2026-05-04
1577
+
1578
+ Schema export hardening: post-release audit fixes addressing coverage gaps and a
1579
+ malformed dialect URI.
1580
+
1581
+ ### Fixed
1582
+
1583
+ - **`pretext-pdf/schema`: `$schema` dialect URI corrected** — was
1584
+ `https://json-schema.org/draft/2020-12` (not a registered URI), now
1585
+ `https://json-schema.org/draft/2020-12/schema`. Strict JSON Schema validators
1586
+ (AJV, Smithery, VS Code) will now correctly identify the dialect.
1587
+ - **`pretext-pdf/schema`: `hr` element spacing fields** — `spaceAbove` and
1588
+ `spaceBelow` (the primary documented fields, default 12) were missing.
1589
+ `spaceBefore` and `spaceAfter` are now correctly marked as aliases.
1590
+ - **`pretext-pdf/schema`: `float-group` and `chart` element types** both
1591
+ first-class public element types were missing from the `content.items.anyOf`
1592
+ list. Schema-driven tooling will now know they exist.
1593
+
1594
+ ### Added (schema coverage)
1595
+
1596
+ - `pdfDocumentSchema.sections`page-range header/footer overrides
1597
+ - `headingSchema.annotation` — annotation field (was already on paragraph)
1598
+ - `tableSchema.cellPaddingH` / `cellPaddingV` primary table density controls
1599
+ - `imageSchema.floatWidth` / `floatGap` / `floatSpans` — column-layout controls
1600
+ for floated images
1601
+
1602
+ ---
1603
+
1604
+ ## [1.0.3] 2026-05-03
1605
+
1606
+ Enhancements: JSON Schema export, simplified marked peer dep range, and internal API polish.
1607
+
1608
+ ### Added
1609
+
1610
+ - **`pretext-pdf/schema` entry point** exports `pdfDocumentSchema`, a machine-readable JSON Schema
1611
+ object describing the full `PdfDocument` type. Covers all 22 element types and 18 top-level
1612
+ document properties. Intended for editor tooling, MCP clients, and Smithery UI form generation.
1613
+
1614
+ ```typescript
1615
+ import { pdfDocumentSchema } from 'pretext-pdf/schema'
1616
+ ```
1617
+
1618
+ ### Changed
1619
+
1620
+ - **`marked` peer dependency simplified** — `^9.0.0 || ^10.0.0 || ... || ^18.0.0` condensed to
1621
+ `>=9.0.0`. Semantically identical, cleaner npm output.
1622
+
1623
+ - **`validateDocument` logger option** — `options.logger` now passed to the underlying `validate()`
1624
+ call via conditional spread, respecting `exactOptionalPropertyTypes: true` constraints.
1625
+
1626
+ ### Fixed
1627
+
1628
+ - **`fonts.ts` unsafe cast removed** `(spec as { style?: string }).style` replaced with direct
1629
+ property access on the widened parameter type.
1630
+
1631
+ ---
1632
+
1633
+ ## [1.0.2]2026-05-03
1634
+
1635
+ ### Added
1636
+
1637
+ - `validateDocument(doc, options?)` — non-throwing validation API that returns a structured `ValidationResult` with typed `ValidationError[]` instead of throwing. Each error includes `path`, `message`, `code`, `severity`, and `suggestion` fields.
1638
+ - `ValidationError` and `ValidationResult` exported from the public API surface.
1639
+ - `Logger` interface and `logger?: Logger` in `RenderOptions` route diagnostic warnings through a custom logger instead of `console.warn`.
1640
+ - Inter italic font support (Inter-400-italic, Inter-700-italic) via bundled `@fontsource/inter` italic markdown and `fontStyle: 'italic'` now work without manual font setup.
1641
+
1642
+ ---
1643
+
1644
+ ## [1.0.1]2026-05-02
1645
+
1646
+ Patch: strict mode correctness fixes. No API changes.
1647
+
1648
+ ### Fixed
1649
+
1650
+ - **`levenshteinDist` early-exit bug** — per-cell `if (curr[j]! > 2) return 999` inside
1651
+ the inner DP loop fired on intermediate cells, causing d=1 pairs like `hrefs→href` and
1652
+ `spaceafter→spaceAfter` to incorrectly return 999 instead of 1. Fix: removed the per-cell
1653
+ guard; final check only (`prev[n]! > 2 ? 999 : prev[n]!`).
1654
+ - **Seven path-prefix annotations** — strict-mode error paths had `(type)` suffixes
1655
+ (e.g. `doc(table).rows[0]`) that no other validator used and that tests didn't expect.
1656
+ All seven removed so paths are plain dot-notation.
1657
+ - **`encryption` block not strict-checked** unknown props inside `doc.encryption`
1658
+ were silently accepted in strict mode. Now validated against `ALLOWED_PROPS_SUB['encryption']`.
1659
+ - **Root path was `'document'` not `'doc'`** top-level `assertUnknownProps` was called
1660
+ with `'document'` as the path prefix, producing paths like `document.content[0]` instead
1661
+ of `doc.content[0]`. Corrected to `'doc'`.
1662
+ - **Suggestion format mismatched** — `Did you mean 'x'?` → `did you mean "x"` (lowercase,
1663
+ double-quotes) to match the format tests asserted.
1664
+ - **`formatErrors` missing header** — multi-error output now begins with
1665
+ `Strict validation failed (N issues):\n` so callers can detect strict vs. regular errors.
1666
+
1667
+ ### Tests
1668
+
1669
+ - Added `test/validate-strict.test.ts` (35 tests) to `test:unit` script — these tests were
1670
+ written but not wired into CI in v1.0.0.
1671
+
1672
+ ---
1673
+
1674
+ ## [1.0.0] 2026-05-02
1675
+
1676
+ First stable release. Completes the plugin extension API, closes all v1.0 gate requirements,
1677
+ and ships a fully verified public surface with zero breaking changes from 0.9.x.
1678
+
1679
+ ### Added
1680
+
1681
+ - **Plugin extension API** — Register custom element types via `RenderOptions.plugins`.
1682
+ Each `PluginDefinition` participates in all four pipeline stages: `validate`, `loadAsset`,
1683
+ `measure`, and `render`. Plugins are fully typed and tree-shaken from documents
1684
+ that don't use them. See README § Custom element types (plugins) and
1685
+ `examples/plugin-custom-element.ts` for a runnable example.
1686
+ - **`PluginDefinition`, `PluginMeasureContext`, `PluginMeasureResult`, `PluginRenderContext`**
1687
+ exported from `pretext-pdf` public surface (previously internal).
1688
+ - **`PdfBuilder` and `PdfBuilderOptions`** exported from `pretext-pdf` (enables type-safe
1689
+ builder construction in downstream code without re-declaring the interface).
1690
+ - **`TocEntryElement`** exported from `pretext-pdf` public surface (was in the `ContentElement`
1691
+ union but not individually importable).
1692
+ - **`plugins` option on `createPdf()`** — `PdfBuilderOptions.plugins` threads plugins through
1693
+ the builder's `build()` call automatically.
1694
+ - **`Intl.Segmenter` pre-flight guard** in `render()` — throws `RENDER_FAILED` with a clear
1695
+ message on Node.js < 16 or runtimes without full-ICU data, instead of silently producing
1696
+ incorrect line breaks.
1697
+ - **`PluginRenderContext.pageWidth/pageHeight/margins`** — render hooks now receive full page
1698
+ geometry for layout calculations (page-relative positioning, bleed boxes, etc.).
1699
+ - **`render` context Y-coordinate docs** expanded JSDoc with multi-line text example showing
1700
+ how to position text baselines relative to `context.y`.
1701
+ - **Benchmark corpora manifest** and **smoke staging** tests wired into `npm test`
1702
+ (previously orphaned).
1703
+ - **`test/table-determinism.test.ts`** asserts that table pagination produces identical
1704
+ layout traces across repeated invocations of `prepareLayoutState`.
1705
+ - **`test/validate-strict.test.ts`** (35 tests) comprehensive contract for `strict: true`
1706
+ validation covering all element types, nested structures, Levenshtein suggestions, error
1707
+ message format, doc-level and sub-structure prop checks. Total test count: 676.
1708
+ - `examples/plugin-custom-element.ts`runnable plugin example (`npm run example:plugin`).
1709
+
1710
+ ### Fixed
1711
+
1712
+ - **`SIGNATURE_CERT_AND_ENCRYPTION` error code** was declared in the `ErrorCode` union
1713
+ but never thrown; validate.ts now uses it correctly when a document specifies both
1714
+ signatures and encryption (previously threw a generic `VALIDATION_ERROR`).
1715
+ - **Build break under `exactOptionalPropertyTypes: true`** — `PdfBuilder.build()` no longer
1716
+ passes `{ plugins: undefined }` to `runPipeline` when no plugins are configured.
1717
+ - **Plugin `validate` hook empty-string normalization** — `plugin.validate()` returning `''`
1718
+ now correctly accepts the element (was previously treated as a rejection message).
1719
+ - **`toc` element reaching render default arm** `render.ts` now has an explicit
1720
+ `case 'toc': return` guard before the default arm; TOC elements are pre-processed
1721
+ during pagination and should never reach the renderer.
1722
+ - **`RichLine` and `RichFragment`** demoted from `@public` to `@internal`; these are
1723
+ implementation details of the rich-text pipeline, not intended for external use.
1724
+ - **Sentinel value documentation** — `MeasuredBlock` comment now explicitly states that
1725
+ `lines: []`, `fontSize: 0`, `lineHeight: 0`, `fontKey: ''` applies to spacers, tables,
1726
+ images, hr, *and plugin blocks* not a bug but a documented convention.
1727
+
1728
+ ### Internal
1729
+
1730
+ - `src/plugin-registry.ts` (new): Pure orchestration helpers for the four plugin injection
1731
+ points (`findPlugin`, `runPluginValidate`, `runPluginLoadAsset`, `runPluginMeasure`,
1732
+ `runPluginRender`).
1733
+ - `src/plugin-types.ts` (new): `PluginDefinition` interface and context/result types.
1734
+ - `src/layout-state.ts`: `prepareLayoutState` now accepts `options?: RenderOptions` and
1735
+ threads plugins to `stageValidate`, `stageLoadAssets`, and `stageMeasure`.
1736
+ - `docs/V1.0-RUNBOOK.md`: Full release runbook with first-principles audit, anti-hallucination
1737
+ protocol, verified-facts table, and phase-by-phase plan.
1738
+
1739
+ ---
1740
+
1741
+ ## [0.9.4] 2026-05-02
1742
+
1743
+ > **Note:** This release was never published to npm as a standalone tag. All changes listed
1744
+ > here shipped as part of [1.0.0] on the same date.
1745
+
1746
+ Architecture hardening + API surface snapshot. No public API changes; internal
1747
+ restructuring to eliminate circular dependencies and add drift guards before v1.0 freeze.
1748
+
1749
+ ### Added
1750
+
1751
+ - **API surface snapshot** (`etc/pretext-pdf.api.md`) checked into source control as
1752
+ the v1.0 baseline. The `api:check` CI step will fail on unintentional public-API drift.
1753
+ - **`src/layout-state.ts`** — `prepareLayoutState()` and `summarizeLayoutState()` extracted
1754
+ from the pipeline for testability; `layout-contract` and `hard-text-contract` tests
1755
+ wired into `test:unit`.
1756
+ - **`src/benchmarks/corpora.ts`** — benchmark corpus manifest (`getBenchmarkCorpora()`)
1757
+ restored from git history; `benchmark-baseline.test.ts` wired into `test:contract`.
1758
+ - **Drift guards** (`test/drift-guards.test.ts`) — asserts that `ELEMENT_TYPES`,
1759
+ `ALLOWED_PROPS`, `validate.ts` cases, and `render.ts` cases all agree at test time.
1760
+ Catches any future element-type addition that isn't plumbed through all four places.
1761
+ - **`render.ts` default arm** unknown element types now throw immediately instead of
1762
+ silently producing a blank block.
1763
+
1764
+ ### Refactored
1765
+
1766
+ - **Circular dependency broken**: `src/post-process.ts` extracted so `builder.ts` and
1767
+ `index.ts` no longer form a cycle through each other.
1768
+ - **`ELEMENT_TYPES` extracted** to `src/element-types.ts` as single source of truth;
1769
+ re-exported from `index.ts`, imported by `validate.ts` eliminates the previous
1770
+ per-file string-literal duplication.
1771
+
1772
+ ### Fixed
1773
+
1774
+ - `post-process.ts`: drop raw signing library error message from `SIGNATURE_FAILED`
1775
+ to avoid leaking certificate or passphrase details in error output.
1776
+ - `layout-state.ts`: polyfill install wrapped in try/catch; throws `CANVAS_UNAVAILABLE`
1777
+ on failure instead of an untyped exception.
1778
+
1779
+ ---
1780
+
1781
+ ## [0.9.3] — 2026-04-23
1782
+
1783
+ Strict validation release. Opt-in property validation to catch unknown properties on elements and sub-structures via typo detection and precise JSONPath error reporting.
1784
+
1785
+ ### Added
1786
+
1787
+ - **Strict validation mode**: Pass `{ strict: true }` to `render(doc, options)` to reject unknown properties. Non-strict mode (default) remains permissive for backwards compatibility.
1788
+ - **`render()` options parameter**: Updated signature to `render(doc: PdfDocument, options?: RenderOptions)` where `RenderOptions = { strict?: boolean }`.
1789
+ - **`validate()` public export**: `validate()` is now exported from `pretext-pdf` for standalone validation and testing.
1790
+ - **Validation error details**:
1791
+ - Unknown properties reported with Levenshtein edit-distance suggestions (distance ≤2) for typo correction.
1792
+ - Errors include JSONPath-like paths (`content[3].table.rows[0].cells[1].align`) for precise location reporting.
1793
+ - Error accumulation: all violations collected before throwing a single VALIDATION_ERROR with formatted multi-line message.
1794
+ - First 20 errors shown; overflow indicator present.
1795
+ - **Compile-time drift guards**: `src/allowed-props.ts` uses `Exact<T, Keys>` TypeScript type assertions to catch property definition drift at type-check time. If element types change, `tsc --noEmit` will error if allowed-props lists don't match.
1796
+ - **Property allowlists**:
1797
+ - `ALLOWED_PROPS`: 22 element types (paragraph, heading, table, image, code, list, etc.)
1798
+ - `ALLOWED_PROPS_SUB`: 8 sub-structures (document, metadata, table-row, table-cell, list-item, inline-span, column-def, annotation)
1799
+
1800
+ ### Internal
1801
+
1802
+ - `src/allowed-props.ts` (new): Central configuration for allowed properties with compile-time assertions.
1803
+ - `src/validate.ts` (enhanced): Added `levenshteinDist()`, `closestMatch()`, `assertUnknownProps()`, and `formatErrors()` helpers; threading strict flag through `validateElement()` for nested structure validation (tables, lists, rich-paragraphs, float-groups, annotations).
1804
+
1805
+ ---
1806
+
1807
+ ## [0.9.2] — 2026-04-22
1808
+
1809
+ Maintenance release. Engine refresh + repo-hygiene automation. No runtime behavior changes beyond the `@chenglou/pretext` bump.
1810
+
1811
+ ### Changed
1812
+
1813
+ - **Bumped `@chenglou/pretext` to 0.0.6** (from 0.0.5). Brings two upstream improvements: (a) CJK text followed by opening-bracket annotations now wraps like browsers instead of leaving the opening bracket on the previous line (upstream PR #148), (b) native numeric `letterSpacing` support on `prepare()` and `prepareWithSegments()` (upstream PRs #108/#156). Our manual letterSpacing compensation in `src/measure-blocks.ts` and `src/rich-text.ts` continues to work unchanged — delegating to pretext's native path is tracked as Tier 1 follow-up in `docs/ROADMAP.md`. All 624 tests green, all 5 visual regression baselines green.
1814
+
1815
+ ### Fixed
1816
+
1817
+ - **README badges matched to reality**: `runtime-deps-7` `runtime-deps-8` (there are 8 direct `dependencies`, not 7), `tests-600+` `tests-624` (the full `npm test` chain runs 624 tests across 5 subsuites). Drift guarded by a new CI step; see below.
1818
+
1819
+ ### Added
1820
+
1821
+ - `scripts/verify-badges.js` + CI step — compares README shields.io badge values against `package.json` dep count and `npm test` total. Fails CI on drift. Fast path via `SKIP_TEST_RUN=1` for pre-commit use.
1822
+ - `release` job in `ci.yml` — on `v*` tag push, auto-extracts the matching `## [X.Y.Z]` section from this file and creates the GitHub release (requires publish to succeed first). Closes the "tag exists but no release page" gap that affected v0.9.1. (Note: originally shipped as `.github/workflows/release-on-tag.yml`; merged into `ci.yml` for dependency ordering in Tier 0.5.)
1823
+ - `renovate.json` — watches dependencies, auto-merges devDependency bumps that pass CI, opens PRs (without auto-merge) for runtime, peer, and `@chenglou/pretext` engine bumps. Closes the gap that left us one release behind upstream.
1824
+
1825
+ ### Removed
1826
+
1827
+ - `test/smoke-staging.test.ts` — exercised a non-existent `{ type: 'paragraph', footnote: {...} }` shape that the permissive validator silently accepted. False coverage. A strict validator rollout (rejecting unknown element properties) is the root fix and is tracked as a Tier 1 item in the rewritten `docs/ROADMAP.md`.
1828
+ - `src/brain/`inert auto-logger artifact (34 blank-body entries, no active writer). Never published to npm.
1829
+
1830
+ ### Docs
1831
+
1832
+ - `docs/ROADMAP.md` — complete rewrite as a living document (Now / Next / Under consideration / Shipped / History + Update discipline). The previous "master remediation plan" with phase-numbered sections was dropped: phases 0–5 all shipped by v0.9.1, and the document had rotted to the point of contradicting `package.json` on dependency pinning and `CHANGELOG.md` on what was live. History section preserves the prior plan's origin date and scope for reference.
1833
+
1834
+ ---
1835
+
1836
+ ## [0.9.1] — 2026-04-21
1837
+
1838
+ Bug-fix + hardening release. Ships the callout + rich-text rendering fixes from PR #2 together with PR #3's producer-validator contract around measured blocks.
1839
+
1840
+ ### Fixed
1841
+
1842
+ - **Rich-paragraph: leading-space tokens stripped after hard break** ([src/rich-text.ts](src/rich-text.ts)). A pre-overflow guard (`isLeadingSpace: currentX === 0 && token.text.trim() === ''`) fired whenever `currentX` was zero — both at block start *and* after a `\n` hard break reset the cursor. Continuation spans beginning with whitespace (e.g. `' · text'`) had their first token silently dropped, causing separator glyphs and indented text to appear mis-positioned. Guard removed; the overflow-wrap skip path that correctly skips trailing spaces after soft wraps is unaffected.
1843
+ - **Callout: `spaceAfter` double-applied by paginator** ([src/measure-blocks.ts](src/measure-blocks.ts)). `callout` block measurement included `el.spaceAfter ?? 12` inside `totalHeight` *and* returned the same value as `block.spaceAfter`. `paginate.ts` added `block.spaceAfter` on top of `block.height`, counting it twice and pushing callout content ~12 pt below its intended position. Fixed by removing `spaceAfter` from the `totalHeight` formula; the value is still returned in `block.spaceAfter` for the paginator.
1844
+ - **Callout with title: background rect clips title row when split across pages** ([src/paginate.ts](src/paginate.ts)). `splitBlock` did not subtract `calloutData.titleHeight` from `availableForLines` for the first chunk, allowing `floor((titleH + lh) / lh)` extra lines to be placed, leaving no room for the title row. `getCurrentY` also omitted `titleHeight` from `blockBottom`, producing incorrect Y tracking after a split callout. Both fixed: `titleH` is now subtracted from available space on the first chunk only, and added to `blockBottom` when computing the cursor position after the first chunk renders.
1845
+
1846
+ ### Added / hardened
1847
+
1848
+ - **Producer-validator contract for measured blocks** ([src/paginate.ts](src/paginate.ts)). `validateMeasuredBlocks()` runs at `paginate()` entry in O(n) and throws `PretextPdfError('PAGINATION_FAILED')` if a callout `MeasuredBlock` is missing `calloutData` or any of `titleHeight` / `paddingV` / `paddingH` is non-finite same for blockquote padding/border fields. Surfaces producer bugs directly instead of as downstream NaN arithmetic or `PAGE_LIMIT_EXCEEDED`.
1849
+ - **Narrowed internal types** `MeasuredCalloutBlock` / `MeasuredBlockquoteBlock` (intersection types in [src/types.ts](src/types.ts)) consumed by `calloutTitleHeight` + `verticalPadding` helpers in `paginate.ts`. No defensive runtime checks downstream.
1850
+ - **Extracted `CalloutData` interface** from the previously-inline shape on `MeasuredBlock.calloutData`. Measurer constructs it as a typed literal, so TypeScript enforces the full contract at the producer site.
1851
+ - **Zero-width non-whitespace tokens preserved**: the rich-text post-soft-wrap guard only skips tokens where `text.trim() === ''`. ZWJ (U+200D), combining marks, and other zero-width non-whitespace characters pass through so emoji / CJK shaping stays intact — pinned by a regression test.
1852
+ - **Extracted `LINK_COLOR_DEFAULT`** constant in `src/rich-text.ts`.
1853
+
1854
+ ### Tests
1855
+
1856
+ - `test/rich-text.test.ts` 20 → 23 (+3): block-start leading whitespace preserved; leading whitespace after hard break preserved; ZWJ preservation.
1857
+ - `test/phase-8d-callout.test.ts` 12 → 19 (+7): callout `spaceAfter` double-count regression, titled split line count, untitled split, continuation chunk `yFromTop === 0`, mid-page split entry, validator rejection on missing `calloutData`, validator rejection on partial `calloutData` (non-finite fields), validator rejection on partial blockquote padding, non-callout-document early-return.
1858
+ - Full suite: 624 tests, 100% pass.
1859
+
1860
+ ### Chore / docs
1861
+
1862
+ - Removed `brain/learnings/*.md`, `docs/PLAN-v0.6-v1.0.md`, `test/paginate.test.ts.archive` — internal dev artifacts not for the public repo.
1863
+ - Stripped `Phase N:` nomenclature from `src/` comments (pure rename — no logic delta).
1864
+ - Added `demo/stackblitz/.stackblitzrc`, `docs/articles/pretext-pdf-vs-pdfmake-2026.md` (draft).
1865
+ - Added `examples/visual-pr2-bug1-separator.ts` + `examples/visual-pr2-bug3-callout-split.ts` plus 4 reference PNGs under `docs/visuals/pr2/` for bug-reproduction demonstrations.
1866
+ - README test badge corrected `650+ → 600+` (verified: 624 tests total).
1867
+
1868
+ ---
1869
+
1870
+ ## [0.9.0]2026-04-20
1871
+
1872
+ Three additive enhancements that broaden the package's surface without growing its mandatory dependency footprint.
1873
+
1874
+ ### Added
1875
+
1876
+ - **CLI binary** — `pretext-pdf` is now a `bin` entry. `pretext-pdf doc.json out.pdf`, `cat doc.json | pretext-pdf > out.pdf`, `echo '{...}' | pretext-pdf -o out.pdf`. Supports stdin/stdout and file arguments. `--markdown` flag converts Markdown input to PDF in one step (requires the `marked` peer dep). See [src/cli.ts](src/cli.ts).
1877
+ - **`pretext-pdf/compat` entry point** — `fromPdfmake(pdfmakeDoc)` translates pdfmake document descriptors into `PdfDocument` so existing pdfmake codebases can switch with a one-line change at the entry point. Covers strings, `text` nodes (with `style`/`bold`/`italics`/`color`/`fontSize`/`alignment`/`font`), `ul`/`ol`, `table` (with `widths` + `headerRows`), `image`, `qr`, `pageBreak` (`before`/`after`), `stack`, `pageSize`/`pageOrientation`/`pageMargins`, `defaultStyle`/`styles`, `info` → metadata, and string-form `header`/`footer`. Default style-name → heading mapping is configurable via `headingMap` option.
1878
+ - **Markdown: GFM tables** ([src/markdown.ts](src/markdown.ts))`markdownToContent()` now recognises GFM tables and translates them to `TableElement`, including column alignment from `:---:` / `---:` markers. Ragged rows are padded with empty cells.
1879
+ - **Markdown: GFM task lists** — `- [x] done` and `- [ ] todo` render with ☑ / ☐ Unicode markers prepended to the item text.
1880
+
1881
+ ### Tests
1882
+
1883
+ - New `test/v0.9.0-features.test.ts` (21 tests): markdown table + task list, full CLI exec coverage (stdin, file, `--markdown`, error paths), and pdfmake compat (strings, headings, rich-paragraphs, lists, tables, images, QR, `pageBreak`, `stack`, `pageSize`/`pageMargins`, end-to-end render of a translated document).
1884
+
1885
+ ### Notes
1886
+
1887
+ - Zero new mandatory dependencies. The CLI uses only Node built-ins. The compat shim is pure TypeScript. Markdown additions ride on the existing optional `marked` peer.
1888
+ - `dist/cli.js` is wired through `package.json#bin.pretext-pdf` `npm install -g pretext-pdf` makes the CLI globally available; `npx pretext-pdf` works without install.
1889
+
1890
+ ---
1891
+
1892
+ ## [0.8.3] — 2026-04-20
1893
+
1894
+ ### Security
1895
+
1896
+ - **SSRFIPv4-mapped IPv6 bypass** ([src/assets.ts](src/assets.ts) `assertSafeUrl`). Pre-0.8.3 the private-IP guard checked the parsed hostname against dotted-decimal regexes only. WHATWG `URL` normalizes `[::ffff:127.0.0.1]` to `[::ffff:7f00:1]` (hex IPv4-in-IPv6), so attacker-supplied URLs of the form `https://[::ffff:127.0.0.1]/admin` slipped past every `^127\.`/`^10\.`/etc. check and reached localhost or RFC 1918 ranges. Patched by detecting both the dotted (`::ffff:127.0.0.1`) and hex-compressed (`::ffff:7f00:1`) IPv4-mapped forms and decoding the embedded IPv4 before regex matching. Also explicitly blocks the IPv6 unspecified address `::`.
1897
+ - **SSRFredirect-following bypass** ([src/assets.ts](src/assets.ts) `fetchWithTimeout`). The previous implementation used the default `redirect: 'follow'`, so a public URL could `302` to `http://127.0.0.1:8080/internal` and the library would happily fetch the private target despite the upfront `assertSafeUrl` check on the *initial* URL. Patched to use `redirect: 'manual'` and re-validate every `Location` hop with `assertSafeUrl`, capped at 3 redirects. Browser opaqueredirect responses are rejected with a clear error.
1898
+
1899
+ ### Fixed
1900
+
1901
+ - **`createGstInvoice` amount-in-words double space for sub-rupee totals** ([src/templates.ts](src/templates.ts)). An invoice whose total was less than ₹1 (e.g. ₹0.50) produced `"Rupees and Fifty Paise Only"` (two spaces after "Rupees") because the rupee-words branch resolved to an empty string. Now uses an explicit `"Zero"` when there are no rupees: `"Rupees Zero and Fifty Paise Only"`.
1902
+ - **Markdown deeper-than-2-level lists silently dropped** ([src/markdown.ts](src/markdown.ts) `convertListItem`). Pre-0.8.3 the converter only created text-only leaves for nested lists, so `- A\n - B\n - C` lost C entirely. Now recursive — preserves arbitrary nesting depth in the resulting `ListItem` tree.
1903
+ - **Markdown list items with paragraph-typed content** ([src/markdown.ts](src/markdown.ts)). When list items were separated by blank lines, marked emits `paragraph` tokens (not `text` tokens) for the item content. The converter only handled `text`, silently dropping the item text. Now also handles `paragraph` tokens.
1904
+
1905
+ ### Tests
1906
+
1907
+ - New `test/v0.8.3-ssrf.test.ts` covers 11 IPv4-mapped IPv6 bypass cases, IPv6 unspecified/loopback regressions, and HTTP rejection.
1908
+ - Extended `test/phase-10c-markdown.test.ts` with regressions for 3-level nesting and paragraph-typed list items.
1909
+ - Extended `test/phase-10d-templates.test.ts` with the sub-rupee amount-in-words case.
1910
+
1911
+ ---
1912
+
1913
+ ## [0.8.2]2026-04-20
1914
+
1915
+ ### Fixed
1916
+
1917
+ - **Rich-paragraph whitespace collapse** — multi-span `rich-paragraph` content rendered with adjacent words overlapping (e.g. `"Founder & CEO" + " — Antigravity Systems"` displayed as `"Founder& CEO—AntigravitySystems"`). Root cause: pretext's `layoutWithLines` follows CSS-like behavior and excludes trailing whitespace from line widths, so tokens like `"Hello "` or `" "` measured to width 0 and downstream fragments overlapped the previous one. `measureTokenWidth` in [src/rich-text.ts](src/rich-text.ts) now uses a sentinel-character technique (append non-whitespace `\u2588`, measure combined string, subtract sentinel width) to recover the true rendered width whenever a token has trailing whitespace. Sentinel width is cached per font config.
1918
+ - The fast path (no trailing whitespace) is unchanged single pretext call. Slow path adds two pretext calls per affected token, with one cached.
1919
+
1920
+ ### Tests
1921
+
1922
+ - Added 3 regression tests in `test/rich-text.test.ts` under `whitespace preservation (v0.8.2 fix)` covering trailing whitespace inside spans, whitespace-only separator spans, and the exact `"Founder & CEO" → "Antigravity Systems"` resume-preset scenario.
1923
+
1924
+ ---
1925
+
1926
+ ## [0.8.1]2026-04-20
1927
+
1928
+ ### Fixed
1929
+
1930
+ - **Browser support** — `pretext-pdf` now imports cleanly in browsers. Module-init in `src/fonts.ts` previously called `fileURLToPath(import.meta.url)` and `createRequire(import.meta.url)` eagerly, which threw `"The URL must be of scheme file"` whenever the module was loaded from a non-`file://` URL (esm.sh, jsdelivr, Vite dev server). Both calls are now gated on a runtime `IS_NODE` check, and the bundled-Inter `BUNDLED_INTER_PATHS` arrays are constructed only in Node.
1931
+ - **Browser font-loading errors** — `loadFontBytes` now throws clear `FONT_LOAD_FAILED` messages when bundled Inter or string font paths are requested in a browser, pointing the consumer at the correct workaround (supply `Uint8Array` bytes via `doc.fonts`).
1932
+
1933
+ ### Notes for browser users
1934
+
1935
+ - Always supply Inter (or your default font) explicitly via `doc.fonts: [{ family: 'Inter', weight: 400, src: <Uint8Array> }, { family: 'Inter', weight: 700, src: <Uint8Array> }]`. The library cannot read local font files in the browser.
1936
+ - SVG / chart / qr-code / barcode elements still depend on `@napi-rs/canvas` at runtime; in the browser, the native `OffscreenCanvas` is used instead and the polyfill is skipped automatically.
1937
+
1938
+ ---
1939
+
1940
+ ## [0.8.0] — 2026-04-19
1941
+
1942
+ ### Added
1943
+
1944
+ - **`qr-code` element** generate QR codes as inline PDF content using the `qrcode` optional peer dependency. Supports `data`, `size`, `errorCorrectionLevel` (L/M/Q/H), `foreground`/`background` hex colours, `margin`, `align`, `spaceBefore`/`spaceAfter`. Fully serverless — pure JS, no canvas required.
1945
+ - **`barcode` element** — generate 100+ barcode symbologies (EAN-13, Code128, PDF417, QR, DataMatrix, etc.) via the `bwip-js` optional peer dependency. Supports `symbology`, `data`, `width`, `height`, `includeText`, `align`, `spaceBefore`/`spaceAfter`. Pure JS, Lambda/Edge safe.
1946
+ - **`chart` element** — embed Vega-Lite charts as vector SVG using `vega` + `vega-lite` optional peer deps. Accepts any Vega-Lite `spec`, `width`, `height`, `caption`, `align`. Rendered with `renderer: 'none'` — zero canvas/puppeteer dependency.
1947
+ - **`pretext-pdf/markdown` entry point** — `markdownToContent(md, options?)` converts a Markdown string to `ContentElement[]`. Requires optional `marked` peer dep. Supports headings, bold/italic/links (→ rich-paragraph), lists (2 levels), blockquotes, code blocks, and HR.
1948
+ - **`pretext-pdf/templates` entry point** three typed template functions with zero extra dependencies: `createInvoice(data)` (generic invoice with currency, tax, discount, QR payment), `createGstInvoice(data)` (GST-compliant Indian tax invoice with IGST/CGST+SGST, UPI QR, bank details, amount in words), `createReport(data)` (structured business report with optional TOC).
1949
+ - **New error codes** — `QR_DEP_MISSING`, `QR_GENERATE_FAILED`, `BARCODE_DEP_MISSING`, `BARCODE_GENERATE_FAILED`, `BARCODE_SYMBOLOGY_INVALID`, `CHART_DEP_MISSING`, `CHART_SPEC_INVALID`, `CHART_RENDER_FAILED`, `MARKDOWN_DEP_MISSING`.
1950
+
1951
+ ---
1952
+
1953
+ ## [0.7.2]2026-04-20
1954
+
1955
+ Phase 11 cross-cutting enhancements. Retroactively attributed to 0.7.2; these features were
1956
+ originally left as `[Unreleased]` and published out of chronological order after 0.7.1.
1957
+
1958
+ ### Added
1959
+
1960
+ - **`floatSpans` on image elements** — rich-text alternative to plain `floatText`. Accepts `InlineSpan[]` for mixed bold/italic/color/link captions beside float images. Mutually exclusive with `floatText` (validated).
1961
+ - **2-level list nesting** `ListItem.items` now supports one further level of nesting (depth 0 1 2). Unordered marker: `▪`. Ordered: inherits parent counter or restarts via `nestedNumberingStyle: 'restart'`.
1962
+ - **Table `rowspan`** `TableCell.rowspan` spans a cell across multiple rows. Works alongside `colspan`. Origin cell draws background over full span height; continuation rows automatically receive placeholder cells.
1963
+ - **`onFormFieldError` callback** — `doc.onFormFieldError: (name, err) => 'skip' | 'throw'` mirrors `onImageLoadError`. Controls render behaviour when a form field fails.
1964
+ - **`createFootnoteSet(defs)`** — helper exported from `pretext-pdf` that generates footnote definition/reference pairs with globally unique IDs. Returns `Array<{ id, def }>`.
1965
+ - **`renderDate` field** — `doc.renderDate: Date | string` overrides the PDF creation date. Useful for reproducible builds and testing.
1966
+ - **`{{date}}` and `{{author}}` tokens** in header/footer text join existing `{{pageNumber}}` / `{{totalPages}}`. `{{date}}` resolves from `renderDate`; `{{author}}` resolves from `doc.metadata.author`.
1967
+ - **`tabularNumbers`** on `rich-paragraph` digits rendered at uniform slot width (widest digit in font), so columns of numbers align without OpenType TNUM feature.
1968
+ - **`smallCaps` + `letterSpacing` per span** — `InlineSpan.smallCaps` and `InlineSpan.letterSpacing` now respected in `rich-paragraph` rendering.
1969
+ - **Per-span `fontSize`** — `InlineSpan.fontSize` overrides the element-level font size for that span. Enables mixed-size text in a single paragraph.
1970
+
1971
+ ### Fixed
1972
+
1973
+ - `resolveTokens()` used `.replace()` (replaces first occurrence only) — changed to `.replaceAll()` for all four tokens.
1974
+ - Table span grid: continuation-row cursor was advancing by 1 instead of `colspan` when skipping a spanned column — now advances by full span width.
1975
+ - Font family names now validated for safe characters (`/^[a-zA-Z0-9 _-]+$/`) in `requireFamily()` rejects null bytes and control characters.
1976
+ - Annotation `color` and `author` fields now validated in `validateElement()` for both `paragraph` and `heading` annotations.
1977
+ - `buildOutlineTree` memoizes `parentIdxOf()` into a pre-computed array — eliminates O(n²) scan for documents with large heading counts.
1978
+ - Table grid-line renderer pre-computes active boundary set — eliminates O(rows × cols) inner loop for large tables.
1979
+ - `addLinkAnnotation()` re-validates URL scheme at render time (defense-in-depth; `validate.ts` is the primary gate).
1980
+
1981
+ ---
1982
+
1983
+ ## [0.7.1] 2026-04-19
1984
+
1985
+ ### Changed
1986
+
1987
+ - **Upstream pretext pinned to `f2014338487a`** — picks up unreleased CJK opening-bracket annotation fix, Hangul jamo line-walker alignment fix, and two internal line-object churn reductions. No public API changes.
1988
+
1989
+ ### Fixed
1990
+
1991
+ - **List nesting depth enforced at validation** — `ListItem.items` (2nd-level items) now correctly rejects any further `.items` property, matching the documented 2-level maximum. Previously the validation silently passed 3-level data which could cause undefined render behaviour.
1992
+ - **3 phase-11 list tests corrected** — test data incorrectly contained 3-level nesting while named "2-level"; data trimmed to match documented contract.
1993
+
1994
+ ---
1995
+
1996
+ ## [0.7.0] — 2026-04-17
1997
+
1998
+ ### Added
1999
+
2000
+ - **6 production templates** (`templates/`) — GST invoice, international invoice, resume, multi-section report, NDA, and meeting minutes. Each is a self-contained `.ts` file outputting a valid PDF. Smoke-tested in Phase 2F Block D.
2001
+ - **StackBlitz live demo** (`demo/stackblitz/`)4-tab UI (Invoice, Report, Resume, Custom) backed by a Node.js render server. Edit JSON and generate PDFs instantly, no install required. Accessible at the StackBlitz link in the README.
2002
+ - **`## Performance` section in README** — measured render times and PDF sizes for 1-page, 10-page, and mixed-element documents. Font subsetting behaviour documented.
2003
+ - **Stress tests and benchmarks** (`test/phase-2f-stress.test.ts`) — 32 tests across 4 blocks: large document stress (400-element, 200-row table), edge case stress (CJK, RTL, empty arrays, extreme sizes), timing benchmarks (1-page < 500 ms, 10-page < 5,000 ms), and template smoke tests.
2004
+ - **Error code coverage** — new tests for `COLUMN_WIDTH_TOO_NARROW`, `IMAGE_LOAD_FAILED`, `SVG_LOAD_FAILED`, and `ASSEMBLY_FAILED`. 16 of 19 error codes now have direct test coverage.
2005
+
2006
+ ### Changed
2007
+
2008
+ - **`as any` audit** — eliminated 10 casts in `validate.ts` by introducing a typed `FormFieldElement` local binding. The remaining 8 instances (pdf-lib interop, dynamic import, internal back-references) are now documented with one-line comments explaining the constraint.
2009
+ - **Comparison article** (`docs/articles/pretext-pdf-vs-pdfmake-2026.md`) — 2,200-word draft covering feature matrix, typography quality, API design, performance, and migration quick-start. Marked `published: false` pending live demo.
2010
+ - **Migration guide** (`docs/MIGRATION_FROM_PDFMAKE.md`)30+ pdfmake pretext-pdf mappings, complete before/after invoice example, and a quick-start checklist. Linked from README.
2011
+
2012
+ ### Fixed
2013
+
2014
+ - **Phase 2F test types** — `fontWeight: 700 as 700` cast in pre-constructed rows array; removed non-existent `creationDate` from `DocumentMetadata`; replaced `allowCopying: false` with correct `encryption: { permissions: { copying: false } }`.
2015
+ - **StackBlitz integration** — added `.stackblitzrc` so WebContainer auto-runs `npm start` and opens the browser preview on port 3000.
2016
+
2017
+ ---
2018
+
2019
+ ## [0.5.3] — 2026-04-16
2020
+
2021
+ ### Changed
2022
+
2023
+ - **Upgraded `@chenglou/pretext` from 0.0.3 to 0.0.5** — picks up improved text analysis accuracy (~35% larger analysis module), better measurement precision, extracted bidi-data module for cleaner tree-shaking, and new `rich-inline` export (not yet used by pretext-pdf). No breaking changes — `prepareWithSegments()` and `layoutWithLines()` APIs are unchanged. All 223 tests pass, 3 example PDFs visually verified (RTL, TOC, hyperlinks).
2024
+
2025
+ ---
2026
+
2027
+ ## [0.5.2] — 2026-04-13
2028
+
2029
+ ### Added
2030
+
2031
+ - **`onImageLoadError` callback on `PdfDocument`** gives callers control over image load failures. Return `'skip'` to silently omit the image (preserves existing default behavior). Return `'throw'` to abort rendering with the original error. Previously, all image failures were silently downgraded to `console.warn` with no way to detect them programmatically.
2032
+
2033
+ ```typescript
2034
+ await render({
2035
+ content: [...],
2036
+ onImageLoadError: (src, error) => {
2037
+ myLogger.warn('Image skipped', { src, error })
2038
+ return 'skip' // or 'throw' to abort
2039
+ }
2040
+ })
2041
+ ```
2042
+
2043
+ ---
2044
+
2045
+ ## [0.4.0]2026-04-08
2046
+
2047
+ ### Breaking Changes
2048
+
2049
+ - **Migrated from `pdf-lib` to `@cantoo/pdf-lib`** `@cantoo/pdf-lib` is now a direct `dependency` (always installed). Previously it was an optional peer dependency required only for encryption. This removes the `ENCRYPTION_NOT_AVAILABLE` error code and the separate `npm install @cantoo/pdf-lib` installation step. Encryption now works out of the box.
2050
+ - **`ENCRYPTION_NOT_AVAILABLE` error code removed** — encryption is now always available. Update any `switch` statements that handled this code.
2051
+
2052
+ ### Why this change
2053
+
2054
+ `pdf-lib` (the original) has not received a meaningful commit since November 2021. `@cantoo/pdf-lib` is the actively maintained fork (v2.6.5, 107+ releases, MIT license). pretext-pdf was already using `@cantoo/pdf-lib` for encryption — this commit makes it the single source of truth for all PDF operations.
2055
+
2056
+ ### Added
2057
+
2058
+ - `test/pretext-api-contract.test.ts` canary test that asserts `@chenglou/pretext` exports the exact functions pretext-pdf depends on. Breaks loudly if pretext changes its API.
2059
+ - `docs/ROADMAP.md` public multi-phase development plan
2060
+
2061
+ ### Changed
2062
+
2063
+ - `@chenglou/pretext` version pinned to exact `0.0.3` (no caret) — prevents surprise breaking changes from upstream auto-updates
2064
+ - `test:contract` script added runs the pretext API contract test before the full test suite
2065
+ - All internal comments updated from `pdf-lib` to `@cantoo/pdf-lib`
2066
+
2067
+ ---
2068
+
2069
+ ## [0.3.1] — 2026-04-08
2070
+
2071
+ ### Fixed
2072
+
2073
+ - **Critical: Font resolution when installed as npm package** `@fontsource/inter` is now resolved via `createRequire(import.meta.url)` instead of a hardcoded relative path. Previously, `path.join(__dirname, '..', 'node_modules', '@fontsource', 'inter', ...)` failed when npm hoisted the dependency to the consumer's top-level `node_modules`, causing `FONT_LOAD_FAILED` on every install. Now resolves correctly regardless of npm hoisting behavior.
2074
+
2075
+ ---
2076
+
2077
+ ## [0.3.0] 2026-04-08
2078
+
2079
+ ### Added (Phase 8BInteractive Forms)
2080
+
2081
+ - New `form-field` element type creates interactive AcroForm fields in PDFs
2082
+ - `fieldType: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'button'`
2083
+ - `label` renders above the field as static text
2084
+ - Text fields: `defaultValue`, `multiline`, `placeholder`, `maxLength`
2085
+ - Checkboxes: `checked` initial state
2086
+ - Radio groups and dropdowns: `options` array, `defaultSelected`
2087
+ - `doc.flattenForms: true` bakes all fields into static content
2088
+ - Custom `borderColor`, `backgroundColor`, `width`, `height`, `fontSize` per field
2089
+ - New error codes: `FORM_FIELD_NAME_DUPLICATE` (duplicate `name` across fields), `FORM_FLATTEN_FAILED`
2090
+ - Post-render `form.updateFieldAppearances()` ensures proper display in all PDF readers
2091
+ - 10 comprehensive tests covering all form field types
2092
+
2093
+ ### Added (Phase 8ESignature Placeholder)
2094
+
2095
+ - `doc.signature` visual signature box drawn on a specified page
2096
+ - Fields: `signerName`, `reason`, `location`, `x`, `y`, `width`, `height`, `page`, `borderColor`, `fontSize`
2097
+ - Draws signature line, date line, and optional text inside a bordered rectangle
2098
+ - `page` is 0-indexed, defaults to last page, clamps gracefully if out of range
2099
+ - 6 comprehensive tests
2100
+
2101
+ ### Added (Phase 8D Callout Boxes)
2102
+
2103
+ - New `callout` element type — styled highlight box with optional title
2104
+ - Preset styles: `style: 'info'` (blue), `'warning'` (amber), `'tip'` (green), `'note'` (gray)
2105
+ - Optional `title` rendered bold above content with left border accent
2106
+ - Fully customizable: `backgroundColor`, `borderColor`, `color`, `titleColor`, `padding`
2107
+ - Paginates correctly across pages (reuses blockquote pagination logic)
2108
+ - 8 comprehensive tests
2109
+
2110
+ ### Added (Phase 8F Document Metadata Extensions)
2111
+
2112
+ - `doc.metadata.language` sets PDF `/Lang` catalog entry (BCP47 tag e.g. `'en-US'`, `'hi'`)
2113
+ - `doc.metadata.producer` sets PDF producer field (e.g. `'MyApp v2.1'`)
2114
+ - Both fields validate as non-empty strings
2115
+ - 5 comprehensive tests
2116
+
2117
+ ---
2118
+
2119
+ ## [0.2.0] — 2026-04-08
2120
+
2121
+ ### Added (Phase 8H Inline Formatting)
2122
+
2123
+ - `verticalAlign: 'superscript' | 'subscript'` on `InlineSpan` in rich-paragraphs
2124
+ - Superscript renders at 65% font size, baseline shifted up by 40% of font size
2125
+ - Subscript renders at 65% font size, baseline shifted down by 20% of font size
2126
+ - `letterSpacing?: number` on `ParagraphElement`, `HeadingElement`, `RichParagraphElement` — extra pt between characters
2127
+ - `smallCaps?: boolean` on those same three element types — simulated via uppercase + 80% fontSize
2128
+ - Character-by-character rendering for letterSpacing (pdf-lib has no native spacing param)
2129
+ - 8 comprehensive tests covering all inline formatting functionality
2130
+
2131
+ ### Added (Phase 8A Annotations/Comments)
2132
+
2133
+ - New `comment` element type — sticky note annotation at position in document
2134
+ - `annotation?: AnnotationSpec` on `ParagraphElement` and `HeadingElement` — attach note to element
2135
+ - Supports: `contents`, `author`, `color` (hex), `open` (popup default state)
2136
+ - Uses PDF `Subtype: 'Text'` annotation (sticky note icon in PDF viewers)
2137
+ - 8 comprehensive tests covering all annotation functionality
2138
+
2139
+ ### Added (Phase 8C — Document Assembly)
2140
+
2141
+ - New `merge(pdfs: Uint8Array[])` exported function combine pre-rendered PDFs
2142
+ - New `assemble(parts: AssemblyPart[])` exported function — mix rendered docs + existing PDFs
2143
+ - `AssemblyPart` interface: `{ doc?: PdfDocument, pdf?: Uint8Array }`
2144
+ - New error codes: `ASSEMBLY_EMPTY`, `ASSEMBLY_FAILED`
2145
+ - 8 comprehensive tests covering all assembly functionality
2146
+
2147
+ ### Fixed
2148
+
2149
+ - **CI case-sensitivity bug**: `test/phase-7-integration.test.ts` used `'en-US'` (uppercase) for hyphenation language. On Linux CI (case-sensitive filesystem) this failed with `UNSUPPORTED_LANGUAGE`. Changed to `'en-us'` to match package name `hyphenation.en-us`.
2150
+
2151
+ ---
2152
+
2153
+ ## [0.1.1] — 2026-04-08
2154
+
2155
+ ### Added
2156
+
2157
+ - **Phase 8G: Hyperlinks** Complete link annotation support:
2158
+ - `paragraph.url` for external URI links on paragraphs
2159
+ - `heading.url` for external URI links on headings
2160
+ - `heading.anchor` for named PDF destinations (internal cross-references)
2161
+ - `InlineSpan.href` for external and internal `#anchorId` links in rich-paragraphs
2162
+ - `mailto:` scheme support for email links
2163
+ - GoTo annotations for internal anchor references
2164
+ - 9 comprehensive tests covering all hyperlink functionality
2165
+
2166
+ ### Fixed
2167
+
2168
+ - **Memory leak in test suite**: Removed module-level `_hypherCache` in `src/measure.ts` that accumulated ~188KB per language across 255+ test runs. Changed from cached Hypher instances to fresh instances per call (negligible performance impact, massive memory savings).
2169
+ - **Node.js version compatibility**: Replaced `--experimental-strip-types` with `tsx` runner to support Node.js 18.x, 20.x, and 22.x in CI
2170
+ - **Broken CI examples**: Removed references to non-existent Phase 8 example scripts from GitHub Actions workflow
2171
+ - **README examples mismatch**: Updated Examples section to only list 5 existing Phase 7 examples (watermark, bookmarks, toc, rtl, encryption)
2172
+ - **Test suite OOM issues**: Split large test files (paginate.test.ts) into paginate-basic.test.ts to work around Node.js `--experimental-strip-types` heap exhaustion bug on files >17KB
2173
+
2174
+ ### Changed
2175
+
2176
+ - `test:unit` now runs only `test/paginate-basic.test.ts` (fast, no canvas overhead)
2177
+ - Reorganized test scripts: `test:unit`, `test:validate`, `test:e2e`, `test:phases` for better memory management
2178
+ - Moved internal planning documentation to archive (preserved, not published)
2179
+ - `devDependencies`: Added `@napi-rs/canvas` explicitly (was missing, causing CI failures)
2180
+
2181
+ ### Added
2182
+
2183
+ - `CONTRIBUTING.md`: Development setup, TDD workflow, PR process guide
2184
+ - `CHENG_LOU_EMAIL_DRAFT.md`: Template for requesting endorsement from pretext creator
2185
+ - `examples/comparison-pdfmake.ts`: pdfmake version of invoice for typography comparison
2186
+
2187
+ ---
2188
+
2189
+ ## [0.1.0] 2026-04-07
2190
+
2191
+ ### Added (Phase 7G Encryption)
2192
+
2193
+ - `doc.encryption` configuration for password-protecting PDFs
2194
+ - User password and owner password support
2195
+ - Granular permission restrictions: printing, copying, modifying, annotating
2196
+ - Lazy-loads `@cantoo/pdf-lib` (optional peer dependency) — zero cost when not used
2197
+ - Error code: `ENCRYPTION_NOT_AVAILABLE` when encryption is requested but dependency not installed
2198
+
2199
+ ### Added (Phase 7F RTL Text Support)
2200
+
2201
+ - Right-to-left text support for Arabic, Hebrew, and other RTL languages
2202
+ - Unicode bidirectional text algorithm via `bidi-js`
2203
+ - `dir` attribute on text elements: `'ltr'` | `'rtl'` | `'auto'` for per-element control
2204
+ - RTL text works with headings, paragraphs, lists, tables, and all text elements
2205
+ - Automatic detection of mixed LTR/RTL content
2206
+
2207
+ ### Added (Phase 7E SVG Support)
2208
+
2209
+ - `{ type: 'svg', svg: '<...' }` element for embedding SVG graphics
2210
+ - SVG rasterization via `@napi-rs/canvas`
2211
+ - ViewBox auto-sizing: automatic height calculation from viewBox aspect ratio
2212
+ - Explicit sizing: `width` and `height` parameters for precise control
2213
+ - Alignment options: `align: 'left' | 'center' | 'right'`
2214
+ - Multi-page support: SVGs paginate correctly across page breaks
2215
+ - Error code: `SVG_RENDER_FAILED` for SVG rasterization errors
2216
+
2217
+ ### Added (Phase 7D Table of Contents)
2218
+
2219
+ - `{ type: 'toc' }` element for automatic TOC generation
2220
+ - Two-pass rendering pipeline ensures accurate page numbers
2221
+ - Configurable: `title`, `showTitle`, `minLevel`/`maxLevel`, dot leaders, level indentation
2222
+ - Auto-indexed from heading structure (H1, H2, H3, etc.)
2223
+ - Supports custom formatting via `fontSize`, `color`, `spaceAfter` parameters
2224
+
2225
+ ### Added (Phase 7C Hyphenation)
2226
+
2227
+ - Automatic word hyphenation for better justified text layout
2228
+ - `doc.hyphenation: { language: 'en-US' }` for document-level config
2229
+ - Liang's algorithm via `hypher` package for accurate break points
2230
+ - Configurable: `minWordLength`, `leftMin`, `rightMin`, per-element `hyphenate: false` opt-out
2231
+ - Language support: includes `hyphenation.en-us` (additional languages via npm packages)
2232
+ - Error code: `UNSUPPORTED_LANGUAGE` when language not available
2233
+
2234
+ ### Added (Phase 7B — Watermarks)
2235
+
2236
+ - `doc.watermark` for text or image watermarks on every page
2237
+ - Text watermarks: `text`, `fontSize`, `fontWeight`, `color`, `opacity`, `rotation`
2238
+ - Image watermarks: `image` (Uint8Array), `opacity`, `rotation`, `color` (tint)
2239
+ - Watermarks render behind content (lower z-index)
2240
+ - Rotation bounds: -360 ≤ rotation ≤ 360 degrees
2241
+ - Validation: must provide either text or image, never both required
2242
+
2243
+ ### Added (Phase 7A — Bookmarks / PDF Outline)
2244
+
2245
+ - PDF sidebar bookmarks auto-generated from heading structure
2246
+ - Enabled by default: `bookmarks: true` or `bookmarks: { minLevel: 1, maxLevel: 3 }`
2247
+ - Level filtering: include/exclude heading levels from outline
2248
+ - Per-heading opt-out: `bookmark: false` on heading elements
2249
+ - Keyboard navigation: Cmd/Ctrl+Opt/Alt+O in PDF readers to toggle bookmark sidebar
2250
+
2251
+ ### Added (Phase 6 — Advanced Features)
2252
+
2253
+ - Header and footer support with {{pageNumber}} and {{totalPages}} tokens
2254
+ - Text decoration: strikethrough, underline
2255
+ - Text alignment: left, center, right, justify
2256
+ - Line height control: custom line-height multipliers
2257
+ - Column layout with multi-column content flow
2258
+ - Tables with colspan/rowspan support
2259
+
2260
+ ### Added (Phase 5 — Rich Text / Builder API)
2261
+
2262
+ - Fluent builder API for programmatic document construction
2263
+ - Rich text element with nested formatting (bold, italic, links)
2264
+ - Inline code and code blocks with syntax highlighting
2265
+ - Block quotes with custom styling
2266
+ - Horizontal rules (hr element)
2267
+ - Numbered and bulleted lists with nesting
2268
+
2269
+ ### Added (Phases 1–4 — Core Engine)
2270
+
2271
+ - Core PDF generation via `pdf-lib`
2272
+ - Element types: paragraph, heading, table, image, list, code, blockquote
2273
+ - Font support: Inter bundled, custom TTF embedding
2274
+ - Document metadata: title, author, subject, keywords, created date
2275
+ - Page sizing: A4, A3, A5, Letter, Legal, or custom dimensions
2276
+ - Margins: top, bottom, left, right per page
2277
+ - Multi-page pagination with orphan/widow control
2278
+ - Image formats: PNG, JPG, WebP
2279
+ - Table features: custom column widths, cell padding, borders, header styling
2280
+ - Colors: hex color codes throughout (text, backgrounds, borders)