pretext-pdf 1.0.9 → 1.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 (45) hide show
  1. package/CHANGELOG.md +973 -913
  2. package/README.md +2 -2
  3. package/UPSTREAM.md +112 -0
  4. package/dist/cli.js +19 -19
  5. package/dist/measure-text.d.ts +1 -1
  6. package/dist/measure-text.d.ts.map +1 -1
  7. package/dist/measure-text.js +1 -1
  8. package/dist/measure-text.js.map +1 -1
  9. package/dist/rich-text.js +1 -1
  10. package/dist/rich-text.js.map +1 -1
  11. package/dist/validate.js +5 -2
  12. package/dist/validate.js.map +1 -1
  13. package/dist/vendor/pretext/analysis.d.ts +35 -0
  14. package/dist/vendor/pretext/analysis.d.ts.map +1 -0
  15. package/dist/vendor/pretext/analysis.js +1162 -0
  16. package/dist/vendor/pretext/analysis.js.map +1 -0
  17. package/dist/vendor/pretext/bidi.d.ts +2 -0
  18. package/dist/vendor/pretext/bidi.d.ts.map +1 -0
  19. package/dist/vendor/pretext/bidi.js +176 -0
  20. package/dist/vendor/pretext/bidi.js.map +1 -0
  21. package/dist/vendor/pretext/generated/bidi-data.d.ts +5 -0
  22. package/dist/vendor/pretext/generated/bidi-data.d.ts.map +1 -0
  23. package/dist/vendor/pretext/generated/bidi-data.js +980 -0
  24. package/dist/vendor/pretext/generated/bidi-data.js.map +1 -0
  25. package/dist/vendor/pretext/layout.d.ts +75 -0
  26. package/dist/vendor/pretext/layout.d.ts.map +1 -0
  27. package/dist/vendor/pretext/layout.js +448 -0
  28. package/dist/vendor/pretext/layout.js.map +1 -0
  29. package/dist/vendor/pretext/line-break.d.ts +43 -0
  30. package/dist/vendor/pretext/line-break.d.ts.map +1 -0
  31. package/dist/vendor/pretext/line-break.js +908 -0
  32. package/dist/vendor/pretext/line-break.js.map +1 -0
  33. package/dist/vendor/pretext/line-text.d.ts +5 -0
  34. package/dist/vendor/pretext/line-text.d.ts.map +1 -0
  35. package/dist/vendor/pretext/line-text.js +69 -0
  36. package/dist/vendor/pretext/line-text.js.map +1 -0
  37. package/dist/vendor/pretext/measurement.d.ts +31 -0
  38. package/dist/vendor/pretext/measurement.d.ts.map +1 -0
  39. package/dist/vendor/pretext/measurement.js +251 -0
  40. package/dist/vendor/pretext/measurement.js.map +1 -0
  41. package/dist/vendor/pretext/rich-inline.d.ts +53 -0
  42. package/dist/vendor/pretext/rich-inline.d.ts.map +1 -0
  43. package/dist/vendor/pretext/rich-inline.js +327 -0
  44. package/dist/vendor/pretext/rich-inline.js.map +1 -0
  45. package/package.json +212 -212
package/CHANGELOG.md CHANGED
@@ -1,913 +1,973 @@
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
- ## [1.0.9] — 2026-05-06
11
-
12
- Test coverage Phase 2: filling blind spots in the CLI, the pdfmake compat shim, and
13
- the performance regression guard. Adds c8 coverage tooling for measurability.
14
-
15
- ### Added
16
-
17
- - **`test/cli.test.ts`** (+13 tests) End-to-end coverage for the `pretext-pdf` CLI binary,
18
- spawning `dist/cli.js` as a subprocess. Covers argument parsing (`--version`, `--help`,
19
- `-i/-o/--markdown/--code-font`, positional fallback, unknown flags), JSON and Markdown
20
- input modes, stdin/stdout piping, and exit codes 0/1/2.
21
-
22
- - **`test/compat.test.ts`** (+34 tests)Coverage for the pdfmake pretext-pdf
23
- translation shim (`fromPdfmake`). Covers page setup (pageSize string and object,
24
- pageMargins scalar/2-tuple/4-tuple, orientation), styles (defaultStyle, named styles,
25
- headingMap override), all content node types (string, `text`, `ul`/`ol` with nesting,
26
- `table` with header rows, `image`, `qr`, `pageBreak`, `stack`), header/footer string
27
- forms, integration render, and unsupported nodes (`columns`, `canvas`).
28
-
29
- - **c8 coverage tooling** — `npm run coverage` (text + lcov reporters) and
30
- `npm run coverage:check` (75/65/75 thresholds, non-blocking in CI initially).
31
- Configuration in `.c8rc.json` excludes type-only files and CLI from instrumentation.
32
- Coverage step added to CI as `continue-on-error: true` while baseline thresholds
33
- are calibrated.
34
-
35
- ### Fixed
36
-
37
- - **`test/benchmark-baseline.test.ts`: regression guard now actually guards** —
38
- Replaced the prior "TODO: enable when baseline is calibrated" stub (which collected
39
- timings but asserted nothing) with a real 3x-baseline-with-500ms-floor budget per
40
- corpus. Missing corpora in the baseline JSON now `assert.fail()` instead of silently
41
- defaulting to a zero budget that would mask any regression.
42
-
43
- - **CONTRIBUTING.md: removed stale "(676 tests)" annotation** — Test count drift bait;
44
- the README badge already auto-verifies via `verify:badges`.
45
-
46
- ### Changed
47
-
48
- - **Test runner now builds first** Added `pretest:unit: npm run build` so contributors
49
- running `npm run test:unit` always get a fresh dist; the new CLI tests spawn the
50
- compiled binary and would otherwise fail with a confusing module-not-found error.
51
-
52
- ---
53
-
54
- ## [1.0.8] — 2026-05-06
55
-
56
- Public API contract integrity: the `RenderOptions.logger` option now actually does what
57
- its JSDoc has always promised, and `@napi-rs/canvas` no longer auto-installs.
58
-
59
- ### Fixed
60
-
61
- - **`RenderOptions.logger` now routes warnings from asset loading and rendering** —
62
- Previously only validation warnings respected the `logger` option. Now all advisory
63
- warnings from `loadImages` (image load, image embed, QR/barcode/chart skipped, plugin
64
- loadAsset failed, watermark image skipped 7 call sites) and `renderDocument` (form
65
- field render failure) flow through `logger.warn` when one is provided. Bidi-js fallback
66
- warnings from RTL reordering remain on `console.warn`; the JSDoc on `RenderOptions.logger`
67
- has been updated to document the actual scope honestly.
68
-
69
- - **Missing `[pretext-pdf]` log prefix on bidi-js error path** — One `console.warn` in
70
- `measure-text.ts` was logging without the canonical `[pretext-pdf]` prefix, making it
71
- hard to identify the library as the source in consumer logs. Now consistent.
72
-
73
- ### Changed
74
-
75
- - **`@napi-rs/canvas` removed from `optionalDependencies`** — Was double-listed in both
76
- `peerDependencies` (with `optional: true`) and `optionalDependencies`. The latter caused
77
- npm to attempt installing the native canvas binary on every install, including in
78
- edge/serverless environments where the platform may not be supported and the dep is
79
- not needed. Now only listed under `peerDependencies` — install it explicitly when you
80
- need SVG/QR/barcode/chart rasterization in Node.
81
-
82
- ### Documentation
83
-
84
- - **README security callout for `allowedFileDirs`** — Added a prominent callout in the
85
- Quick Start section. The default behavior allows `image.src` and `svg.src` to read any
86
- absolute file path, which is a path-traversal vector when document JSON originates from
87
- user input or an LLM. The callout now appears immediately after the first `render()`
88
- example.
89
-
90
- ---
91
-
92
- ## [1.0.7] 2026-05-05
93
-
94
- Picks up pretext fork v0.0.6-patched.2: 8 additional upstream PRs (11 total).
95
-
96
- ### Fixed
97
-
98
- - **German low opening quote `„` no longer breaks at line-start on hyphenation path** —
99
- `KINSOKU_START_FORBIDDEN` in `src/measure-text.ts` now includes U+201E (`„`), matching
100
- pretext PR #165 which fixed the non-hyphenation path. Previously `„` could appear
101
- at the start of a wrapped line when hyphenation was active.
102
-
103
- - **Currency symbols stay glued to adjacent numbers** — Upstream PR #105 (cherry-picked in
104
- `v0.0.6-patched.2`) prevents `$`, `€`, `£`, `₹` etc. from line-breaking away from
105
- the number they annotate.
106
-
107
- - **Trailing collapsible-space reconstruction fixed** — Upstream PR #29 fix (extended in
108
- v0.0.6-patched.2): a word followed by a space that exactly fills `maxWidth` no longer
109
- drops the space from line boundary cursors, preventing Arabic/mixed-script text from
110
- losing inter-word spaces during reconstruction.
111
-
112
- ### Changed
113
-
114
- - **`@chenglou/pretext` dependency** — Bumped from `v0.0.6-patched` to `v0.0.6-patched.2`
115
- (GitHub fork, 11 upstream PRs total). Adds: CJK overflow prevention (PR #132),
116
- fit-advance cache fix (PR #161), rich inline stats unification (PR #138),
117
- chunk layout side table O(1) lookup (PR #140), bidi surrogate handling (PR #3),
118
- skip no-op merge passes (PR #119), currency stickiness (PR #105),
119
- German quote fix (PR #165), and trailing-space reconstruction (PR #29).
120
-
121
- ---
122
-
123
- ## [1.0.6] 2026-05-04
124
-
125
- Audit bug fixes: validator correctness, internal export hygiene, schema gaps, README accuracy.
126
-
127
- ### Fixed
128
-
129
- - **lineHeight upper-bound cap removed** `validate()` no longer rejects `lineHeight > 20`. The
130
- field is in points (pt), not a multiplier; 36pt is valid for a large heading. The `> 20` cap in
131
- `paragraph`, `heading`, and `defaultParagraphStyle` validators has been removed. The lower-bound
132
- check (lineHeight >= fontSize) is preserved.
133
-
134
- - **form-field error messages use `${prefix}` format** — Error messages from the `form-field` case
135
- now follow the `content[N] (form-field): ...` format used by all other element types, instead of
136
- the old `[N] form-field.` prefix.
137
-
138
- - **`assertUnknownProps` hint punctuation fixed** The "unknown property" message previously
139
- produced `unknown property. did you mean "color"` (period before hint). Fixed to
140
- `unknown property; did you mean "color"` — no period, semicolon separator.
141
-
142
- - **British "colour" → "color" in JSDoc** — Two `QrCodeElement` field comments
143
- (`foreground`, `background`) and the `ValidationError.path` JSDoc example corrected.
144
-
145
- - **`TocEntryElement`, `RichLine`, `RichFragment` removed from public exports** These types are
146
- marked `@internal` in `types-public.ts` and should not be part of the npm API surface. Removed
147
- from `src/index.ts`.
148
-
149
- - **Signature error includes original cause** — `SIGNATURE_FAILED` now preserves the underlying
150
- error message: `PDF signing failed: <original message>` instead of a static string.
151
-
152
- - **Header-only table now valid** `validate()` previously rejected tables where all rows are
153
- headers (`headerRowCount === rows.length`). Changed `>=` to `>`: tables where every row is a
154
- header are valid (useful for column-label-only tables).
155
-
156
- - **Dead sub-condition removed in `float-group` floatWidth guard** — `fg.floatWidth <= 0` was a
157
- dead branch (any value `<= 0` is already `< 30`). Removed to clarify intent.
158
-
159
- - **`warningCount` JSDoc updated** Documents that the validator currently only emits errors, so
160
- `warningCount` is always 0 (reserved for future use).
161
-
162
- - **`validateDocument` no longer re-throws unexpected errors** — Non-`PretextPdfError` exceptions
163
- (e.g. circular JSON, unexpected runtime errors) are now caught and returned as a structured
164
- `ValidationResult` instead of propagating. `validateDocument` now always returns, never throws.
165
-
166
- ### Changed (Schema additions — `pretext-pdf/schema`)
167
-
168
- - `qrCodeSchema`: added `margin` field.
169
- - `imageSchema`: added `floatFontSize`, `floatFontFamily`, `floatColor` fields.
170
- - `codeSchema`: added `dir` and `highlightTheme` fields.
171
- - `tableSchema`: added `dir`, `headerRows`, and cell-level `dir`, `fontFamily`, `fontSize`,
172
- `tabularNumbers` fields.
173
-
174
- ### Docs
175
-
176
- - README: `highlight.js` added to optional peer dependencies table.
177
- - README: `validate_document` added to MCP server tool list.
178
-
179
- ---
180
-
181
- ## [1.0.5] — 2026-05-04
182
-
183
- Schema coverage completion, `ValidationResult.warningCount`, and README API docs.
184
-
185
- ### Added
186
-
187
- - **`ValidationResult.warningCount`** — `validateDocument()` now returns `warningCount` alongside
188
- `errorCount`. Computed by filtering `errors[]` by `severity === 'warning'`. MCP consumers no
189
- longer need to derive it client-side.
190
-
191
- - **JSON Schema: remaining field coverage** `src/schema.ts` now covers all previously missing
192
- fields across 9 element types:
193
- - `inlineSpanSchema`: `dir`
194
- - `paragraphSchema`: `columns`, `columnGap`, `tabularNumbers`, `hyphenate`
195
- - `headingSchema`: `tabularNumbers`, `hyphenate`
196
- - `blockquoteSchema`: `lineHeight`, `padding`, `paddingH`, `paddingV`, `underline`, `strikethrough`
197
- - `calloutSchema`: `titleColor`, `fontWeight`, `lineHeight`, `padding`, `paddingH`, `paddingV`
198
- - `listSchema`: `lineHeight`, `markerWidth`, `itemSpaceAfter`, `nestedNumberingStyle`; nested
199
- items now carry `dir` and have a typed inner schema
200
- - `tocSchema`: `titleFontSize`, `levelIndent`, `leader`, `entrySpacing`
201
- - `formFieldSchema`: `borderColor`, `backgroundColor`, `keepTogether`, `defaultSelected`
202
- - `richParagraphSchema`: `columns`, `columnGap`, `tabularNumbers`
203
-
204
- - **README: `validateDocument` and `pretext-pdf/schema` documented** — both entry points now have
205
- `### API reference` sections with code examples.
206
-
207
- ---
208
-
209
- ## [1.0.4]2026-05-04
210
-
211
- Schema export hardening: post-release audit fixes addressing coverage gaps and a
212
- malformed dialect URI.
213
-
214
- ### Fixed
215
-
216
- - **`pretext-pdf/schema`: `$schema` dialect URI corrected** — was
217
- `https://json-schema.org/draft/2020-12` (not a registered URI), now
218
- `https://json-schema.org/draft/2020-12/schema`. Strict JSON Schema validators
219
- (AJV, Smithery, VS Code) will now correctly identify the dialect.
220
- - **`pretext-pdf/schema`: `hr` element spacing fields** `spaceAbove` and
221
- `spaceBelow` (the primary documented fields, default 12) were missing.
222
- `spaceBefore` and `spaceAfter` are now correctly marked as aliases.
223
- - **`pretext-pdf/schema`: `float-group` and `chart` element types** — both
224
- first-class public element types were missing from the `content.items.anyOf`
225
- list. Schema-driven tooling will now know they exist.
226
-
227
- ### Added (schema coverage)
228
-
229
- - `pdfDocumentSchema.sections` page-range header/footer overrides
230
- - `headingSchema.annotation` annotation field (was already on paragraph)
231
- - `tableSchema.cellPaddingH` / `cellPaddingV` primary table density controls
232
- - `imageSchema.floatWidth` / `floatGap` / `floatSpans` — column-layout controls
233
- for floated images
234
-
235
- ---
236
-
237
- ## [1.0.3] 2026-05-03
238
-
239
- Enhancements: JSON Schema export, simplified marked peer dep range, and internal API polish.
240
-
241
- ### Added
242
-
243
- - **`pretext-pdf/schema` entry point** — exports `pdfDocumentSchema`, a machine-readable JSON Schema
244
- object describing the full `PdfDocument` type. Covers all 22 element types and 18 top-level
245
- document properties. Intended for editor tooling, MCP clients, and Smithery UI form generation.
246
-
247
- ```typescript
248
- import { pdfDocumentSchema } from 'pretext-pdf/schema'
249
- ```
250
-
251
- ### Changed
252
-
253
- - **`marked` peer dependency simplified** — `^9.0.0 || ^10.0.0 || ... || ^18.0.0` condensed to
254
- `>=9.0.0`. Semantically identical, cleaner npm output.
255
-
256
- - **`validateDocument` logger option** `options.logger` now passed to the underlying `validate()`
257
- call via conditional spread, respecting `exactOptionalPropertyTypes: true` constraints.
258
-
259
- ### Fixed
260
-
261
- - **`fonts.ts` unsafe cast removed** — `(spec as { style?: string }).style` replaced with direct
262
- property access on the widened parameter type.
263
-
264
- ---
265
-
266
- ## [1.0.2] — 2026-05-03
267
-
268
- ### Added
269
-
270
- - `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.
271
- - `ValidationError` and `ValidationResult` exported from the public API surface.
272
- - `Logger` interface and `logger?: Logger` in `RenderOptions` — route diagnostic warnings through a custom logger instead of `console.warn`.
273
- - 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.
274
-
275
- ---
276
-
277
- ## [1.0.1] 2026-05-02
278
-
279
- Patch: strict mode correctness fixes. No API changes.
280
-
281
- ### Fixed
282
-
283
- - **`levenshteinDist` early-exit bug** — per-cell `if (curr[j]! > 2) return 999` inside
284
- the inner DP loop fired on intermediate cells, causing d=1 pairs like `hrefs→href` and
285
- `spaceafter→spaceAfter` to incorrectly return 999 instead of 1. Fix: removed the per-cell
286
- guard; final check only (`prev[n]! > 2 ? 999 : prev[n]!`).
287
- - **Seven path-prefix annotations** — strict-mode error paths had `(type)` suffixes
288
- (e.g. `doc(table).rows[0]`) that no other validator used and that tests didn't expect.
289
- All seven removed so paths are plain dot-notation.
290
- - **`encryption` block not strict-checked** unknown props inside `doc.encryption`
291
- were silently accepted in strict mode. Now validated against `ALLOWED_PROPS_SUB['encryption']`.
292
- - **Root path was `'document'` not `'doc'`**top-level `assertUnknownProps` was called
293
- with `'document'` as the path prefix, producing paths like `document.content[0]` instead
294
- of `doc.content[0]`. Corrected to `'doc'`.
295
- - **Suggestion format mismatched** — `Did you mean 'x'?` → `did you mean "x"` (lowercase,
296
- double-quotes) to match the format tests asserted.
297
- - **`formatErrors` missing header** multi-error output now begins with
298
- `Strict validation failed (N issues):\n` so callers can detect strict vs. regular errors.
299
-
300
- ### Tests
301
-
302
- - Added `test/validate-strict.test.ts` (35 tests) to `test:unit` script — these tests were
303
- written but not wired into CI in v1.0.0.
304
-
305
- ---
306
-
307
- ## [1.0.0] — 2026-05-02
308
-
309
- First stable release. Completes the plugin extension API, closes all v1.0 gate requirements,
310
- and ships a fully verified public surface with zero breaking changes from 0.9.x.
311
-
312
- ### Added
313
-
314
- - **Plugin extension API** Register custom element types via `RenderOptions.plugins`.
315
- Each `PluginDefinition` participates in all four pipeline stages: `validate`, `loadAsset`,
316
- `measure`, and `render`. Plugins are fully typed and tree-shaken from documents
317
- that don't use them. See README § Custom element types (plugins) and
318
- `examples/plugin-custom-element.ts` for a runnable example.
319
- - **`PluginDefinition`, `PluginMeasureContext`, `PluginMeasureResult`, `PluginRenderContext`**
320
- exported from `pretext-pdf` public surface (previously internal).
321
- - **`PdfBuilder` and `PdfBuilderOptions`** exported from `pretext-pdf` (enables type-safe
322
- builder construction in downstream code without re-declaring the interface).
323
- - **`TocEntryElement`** exported from `pretext-pdf` public surface (was in the `ContentElement`
324
- union but not individually importable).
325
- - **`plugins` option on `createPdf()`** — `PdfBuilderOptions.plugins` threads plugins through
326
- the builder's `build()` call automatically.
327
- - **`Intl.Segmenter` pre-flight guard** in `render()` — throws `RENDER_FAILED` with a clear
328
- message on Node.js < 16 or runtimes without full-ICU data, instead of silently producing
329
- incorrect line breaks.
330
- - **`PluginRenderContext.pageWidth/pageHeight/margins`**render hooks now receive full page
331
- geometry for layout calculations (page-relative positioning, bleed boxes, etc.).
332
- - **`render` context Y-coordinate docs**expanded JSDoc with multi-line text example showing
333
- how to position text baselines relative to `context.y`.
334
- - **Benchmark corpora manifest** and **smoke staging** tests wired into `npm test`
335
- (previously orphaned).
336
- - **`test/table-determinism.test.ts`** — asserts that table pagination produces identical
337
- layout traces across repeated invocations of `prepareLayoutState`.
338
- - **`test/validate-strict.test.ts`** (35 tests) — comprehensive contract for `strict: true`
339
- validation covering all element types, nested structures, Levenshtein suggestions, error
340
- message format, doc-level and sub-structure prop checks. Total test count: 676.
341
- - `examples/plugin-custom-element.ts` — runnable plugin example (`npm run example:plugin`).
342
-
343
- ### Fixed
344
-
345
- - **`SIGNATURE_CERT_AND_ENCRYPTION` error code** was declared in the `ErrorCode` union
346
- but never thrown; validate.ts now uses it correctly when a document specifies both
347
- signatures and encryption (previously threw a generic `VALIDATION_ERROR`).
348
- - **Build break under `exactOptionalPropertyTypes: true`** `PdfBuilder.build()` no longer
349
- passes `{ plugins: undefined }` to `runPipeline` when no plugins are configured.
350
- - **Plugin `validate` hook empty-string normalization** — `plugin.validate()` returning `''`
351
- now correctly accepts the element (was previously treated as a rejection message).
352
- - **`toc` element reaching render default arm** — `render.ts` now has an explicit
353
- `case 'toc': return` guard before the default arm; TOC elements are pre-processed
354
- during pagination and should never reach the renderer.
355
- - **`RichLine` and `RichFragment`** demoted from `@public` to `@internal`; these are
356
- implementation details of the rich-text pipeline, not intended for external use.
357
- - **Sentinel value documentation** — `MeasuredBlock` comment now explicitly states that
358
- `lines: []`, `fontSize: 0`, `lineHeight: 0`, `fontKey: ''` applies to spacers, tables,
359
- images, hr, *and plugin blocks* — not a bug but a documented convention.
360
-
361
- ### Internal
362
-
363
- - `src/plugin-registry.ts` (new): Pure orchestration helpers for the four plugin injection
364
- points (`findPlugin`, `runPluginValidate`, `runPluginLoadAsset`, `runPluginMeasure`,
365
- `runPluginRender`).
366
- - `src/plugin-types.ts` (new): `PluginDefinition` interface and context/result types.
367
- - `src/layout-state.ts`: `prepareLayoutState` now accepts `options?: RenderOptions` and
368
- threads plugins to `stageValidate`, `stageLoadAssets`, and `stageMeasure`.
369
- - `docs/V1.0-RUNBOOK.md`: Full release runbook with first-principles audit, anti-hallucination
370
- protocol, verified-facts table, and phase-by-phase plan.
371
-
372
- ---
373
-
374
- ## [0.9.4]2026-05-02
375
-
376
- > **Note:** This release was never published to npm as a standalone tag. All changes listed
377
- > here shipped as part of [1.0.0] on the same date.
378
-
379
- Architecture hardening + API surface snapshot. No public API changes; internal
380
- restructuring to eliminate circular dependencies and add drift guards before v1.0 freeze.
381
-
382
- ### Added
383
-
384
- - **API surface snapshot** (`etc/pretext-pdf.api.md`) checked into source control as
385
- the v1.0 baseline. The `api:check` CI step will fail on unintentional public-API drift.
386
- - **`src/layout-state.ts`** `prepareLayoutState()` and `summarizeLayoutState()` extracted
387
- from the pipeline for testability; `layout-contract` and `hard-text-contract` tests
388
- wired into `test:unit`.
389
- - **`src/benchmarks/corpora.ts`** — benchmark corpus manifest (`getBenchmarkCorpora()`)
390
- restored from git history; `benchmark-baseline.test.ts` wired into `test:contract`.
391
- - **Drift guards** (`test/drift-guards.test.ts`) asserts that `ELEMENT_TYPES`,
392
- `ALLOWED_PROPS`, `validate.ts` cases, and `render.ts` cases all agree at test time.
393
- Catches any future element-type addition that isn't plumbed through all four places.
394
- - **`render.ts` default arm** unknown element types now throw immediately instead of
395
- silently producing a blank block.
396
-
397
- ### Refactored
398
-
399
- - **Circular dependency broken**: `src/post-process.ts` extracted so `builder.ts` and
400
- `index.ts` no longer form a cycle through each other.
401
- - **`ELEMENT_TYPES` extracted** to `src/element-types.ts` as single source of truth;
402
- re-exported from `index.ts`, imported by `validate.ts` — eliminates the previous
403
- per-file string-literal duplication.
404
-
405
- ### Fixed
406
-
407
- - `post-process.ts`: drop raw signing library error message from `SIGNATURE_FAILED`
408
- to avoid leaking certificate or passphrase details in error output.
409
- - `layout-state.ts`: polyfill install wrapped in try/catch; throws `CANVAS_UNAVAILABLE`
410
- on failure instead of an untyped exception.
411
-
412
- ---
413
-
414
- ## [0.9.3] 2026-04-23
415
-
416
- Strict validation release. Opt-in property validation to catch unknown properties on elements and sub-structures via typo detection and precise JSONPath error reporting.
417
-
418
- ### Added
419
-
420
- - **Strict validation mode**: Pass `{ strict: true }` to `render(doc, options)` to reject unknown properties. Non-strict mode (default) remains permissive for backwards compatibility.
421
- - **`render()` options parameter**: Updated signature to `render(doc: PdfDocument, options?: RenderOptions)` where `RenderOptions = { strict?: boolean }`.
422
- - **`validate()` public export**: `validate()` is now exported from `pretext-pdf` for standalone validation and testing.
423
- - **Validation error details**:
424
- - Unknown properties reported with Levenshtein edit-distance suggestions (distance ≤2) for typo correction.
425
- - Errors include JSONPath-like paths (`content[3].table.rows[0].cells[1].align`) for precise location reporting.
426
- - Error accumulation: all violations collected before throwing a single VALIDATION_ERROR with formatted multi-line message.
427
- - First 20 errors shown; overflow indicator present.
428
- - **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.
429
- - **Property allowlists**:
430
- - `ALLOWED_PROPS`: 22 element types (paragraph, heading, table, image, code, list, etc.)
431
- - `ALLOWED_PROPS_SUB`: 8 sub-structures (document, metadata, table-row, table-cell, list-item, inline-span, column-def, annotation)
432
-
433
- ### Internal
434
-
435
- - `src/allowed-props.ts` (new): Central configuration for allowed properties with compile-time assertions.
436
- - `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).
437
-
438
- ---
439
-
440
- ## [0.9.2] — 2026-04-22
441
-
442
- Maintenance release. Engine refresh + repo-hygiene automation. No runtime behavior changes beyond the `@chenglou/pretext` bump.
443
-
444
- ### Changed
445
-
446
- - **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.
447
-
448
- ### Fixed
449
-
450
- - **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.
451
-
452
- ### Added
453
-
454
- - `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.
455
- - `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.)
456
- - `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.
457
-
458
- ### Removed
459
-
460
- - `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`.
461
- - `src/brain/` inert auto-logger artifact (34 blank-body entries, no active writer). Never published to npm.
462
-
463
- ### Docs
464
-
465
- - `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.
466
-
467
- ---
468
-
469
- ## [0.9.1] 2026-04-21
470
-
471
- 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.
472
-
473
- ### Fixed
474
-
475
- - **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.
476
- - **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.
477
- - **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.
478
-
479
- ### Added / hardened
480
-
481
- - **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`.
482
- - **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.
483
- - **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.
484
- - **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.
485
- - **Extracted `LINK_COLOR_DEFAULT`** constant in `src/rich-text.ts`.
486
-
487
- ### Tests
488
-
489
- - `test/rich-text.test.ts` 20 → 23 (+3): block-start leading whitespace preserved; leading whitespace after hard break preserved; ZWJ preservation.
490
- - `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.
491
- - Full suite: 624 tests, 100% pass.
492
-
493
- ### Chore / docs
494
-
495
- - Removed `brain/learnings/*.md`, `docs/PLAN-v0.6-v1.0.md`, `test/paginate.test.ts.archive` internal dev artifacts not for the public repo.
496
- - Stripped `Phase N:` nomenclature from `src/` comments (pure rename no logic delta).
497
- - Added `demo/stackblitz/.stackblitzrc`, `docs/articles/pretext-pdf-vs-pdfmake-2026.md` (draft).
498
- - 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.
499
- - README test badge corrected `650+ → 600+` (verified: 624 tests total).
500
-
501
- ---
502
-
503
- ## [0.9.0] — 2026-04-20
504
-
505
- Three additive enhancements that broaden the package's surface without growing its mandatory dependency footprint.
506
-
507
- ### Added
508
-
509
- - **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).
510
- - **`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.
511
- - **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.
512
- - **Markdown: GFM task lists** — `- [x] done` and `- [ ] todo` render with ☑ / ☐ Unicode markers prepended to the item text.
513
-
514
- ### Tests
515
-
516
- - 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).
517
-
518
- ### Notes
519
-
520
- - 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.
521
- - `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.
522
-
523
- ---
524
-
525
- ## [0.8.3] 2026-04-20
526
-
527
- ### Security
528
-
529
- - **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 `::`.
530
- - **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.
531
-
532
- ### Fixed
533
-
534
- - **`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"`.
535
- - **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.
536
- - **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.
537
-
538
- ### Tests
539
-
540
- - New `test/v0.8.3-ssrf.test.ts` covers 11 IPv4-mapped IPv6 bypass cases, IPv6 unspecified/loopback regressions, and HTTP rejection.
541
- - Extended `test/phase-10c-markdown.test.ts` with regressions for 3-level nesting and paragraph-typed list items.
542
- - Extended `test/phase-10d-templates.test.ts` with the sub-rupee amount-in-words case.
543
-
544
- ---
545
-
546
- ## [0.8.2] — 2026-04-20
547
-
548
- ### Fixed
549
-
550
- - **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.
551
- - The fast path (no trailing whitespace) is unchanged — single pretext call. Slow path adds two pretext calls per affected token, with one cached.
552
-
553
- ### Tests
554
-
555
- - 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.
556
-
557
- ---
558
-
559
- ## [0.8.1] 2026-04-20
560
-
561
- ### Fixed
562
-
563
- - **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.
564
- - **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`).
565
-
566
- ### Notes for browser users
567
-
568
- - 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.
569
- - 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.
570
-
571
- ---
572
-
573
- ## [0.8.0] — 2026-04-19
574
-
575
- ### Added
576
-
577
- - **`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.
578
- - **`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.
579
- - **`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.
580
- - **`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.
581
- - **`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).
582
- - **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`.
583
-
584
- ---
585
-
586
- ## [0.7.2] — 2026-04-20
587
-
588
- Phase 11 cross-cutting enhancements. Retroactively attributed to 0.7.2; these features were
589
- originally left as `[Unreleased]` and published out of chronological order after 0.7.1.
590
-
591
- ### Added
592
-
593
- - **`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).
594
- - **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'`.
595
- - **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.
596
- - **`onFormFieldError` callback** `doc.onFormFieldError: (name, err) => 'skip' | 'throw'` mirrors `onImageLoadError`. Controls render behaviour when a form field fails.
597
- - **`createFootnoteSet(defs)`** — helper exported from `pretext-pdf` that generates footnote definition/reference pairs with globally unique IDs. Returns `Array<{ id, def }>`.
598
- - **`renderDate` field** — `doc.renderDate: Date | string` overrides the PDF creation date. Useful for reproducible builds and testing.
599
- - **`{{date}}` and `{{author}}` tokens** in header/footer text — join existing `{{pageNumber}}` / `{{totalPages}}`. `{{date}}` resolves from `renderDate`; `{{author}}` resolves from `doc.metadata.author`.
600
- - **`tabularNumbers`** on `rich-paragraph` digits rendered at uniform slot width (widest digit in font), so columns of numbers align without OpenType TNUM feature.
601
- - **`smallCaps` + `letterSpacing` per span** `InlineSpan.smallCaps` and `InlineSpan.letterSpacing` now respected in `rich-paragraph` rendering.
602
- - **Per-span `fontSize`** — `InlineSpan.fontSize` overrides the element-level font size for that span. Enables mixed-size text in a single paragraph.
603
-
604
- ### Fixed
605
-
606
- - `resolveTokens()` used `.replace()` (replaces first occurrence only) changed to `.replaceAll()` for all four tokens.
607
- - Table span grid: continuation-row cursor was advancing by 1 instead of `colspan` when skipping a spanned column — now advances by full span width.
608
- - Font family names now validated for safe characters (`/^[a-zA-Z0-9 _-]+$/`) in `requireFamily()` — rejects null bytes and control characters.
609
- - Annotation `color` and `author` fields now validated in `validateElement()` for both `paragraph` and `heading` annotations.
610
- - `buildOutlineTree` memoizes `parentIdxOf()` into a pre-computed array eliminates O(n²) scan for documents with large heading counts.
611
- - Table grid-line renderer pre-computes active boundary seteliminates O(rows × cols) inner loop for large tables.
612
- - `addLinkAnnotation()` re-validates URL scheme at render time (defense-in-depth; `validate.ts` is the primary gate).
613
-
614
- ---
615
-
616
- ## [0.7.1] — 2026-04-19
617
-
618
- ### Changed
619
-
620
- - **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.
621
-
622
- ### Fixed
623
-
624
- - **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.
625
- - **3 phase-11 list tests corrected** — test data incorrectly contained 3-level nesting while named "2-level"; data trimmed to match documented contract.
626
-
627
- ---
628
-
629
- ## [0.7.0] 2026-04-17
630
-
631
- ### Added
632
-
633
- - **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.
634
- - **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.
635
- - **`## Performance` section in README** — measured render times and PDF sizes for 1-page, 10-page, and mixed-element documents. Font subsetting behaviour documented.
636
- - **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.
637
- - **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.
638
-
639
- ### Changed
640
-
641
- - **`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.
642
- - **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.
643
- - **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.
644
-
645
- ### Fixed
646
-
647
- - **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 } }`.
648
- - **StackBlitz integration** — added `.stackblitzrc` so WebContainer auto-runs `npm start` and opens the browser preview on port 3000.
649
-
650
- ---
651
-
652
- ## [0.5.3] — 2026-04-16
653
-
654
- ### Changed
655
-
656
- - **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).
657
-
658
- ---
659
-
660
- ## [0.5.2]2026-04-13
661
-
662
- ### Added
663
-
664
- - **`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.
665
-
666
- ```typescript
667
- await render({
668
- content: [...],
669
- onImageLoadError: (src, error) => {
670
- myLogger.warn('Image skipped', { src, error })
671
- return 'skip' // or 'throw' to abort
672
- }
673
- })
674
- ```
675
-
676
- ---
677
-
678
- ## [0.4.0] — 2026-04-08
679
-
680
- ### Breaking Changes
681
-
682
- - **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.
683
- - **`ENCRYPTION_NOT_AVAILABLE` error code removed** — encryption is now always available. Update any `switch` statements that handled this code.
684
-
685
- ### Why this change
686
-
687
- `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.
688
-
689
- ### Added
690
-
691
- - `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.
692
- - `docs/ROADMAP.md` — public multi-phase development plan
693
-
694
- ### Changed
695
-
696
- - `@chenglou/pretext` version pinned to exact `0.0.3` (no caret) prevents surprise breaking changes from upstream auto-updates
697
- - `test:contract` script added runs the pretext API contract test before the full test suite
698
- - All internal comments updated from `pdf-lib` to `@cantoo/pdf-lib`
699
-
700
- ---
701
-
702
- ## [0.3.1]2026-04-08
703
-
704
- ### Fixed
705
-
706
- - **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.
707
-
708
- ---
709
-
710
- ## [0.3.0] — 2026-04-08
711
-
712
- ### Added (Phase 8B Interactive Forms)
713
-
714
- - New `form-field` element type — creates interactive AcroForm fields in PDFs
715
- - `fieldType: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'button'`
716
- - `label` renders above the field as static text
717
- - Text fields: `defaultValue`, `multiline`, `placeholder`, `maxLength`
718
- - Checkboxes: `checked` initial state
719
- - Radio groups and dropdowns: `options` array, `defaultSelected`
720
- - `doc.flattenForms: true` bakes all fields into static content
721
- - Custom `borderColor`, `backgroundColor`, `width`, `height`, `fontSize` per field
722
- - New error codes: `FORM_FIELD_NAME_DUPLICATE` (duplicate `name` across fields), `FORM_FLATTEN_FAILED`
723
- - Post-render `form.updateFieldAppearances()` ensures proper display in all PDF readers
724
- - 10 comprehensive tests covering all form field types
725
-
726
- ### Added (Phase 8E — Signature Placeholder)
727
-
728
- - `doc.signature` — visual signature box drawn on a specified page
729
- - Fields: `signerName`, `reason`, `location`, `x`, `y`, `width`, `height`, `page`, `borderColor`, `fontSize`
730
- - Draws signature line, date line, and optional text inside a bordered rectangle
731
- - `page` is 0-indexed, defaults to last page, clamps gracefully if out of range
732
- - 6 comprehensive tests
733
-
734
- ### Added (Phase 8D — Callout Boxes)
735
-
736
- - New `callout` element type — styled highlight box with optional title
737
- - Preset styles: `style: 'info'` (blue), `'warning'` (amber), `'tip'` (green), `'note'` (gray)
738
- - Optional `title` rendered bold above content with left border accent
739
- - Fully customizable: `backgroundColor`, `borderColor`, `color`, `titleColor`, `padding`
740
- - Paginates correctly across pages (reuses blockquote pagination logic)
741
- - 8 comprehensive tests
742
-
743
- ### Added (Phase 8FDocument Metadata Extensions)
744
-
745
- - `doc.metadata.language` sets PDF `/Lang` catalog entry (BCP47 tag e.g. `'en-US'`, `'hi'`)
746
- - `doc.metadata.producer` — sets PDF producer field (e.g. `'MyApp v2.1'`)
747
- - Both fields validate as non-empty strings
748
- - 5 comprehensive tests
749
-
750
- ---
751
-
752
- ## [0.2.0]2026-04-08
753
-
754
- ### Added (Phase 8H — Inline Formatting)
755
-
756
- - `verticalAlign: 'superscript' | 'subscript'` on `InlineSpan` in rich-paragraphs
757
- - Superscript renders at 65% font size, baseline shifted up by 40% of font size
758
- - Subscript renders at 65% font size, baseline shifted down by 20% of font size
759
- - `letterSpacing?: number` on `ParagraphElement`, `HeadingElement`, `RichParagraphElement` — extra pt between characters
760
- - `smallCaps?: boolean` on those same three element types — simulated via uppercase + 80% fontSize
761
- - Character-by-character rendering for letterSpacing (pdf-lib has no native spacing param)
762
- - 8 comprehensive tests covering all inline formatting functionality
763
-
764
- ### Added (Phase 8A — Annotations/Comments)
765
-
766
- - New `comment` element type sticky note annotation at position in document
767
- - `annotation?: AnnotationSpec` on `ParagraphElement` and `HeadingElement` — attach note to element
768
- - Supports: `contents`, `author`, `color` (hex), `open` (popup default state)
769
- - Uses PDF `Subtype: 'Text'` annotation (sticky note icon in PDF viewers)
770
- - 8 comprehensive tests covering all annotation functionality
771
-
772
- ### Added (Phase 8CDocument Assembly)
773
-
774
- - New `merge(pdfs: Uint8Array[])` exported functioncombine pre-rendered PDFs
775
- - New `assemble(parts: AssemblyPart[])` exported function mix rendered docs + existing PDFs
776
- - `AssemblyPart` interface: `{ doc?: PdfDocument, pdf?: Uint8Array }`
777
- - New error codes: `ASSEMBLY_EMPTY`, `ASSEMBLY_FAILED`
778
- - 8 comprehensive tests covering all assembly functionality
779
-
780
- ### Fixed
781
-
782
- - **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`.
783
-
784
- ---
785
-
786
- ## [0.1.1]2026-04-08
787
-
788
- ### Added
789
-
790
- - **Phase 8G: Hyperlinks** Complete link annotation support:
791
- - `paragraph.url` for external URI links on paragraphs
792
- - `heading.url` for external URI links on headings
793
- - `heading.anchor` for named PDF destinations (internal cross-references)
794
- - `InlineSpan.href` for external and internal `#anchorId` links in rich-paragraphs
795
- - `mailto:` scheme support for email links
796
- - GoTo annotations for internal anchor references
797
- - 9 comprehensive tests covering all hyperlink functionality
798
-
799
- ### Fixed
800
-
801
- - **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).
802
- - **Node.js version compatibility**: Replaced `--experimental-strip-types` with `tsx` runner to support Node.js 18.x, 20.x, and 22.x in CI
803
- - **Broken CI examples**: Removed references to non-existent Phase 8 example scripts from GitHub Actions workflow
804
- - **README examples mismatch**: Updated Examples section to only list 5 existing Phase 7 examples (watermark, bookmarks, toc, rtl, encryption)
805
- - **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
806
-
807
- ### Changed
808
-
809
- - `test:unit` now runs only `test/paginate-basic.test.ts` (fast, no canvas overhead)
810
- - Reorganized test scripts: `test:unit`, `test:validate`, `test:e2e`, `test:phases` for better memory management
811
- - Moved internal planning documentation to archive (preserved, not published)
812
- - `devDependencies`: Added `@napi-rs/canvas` explicitly (was missing, causing CI failures)
813
-
814
- ### Added
815
-
816
- - `CONTRIBUTING.md`: Development setup, TDD workflow, PR process guide
817
- - `CHENG_LOU_EMAIL_DRAFT.md`: Template for requesting endorsement from pretext creator
818
- - `examples/comparison-pdfmake.ts`: pdfmake version of invoice for typography comparison
819
-
820
- ---
821
-
822
- ## [0.1.0] 2026-04-07
823
-
824
- ### Added (Phase 7GEncryption)
825
-
826
- - `doc.encryption` configuration for password-protecting PDFs
827
- - User password and owner password support
828
- - Granular permission restrictions: printing, copying, modifying, annotating
829
- - Lazy-loads `@cantoo/pdf-lib` (optional peer dependency) zero cost when not used
830
- - Error code: `ENCRYPTION_NOT_AVAILABLE` when encryption is requested but dependency not installed
831
-
832
- ### Added (Phase 7FRTL Text Support)
833
-
834
- - Right-to-left text support for Arabic, Hebrew, and other RTL languages
835
- - Unicode bidirectional text algorithm via `bidi-js`
836
- - `dir` attribute on text elements: `'ltr'` | `'rtl'` | `'auto'` for per-element control
837
- - RTL text works with headings, paragraphs, lists, tables, and all text elements
838
- - Automatic detection of mixed LTR/RTL content
839
-
840
- ### Added (Phase 7E — SVG Support)
841
-
842
- - `{ type: 'svg', svg: '<...' }` element for embedding SVG graphics
843
- - SVG rasterization via `@napi-rs/canvas`
844
- - ViewBox auto-sizing: automatic height calculation from viewBox aspect ratio
845
- - Explicit sizing: `width` and `height` parameters for precise control
846
- - Alignment options: `align: 'left' | 'center' | 'right'`
847
- - Multi-page support: SVGs paginate correctly across page breaks
848
- - Error code: `SVG_RENDER_FAILED` for SVG rasterization errors
849
-
850
- ### Added (Phase 7DTable of Contents)
851
-
852
- - `{ type: 'toc' }` element for automatic TOC generation
853
- - Two-pass rendering pipeline ensures accurate page numbers
854
- - Configurable: `title`, `showTitle`, `minLevel`/`maxLevel`, dot leaders, level indentation
855
- - Auto-indexed from heading structure (H1, H2, H3, etc.)
856
- - Supports custom formatting via `fontSize`, `color`, `spaceAfter` parameters
857
-
858
- ### Added (Phase 7C — Hyphenation)
859
-
860
- - Automatic word hyphenation for better justified text layout
861
- - `doc.hyphenation: { language: 'en-US' }` for document-level config
862
- - Liang's algorithm via `hypher` package for accurate break points
863
- - Configurable: `minWordLength`, `leftMin`, `rightMin`, per-element `hyphenate: false` opt-out
864
- - Language support: includes `hyphenation.en-us` (additional languages via npm packages)
865
- - Error code: `UNSUPPORTED_LANGUAGE` when language not available
866
-
867
- ### Added (Phase 7B — Watermarks)
868
-
869
- - `doc.watermark` for text or image watermarks on every page
870
- - Text watermarks: `text`, `fontSize`, `fontWeight`, `color`, `opacity`, `rotation`
871
- - Image watermarks: `image` (Uint8Array), `opacity`, `rotation`, `color` (tint)
872
- - Watermarks render behind content (lower z-index)
873
- - Rotation bounds: -360 ≤ rotation ≤ 360 degrees
874
- - Validation: must provide either text or image, never both required
875
-
876
- ### Added (Phase 7A Bookmarks / PDF Outline)
877
-
878
- - PDF sidebar bookmarks auto-generated from heading structure
879
- - Enabled by default: `bookmarks: true` or `bookmarks: { minLevel: 1, maxLevel: 3 }`
880
- - Level filtering: include/exclude heading levels from outline
881
- - Per-heading opt-out: `bookmark: false` on heading elements
882
- - Keyboard navigation: Cmd/Ctrl+Opt/Alt+O in PDF readers to toggle bookmark sidebar
883
-
884
- ### Added (Phase 6Advanced Features)
885
-
886
- - Header and footer support with {{pageNumber}} and {{totalPages}} tokens
887
- - Text decoration: strikethrough, underline
888
- - Text alignment: left, center, right, justify
889
- - Line height control: custom line-height multipliers
890
- - Column layout with multi-column content flow
891
- - Tables with colspan/rowspan support
892
-
893
- ### Added (Phase 5 — Rich Text / Builder API)
894
-
895
- - Fluent builder API for programmatic document construction
896
- - Rich text element with nested formatting (bold, italic, links)
897
- - Inline code and code blocks with syntax highlighting
898
- - Block quotes with custom styling
899
- - Horizontal rules (hr element)
900
- - Numbered and bulleted lists with nesting
901
-
902
- ### Added (Phases 1–4 Core Engine)
903
-
904
- - Core PDF generation via `pdf-lib`
905
- - Element types: paragraph, heading, table, image, list, code, blockquote
906
- - Font support: Inter bundled, custom TTF embedding
907
- - Document metadata: title, author, subject, keywords, created date
908
- - Page sizing: A4, A3, A5, Letter, Legal, or custom dimensions
909
- - Margins: top, bottom, left, right per page
910
- - Multi-page pagination with orphan/widow control
911
- - Image formats: PNG, JPG, WebP
912
- - Table features: custom column widths, cell padding, borders, header styling
913
- - 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
+ ## [1.1.1] — 2026-05-08
11
+
12
+ ### Fixed
13
+
14
+ - **`validateDocument` fallback parser: path extraction** — `parseValidationErrorsStructured`
15
+ now correctly falls back to `path: "document"` for single-throw errors whose message
16
+ contains a sentence (e.g. `"margins.left must be a non-negative finite number. Got: -1"`).
17
+ Previously the heuristic accepted any text-before-colon that started with a letter,
18
+ producing a corrupted path like `"margins.left must be a non-negative finite number. Got"`.
19
+ Fix: reject candidates that contain `". "` (period + space), which only appears in
20
+ prose sentences, never in path expressions like `content[0] (paragraph) spans[0].href`.
21
+
22
+ - **README `runtime%20deps` badge**Updated from `8` to `7` to reflect the removal
23
+ of `@chenglou/pretext` from `dependencies` in v1.1.0.
24
+
25
+ - **`SECURITY.md` personal email removed** Replaced `akashchikara1998@gmail.com`
26
+ with the GitHub private vulnerability reporting URL.
27
+
28
+ ### Changed
29
+
30
+ - **CI matrix: Node 18.x removed** — Node 18 reached End of Life in April 2025.
31
+ The CI matrix now targets Node 20.x and 22.x only. The `engines.node` field in
32
+ `package.json` is updated to `>=20.0.0`. The Node 18 matrix slot was causing
33
+ flaky benchmark failures (EOL runners are slower) that killed the `&&` test chain
34
+ and caused the badge verifier to see a truncated test count.
35
+
36
+ ---
37
+
38
+ ## [1.1.0] 2026-05-07
39
+
40
+ Vendor `@chenglou/pretext` source directly into the package, eliminating the
41
+ GitHub URL dependency and all associated install risks (mutable tags, npm audit
42
+ gaps, network-only install, no SRI).
43
+
44
+ ### Changed
45
+
46
+ - **`@chenglou/pretext` is now vendored** — The upstream text-layout engine
47
+ (`src/vendor/pretext/`) is compiled as part of pretext-pdf itself. Consumers
48
+ no longer need to install `@chenglou/pretext`; the GitHub URL dependency has
49
+ been removed from `package.json`. The vendored snapshot is pinned to
50
+ `v0.0.6-patched.2` (commit `658edfec`) with 9 upstream PRs cherry-picked on
51
+ top of the `v0.0.6` release. See `UPSTREAM.md` for the full patch inventory
52
+ and upgrade procedure.
53
+
54
+ ### Added
55
+
56
+ - **`UPSTREAM.md`** Authoritative attribution and upgrade guide for the
57
+ vendored `@chenglou/pretext` source. Documents provenance, the 9 cherry-picked
58
+ upstream PRs (#3, #29, #105, #119, #132, #138, #140, #161, #165), which
59
+ commits are excluded from vendoring (fork infra), and the procedure for
60
+ updating when upstream publishes a new release.
61
+
62
+ ### Removed
63
+
64
+ - **`@chenglou/pretext` dependency**Removed from `dependencies`. The library
65
+ source is now bundled inside the package at `dist/vendor/pretext/`. No runtime
66
+ behavior change; the same patched code is used.
67
+
68
+ ---
69
+
70
+ ## [1.0.9] 2026-05-06
71
+
72
+ Test coverage Phase 2: filling blind spots in the CLI, the pdfmake compat shim, and
73
+ the performance regression guard. Adds c8 coverage tooling for measurability.
74
+
75
+ ### Added
76
+
77
+ - **`test/cli.test.ts`** (+13 tests) — End-to-end coverage for the `pretext-pdf` CLI binary,
78
+ spawning `dist/cli.js` as a subprocess. Covers argument parsing (`--version`, `--help`,
79
+ `-i/-o/--markdown/--code-font`, positional fallback, unknown flags), JSON and Markdown
80
+ input modes, stdin/stdout piping, and exit codes 0/1/2.
81
+
82
+ - **`test/compat.test.ts`** (+34 tests) — Coverage for the pdfmake → pretext-pdf
83
+ translation shim (`fromPdfmake`). Covers page setup (pageSize string and object,
84
+ pageMargins scalar/2-tuple/4-tuple, orientation), styles (defaultStyle, named styles,
85
+ headingMap override), all content node types (string, `text`, `ul`/`ol` with nesting,
86
+ `table` with header rows, `image`, `qr`, `pageBreak`, `stack`), header/footer string
87
+ forms, integration render, and unsupported nodes (`columns`, `canvas`).
88
+
89
+ - **c8 coverage tooling** — `npm run coverage` (text + lcov reporters) and
90
+ `npm run coverage:check` (75/65/75 thresholds, non-blocking in CI initially).
91
+ Configuration in `.c8rc.json` excludes type-only files and CLI from instrumentation.
92
+ Coverage step added to CI as `continue-on-error: true` while baseline thresholds
93
+ are calibrated.
94
+
95
+ ### Fixed
96
+
97
+ - **`test/benchmark-baseline.test.ts`: regression guard now actually guards** —
98
+ Replaced the prior "TODO: enable when baseline is calibrated" stub (which collected
99
+ timings but asserted nothing) with a real 3x-baseline-with-500ms-floor budget per
100
+ corpus. Missing corpora in the baseline JSON now `assert.fail()` instead of silently
101
+ defaulting to a zero budget that would mask any regression.
102
+
103
+ - **CONTRIBUTING.md: removed stale "(676 tests)" annotation** — Test count drift bait;
104
+ the README badge already auto-verifies via `verify:badges`.
105
+
106
+ ### Changed
107
+
108
+ - **Test runner now builds first** Added `pretest:unit: npm run build` so contributors
109
+ running `npm run test:unit` always get a fresh dist; the new CLI tests spawn the
110
+ compiled binary and would otherwise fail with a confusing module-not-found error.
111
+
112
+ ---
113
+
114
+ ## [1.0.8] 2026-05-06
115
+
116
+ Public API contract integrity: the `RenderOptions.logger` option now actually does what
117
+ its JSDoc has always promised, and `@napi-rs/canvas` no longer auto-installs.
118
+
119
+ ### Fixed
120
+
121
+ - **`RenderOptions.logger` now routes warnings from asset loading and rendering** —
122
+ Previously only validation warnings respected the `logger` option. Now all advisory
123
+ warnings from `loadImages` (image load, image embed, QR/barcode/chart skipped, plugin
124
+ loadAsset failed, watermark image skipped — 7 call sites) and `renderDocument` (form
125
+ field render failure) flow through `logger.warn` when one is provided. Bidi-js fallback
126
+ warnings from RTL reordering remain on `console.warn`; the JSDoc on `RenderOptions.logger`
127
+ has been updated to document the actual scope honestly.
128
+
129
+ - **Missing `[pretext-pdf]` log prefix on bidi-js error path** One `console.warn` in
130
+ `measure-text.ts` was logging without the canonical `[pretext-pdf]` prefix, making it
131
+ hard to identify the library as the source in consumer logs. Now consistent.
132
+
133
+ ### Changed
134
+
135
+ - **`@napi-rs/canvas` removed from `optionalDependencies`** Was double-listed in both
136
+ `peerDependencies` (with `optional: true`) and `optionalDependencies`. The latter caused
137
+ npm to attempt installing the native canvas binary on every install, including in
138
+ edge/serverless environments where the platform may not be supported and the dep is
139
+ not needed. Now only listed under `peerDependencies` install it explicitly when you
140
+ need SVG/QR/barcode/chart rasterization in Node.
141
+
142
+ ### Documentation
143
+
144
+ - **README — security callout for `allowedFileDirs`** — Added a prominent callout in the
145
+ Quick Start section. The default behavior allows `image.src` and `svg.src` to read any
146
+ absolute file path, which is a path-traversal vector when document JSON originates from
147
+ user input or an LLM. The callout now appears immediately after the first `render()`
148
+ example.
149
+
150
+ ---
151
+
152
+ ## [1.0.7]2026-05-05
153
+
154
+ Picks up pretext fork v0.0.6-patched.2: 8 additional upstream PRs (11 total).
155
+
156
+ ### Fixed
157
+
158
+ - **German low opening quote `„` no longer breaks at line-start on hyphenation path** —
159
+ `KINSOKU_START_FORBIDDEN` in `src/measure-text.ts` now includes U+201E (`„`), matching
160
+ pretext PR #165 which fixed the non-hyphenation path. Previously `„` could appear
161
+ at the start of a wrapped line when hyphenation was active.
162
+
163
+ - **Currency symbols stay glued to adjacent numbers** Upstream PR #105 (cherry-picked in
164
+ `v0.0.6-patched.2`) prevents `$`, `€`, `£`, `₹` etc. from line-breaking away from
165
+ the number they annotate.
166
+
167
+ - **Trailing collapsible-space reconstruction fixed** — Upstream PR #29 fix (extended in
168
+ v0.0.6-patched.2): a word followed by a space that exactly fills `maxWidth` no longer
169
+ drops the space from line boundary cursors, preventing Arabic/mixed-script text from
170
+ losing inter-word spaces during reconstruction.
171
+
172
+ ### Changed
173
+
174
+ - **`@chenglou/pretext` dependency** — Bumped from `v0.0.6-patched` to `v0.0.6-patched.2`
175
+ (GitHub fork, 11 upstream PRs total). Adds: CJK overflow prevention (PR #132),
176
+ fit-advance cache fix (PR #161), rich inline stats unification (PR #138),
177
+ chunk layout side table O(1) lookup (PR #140), bidi surrogate handling (PR #3),
178
+ skip no-op merge passes (PR #119), currency stickiness (PR #105),
179
+ German quote fix (PR #165), and trailing-space reconstruction (PR #29).
180
+
181
+ ---
182
+
183
+ ## [1.0.6] 2026-05-04
184
+
185
+ Audit bug fixes: validator correctness, internal export hygiene, schema gaps, README accuracy.
186
+
187
+ ### Fixed
188
+
189
+ - **lineHeight upper-bound cap removed** — `validate()` no longer rejects `lineHeight > 20`. The
190
+ field is in points (pt), not a multiplier; 36pt is valid for a large heading. The `> 20` cap in
191
+ `paragraph`, `heading`, and `defaultParagraphStyle` validators has been removed. The lower-bound
192
+ check (lineHeight >= fontSize) is preserved.
193
+
194
+ - **form-field error messages use `${prefix}` format** — Error messages from the `form-field` case
195
+ now follow the `content[N] (form-field): ...` format used by all other element types, instead of
196
+ the old `[N] form-field.` prefix.
197
+
198
+ - **`assertUnknownProps` hint punctuation fixed** The "unknown property" message previously
199
+ produced `unknown property. did you mean "color"` (period before hint). Fixed to
200
+ `unknown property; did you mean "color"` — no period, semicolon separator.
201
+
202
+ - **British "colour" "color" in JSDoc** — Two `QrCodeElement` field comments
203
+ (`foreground`, `background`) and the `ValidationError.path` JSDoc example corrected.
204
+
205
+ - **`TocEntryElement`, `RichLine`, `RichFragment` removed from public exports** — These types are
206
+ marked `@internal` in `types-public.ts` and should not be part of the npm API surface. Removed
207
+ from `src/index.ts`.
208
+
209
+ - **Signature error includes original cause** `SIGNATURE_FAILED` now preserves the underlying
210
+ error message: `PDF signing failed: <original message>` instead of a static string.
211
+
212
+ - **Header-only table now valid** — `validate()` previously rejected tables where all rows are
213
+ headers (`headerRowCount === rows.length`). Changed `>=` to `>`: tables where every row is a
214
+ header are valid (useful for column-label-only tables).
215
+
216
+ - **Dead sub-condition removed in `float-group` floatWidth guard** — `fg.floatWidth <= 0` was a
217
+ dead branch (any value `<= 0` is already `< 30`). Removed to clarify intent.
218
+
219
+ - **`warningCount` JSDoc updated** Documents that the validator currently only emits errors, so
220
+ `warningCount` is always 0 (reserved for future use).
221
+
222
+ - **`validateDocument` no longer re-throws unexpected errors** Non-`PretextPdfError` exceptions
223
+ (e.g. circular JSON, unexpected runtime errors) are now caught and returned as a structured
224
+ `ValidationResult` instead of propagating. `validateDocument` now always returns, never throws.
225
+
226
+ ### Changed (Schema additions — `pretext-pdf/schema`)
227
+
228
+ - `qrCodeSchema`: added `margin` field.
229
+ - `imageSchema`: added `floatFontSize`, `floatFontFamily`, `floatColor` fields.
230
+ - `codeSchema`: added `dir` and `highlightTheme` fields.
231
+ - `tableSchema`: added `dir`, `headerRows`, and cell-level `dir`, `fontFamily`, `fontSize`,
232
+ `tabularNumbers` fields.
233
+
234
+ ### Docs
235
+
236
+ - README: `highlight.js` added to optional peer dependencies table.
237
+ - README: `validate_document` added to MCP server tool list.
238
+
239
+ ---
240
+
241
+ ## [1.0.5] — 2026-05-04
242
+
243
+ Schema coverage completion, `ValidationResult.warningCount`, and README API docs.
244
+
245
+ ### Added
246
+
247
+ - **`ValidationResult.warningCount`** — `validateDocument()` now returns `warningCount` alongside
248
+ `errorCount`. Computed by filtering `errors[]` by `severity === 'warning'`. MCP consumers no
249
+ longer need to derive it client-side.
250
+
251
+ - **JSON Schema: remaining field coverage** — `src/schema.ts` now covers all previously missing
252
+ fields across 9 element types:
253
+ - `inlineSpanSchema`: `dir`
254
+ - `paragraphSchema`: `columns`, `columnGap`, `tabularNumbers`, `hyphenate`
255
+ - `headingSchema`: `tabularNumbers`, `hyphenate`
256
+ - `blockquoteSchema`: `lineHeight`, `padding`, `paddingH`, `paddingV`, `underline`, `strikethrough`
257
+ - `calloutSchema`: `titleColor`, `fontWeight`, `lineHeight`, `padding`, `paddingH`, `paddingV`
258
+ - `listSchema`: `lineHeight`, `markerWidth`, `itemSpaceAfter`, `nestedNumberingStyle`; nested
259
+ items now carry `dir` and have a typed inner schema
260
+ - `tocSchema`: `titleFontSize`, `levelIndent`, `leader`, `entrySpacing`
261
+ - `formFieldSchema`: `borderColor`, `backgroundColor`, `keepTogether`, `defaultSelected`
262
+ - `richParagraphSchema`: `columns`, `columnGap`, `tabularNumbers`
263
+
264
+ - **README: `validateDocument` and `pretext-pdf/schema` documented** — both entry points now have
265
+ `### API reference` sections with code examples.
266
+
267
+ ---
268
+
269
+ ## [1.0.4] — 2026-05-04
270
+
271
+ Schema export hardening: post-release audit fixes addressing coverage gaps and a
272
+ malformed dialect URI.
273
+
274
+ ### Fixed
275
+
276
+ - **`pretext-pdf/schema`: `$schema` dialect URI corrected** — was
277
+ `https://json-schema.org/draft/2020-12` (not a registered URI), now
278
+ `https://json-schema.org/draft/2020-12/schema`. Strict JSON Schema validators
279
+ (AJV, Smithery, VS Code) will now correctly identify the dialect.
280
+ - **`pretext-pdf/schema`: `hr` element spacing fields** — `spaceAbove` and
281
+ `spaceBelow` (the primary documented fields, default 12) were missing.
282
+ `spaceBefore` and `spaceAfter` are now correctly marked as aliases.
283
+ - **`pretext-pdf/schema`: `float-group` and `chart` element types** both
284
+ first-class public element types were missing from the `content.items.anyOf`
285
+ list. Schema-driven tooling will now know they exist.
286
+
287
+ ### Added (schema coverage)
288
+
289
+ - `pdfDocumentSchema.sections` page-range header/footer overrides
290
+ - `headingSchema.annotation` annotation field (was already on paragraph)
291
+ - `tableSchema.cellPaddingH` / `cellPaddingV` primary table density controls
292
+ - `imageSchema.floatWidth` / `floatGap` / `floatSpans`column-layout controls
293
+ for floated images
294
+
295
+ ---
296
+
297
+ ## [1.0.3]2026-05-03
298
+
299
+ Enhancements: JSON Schema export, simplified marked peer dep range, and internal API polish.
300
+
301
+ ### Added
302
+
303
+ - **`pretext-pdf/schema` entry point** exports `pdfDocumentSchema`, a machine-readable JSON Schema
304
+ object describing the full `PdfDocument` type. Covers all 22 element types and 18 top-level
305
+ document properties. Intended for editor tooling, MCP clients, and Smithery UI form generation.
306
+
307
+ ```typescript
308
+ import { pdfDocumentSchema } from 'pretext-pdf/schema'
309
+ ```
310
+
311
+ ### Changed
312
+
313
+ - **`marked` peer dependency simplified** — `^9.0.0 || ^10.0.0 || ... || ^18.0.0` condensed to
314
+ `>=9.0.0`. Semantically identical, cleaner npm output.
315
+
316
+ - **`validateDocument` logger option** `options.logger` now passed to the underlying `validate()`
317
+ call via conditional spread, respecting `exactOptionalPropertyTypes: true` constraints.
318
+
319
+ ### Fixed
320
+
321
+ - **`fonts.ts` unsafe cast removed** — `(spec as { style?: string }).style` replaced with direct
322
+ property access on the widened parameter type.
323
+
324
+ ---
325
+
326
+ ## [1.0.2] 2026-05-03
327
+
328
+ ### Added
329
+
330
+ - `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.
331
+ - `ValidationError` and `ValidationResult` exported from the public API surface.
332
+ - `Logger` interface and `logger?: Logger` in `RenderOptions` route diagnostic warnings through a custom logger instead of `console.warn`.
333
+ - 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.
334
+
335
+ ---
336
+
337
+ ## [1.0.1] 2026-05-02
338
+
339
+ Patch: strict mode correctness fixes. No API changes.
340
+
341
+ ### Fixed
342
+
343
+ - **`levenshteinDist` early-exit bug** — per-cell `if (curr[j]! > 2) return 999` inside
344
+ the inner DP loop fired on intermediate cells, causing d=1 pairs like `hrefs→href` and
345
+ `spaceafter→spaceAfter` to incorrectly return 999 instead of 1. Fix: removed the per-cell
346
+ guard; final check only (`prev[n]! > 2 ? 999 : prev[n]!`).
347
+ - **Seven path-prefix annotations** strict-mode error paths had `(type)` suffixes
348
+ (e.g. `doc(table).rows[0]`) that no other validator used and that tests didn't expect.
349
+ All seven removed so paths are plain dot-notation.
350
+ - **`encryption` block not strict-checked** — unknown props inside `doc.encryption`
351
+ were silently accepted in strict mode. Now validated against `ALLOWED_PROPS_SUB['encryption']`.
352
+ - **Root path was `'document'` not `'doc'`**top-level `assertUnknownProps` was called
353
+ with `'document'` as the path prefix, producing paths like `document.content[0]` instead
354
+ of `doc.content[0]`. Corrected to `'doc'`.
355
+ - **Suggestion format mismatched** — `Did you mean 'x'?` → `did you mean "x"` (lowercase,
356
+ double-quotes) to match the format tests asserted.
357
+ - **`formatErrors` missing header** — multi-error output now begins with
358
+ `Strict validation failed (N issues):\n` so callers can detect strict vs. regular errors.
359
+
360
+ ### Tests
361
+
362
+ - Added `test/validate-strict.test.ts` (35 tests) to `test:unit` script — these tests were
363
+ written but not wired into CI in v1.0.0.
364
+
365
+ ---
366
+
367
+ ## [1.0.0] 2026-05-02
368
+
369
+ First stable release. Completes the plugin extension API, closes all v1.0 gate requirements,
370
+ and ships a fully verified public surface with zero breaking changes from 0.9.x.
371
+
372
+ ### Added
373
+
374
+ - **Plugin extension API** Register custom element types via `RenderOptions.plugins`.
375
+ Each `PluginDefinition` participates in all four pipeline stages: `validate`, `loadAsset`,
376
+ `measure`, and `render`. Plugins are fully typed and tree-shaken from documents
377
+ that don't use them. See README § Custom element types (plugins) and
378
+ `examples/plugin-custom-element.ts` for a runnable example.
379
+ - **`PluginDefinition`, `PluginMeasureContext`, `PluginMeasureResult`, `PluginRenderContext`**
380
+ exported from `pretext-pdf` public surface (previously internal).
381
+ - **`PdfBuilder` and `PdfBuilderOptions`** exported from `pretext-pdf` (enables type-safe
382
+ builder construction in downstream code without re-declaring the interface).
383
+ - **`TocEntryElement`** exported from `pretext-pdf` public surface (was in the `ContentElement`
384
+ union but not individually importable).
385
+ - **`plugins` option on `createPdf()`** `PdfBuilderOptions.plugins` threads plugins through
386
+ the builder's `build()` call automatically.
387
+ - **`Intl.Segmenter` pre-flight guard** in `render()` throws `RENDER_FAILED` with a clear
388
+ message on Node.js < 16 or runtimes without full-ICU data, instead of silently producing
389
+ incorrect line breaks.
390
+ - **`PluginRenderContext.pageWidth/pageHeight/margins`** render hooks now receive full page
391
+ geometry for layout calculations (page-relative positioning, bleed boxes, etc.).
392
+ - **`render` context Y-coordinate docs** expanded JSDoc with multi-line text example showing
393
+ how to position text baselines relative to `context.y`.
394
+ - **Benchmark corpora manifest** and **smoke staging** tests wired into `npm test`
395
+ (previously orphaned).
396
+ - **`test/table-determinism.test.ts`** — asserts that table pagination produces identical
397
+ layout traces across repeated invocations of `prepareLayoutState`.
398
+ - **`test/validate-strict.test.ts`** (35 tests) — comprehensive contract for `strict: true`
399
+ validation covering all element types, nested structures, Levenshtein suggestions, error
400
+ message format, doc-level and sub-structure prop checks. Total test count: 676.
401
+ - `examples/plugin-custom-element.ts` runnable plugin example (`npm run example:plugin`).
402
+
403
+ ### Fixed
404
+
405
+ - **`SIGNATURE_CERT_AND_ENCRYPTION` error code** — was declared in the `ErrorCode` union
406
+ but never thrown; validate.ts now uses it correctly when a document specifies both
407
+ signatures and encryption (previously threw a generic `VALIDATION_ERROR`).
408
+ - **Build break under `exactOptionalPropertyTypes: true`** `PdfBuilder.build()` no longer
409
+ passes `{ plugins: undefined }` to `runPipeline` when no plugins are configured.
410
+ - **Plugin `validate` hook empty-string normalization** — `plugin.validate()` returning `''`
411
+ now correctly accepts the element (was previously treated as a rejection message).
412
+ - **`toc` element reaching render default arm** — `render.ts` now has an explicit
413
+ `case 'toc': return` guard before the default arm; TOC elements are pre-processed
414
+ during pagination and should never reach the renderer.
415
+ - **`RichLine` and `RichFragment`** demoted from `@public` to `@internal`; these are
416
+ implementation details of the rich-text pipeline, not intended for external use.
417
+ - **Sentinel value documentation** — `MeasuredBlock` comment now explicitly states that
418
+ `lines: []`, `fontSize: 0`, `lineHeight: 0`, `fontKey: ''` applies to spacers, tables,
419
+ images, hr, *and plugin blocks* — not a bug but a documented convention.
420
+
421
+ ### Internal
422
+
423
+ - `src/plugin-registry.ts` (new): Pure orchestration helpers for the four plugin injection
424
+ points (`findPlugin`, `runPluginValidate`, `runPluginLoadAsset`, `runPluginMeasure`,
425
+ `runPluginRender`).
426
+ - `src/plugin-types.ts` (new): `PluginDefinition` interface and context/result types.
427
+ - `src/layout-state.ts`: `prepareLayoutState` now accepts `options?: RenderOptions` and
428
+ threads plugins to `stageValidate`, `stageLoadAssets`, and `stageMeasure`.
429
+ - `docs/V1.0-RUNBOOK.md`: Full release runbook with first-principles audit, anti-hallucination
430
+ protocol, verified-facts table, and phase-by-phase plan.
431
+
432
+ ---
433
+
434
+ ## [0.9.4] — 2026-05-02
435
+
436
+ > **Note:** This release was never published to npm as a standalone tag. All changes listed
437
+ > here shipped as part of [1.0.0] on the same date.
438
+
439
+ Architecture hardening + API surface snapshot. No public API changes; internal
440
+ restructuring to eliminate circular dependencies and add drift guards before v1.0 freeze.
441
+
442
+ ### Added
443
+
444
+ - **API surface snapshot** (`etc/pretext-pdf.api.md`) checked into source control as
445
+ the v1.0 baseline. The `api:check` CI step will fail on unintentional public-API drift.
446
+ - **`src/layout-state.ts`** `prepareLayoutState()` and `summarizeLayoutState()` extracted
447
+ from the pipeline for testability; `layout-contract` and `hard-text-contract` tests
448
+ wired into `test:unit`.
449
+ - **`src/benchmarks/corpora.ts`** — benchmark corpus manifest (`getBenchmarkCorpora()`)
450
+ restored from git history; `benchmark-baseline.test.ts` wired into `test:contract`.
451
+ - **Drift guards** (`test/drift-guards.test.ts`) — asserts that `ELEMENT_TYPES`,
452
+ `ALLOWED_PROPS`, `validate.ts` cases, and `render.ts` cases all agree at test time.
453
+ Catches any future element-type addition that isn't plumbed through all four places.
454
+ - **`render.ts` default arm**unknown element types now throw immediately instead of
455
+ silently producing a blank block.
456
+
457
+ ### Refactored
458
+
459
+ - **Circular dependency broken**: `src/post-process.ts` extracted so `builder.ts` and
460
+ `index.ts` no longer form a cycle through each other.
461
+ - **`ELEMENT_TYPES` extracted** to `src/element-types.ts` as single source of truth;
462
+ re-exported from `index.ts`, imported by `validate.ts` — eliminates the previous
463
+ per-file string-literal duplication.
464
+
465
+ ### Fixed
466
+
467
+ - `post-process.ts`: drop raw signing library error message from `SIGNATURE_FAILED`
468
+ to avoid leaking certificate or passphrase details in error output.
469
+ - `layout-state.ts`: polyfill install wrapped in try/catch; throws `CANVAS_UNAVAILABLE`
470
+ on failure instead of an untyped exception.
471
+
472
+ ---
473
+
474
+ ## [0.9.3] — 2026-04-23
475
+
476
+ Strict validation release. Opt-in property validation to catch unknown properties on elements and sub-structures via typo detection and precise JSONPath error reporting.
477
+
478
+ ### Added
479
+
480
+ - **Strict validation mode**: Pass `{ strict: true }` to `render(doc, options)` to reject unknown properties. Non-strict mode (default) remains permissive for backwards compatibility.
481
+ - **`render()` options parameter**: Updated signature to `render(doc: PdfDocument, options?: RenderOptions)` where `RenderOptions = { strict?: boolean }`.
482
+ - **`validate()` public export**: `validate()` is now exported from `pretext-pdf` for standalone validation and testing.
483
+ - **Validation error details**:
484
+ - Unknown properties reported with Levenshtein edit-distance suggestions (distance ≤2) for typo correction.
485
+ - Errors include JSONPath-like paths (`content[3].table.rows[0].cells[1].align`) for precise location reporting.
486
+ - Error accumulation: all violations collected before throwing a single VALIDATION_ERROR with formatted multi-line message.
487
+ - First 20 errors shown; overflow indicator present.
488
+ - **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.
489
+ - **Property allowlists**:
490
+ - `ALLOWED_PROPS`: 22 element types (paragraph, heading, table, image, code, list, etc.)
491
+ - `ALLOWED_PROPS_SUB`: 8 sub-structures (document, metadata, table-row, table-cell, list-item, inline-span, column-def, annotation)
492
+
493
+ ### Internal
494
+
495
+ - `src/allowed-props.ts` (new): Central configuration for allowed properties with compile-time assertions.
496
+ - `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).
497
+
498
+ ---
499
+
500
+ ## [0.9.2] — 2026-04-22
501
+
502
+ Maintenance release. Engine refresh + repo-hygiene automation. No runtime behavior changes beyond the `@chenglou/pretext` bump.
503
+
504
+ ### Changed
505
+
506
+ - **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.
507
+
508
+ ### Fixed
509
+
510
+ - **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.
511
+
512
+ ### Added
513
+
514
+ - `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.
515
+ - `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.)
516
+ - `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.
517
+
518
+ ### Removed
519
+
520
+ - `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`.
521
+ - `src/brain/` inert auto-logger artifact (34 blank-body entries, no active writer). Never published to npm.
522
+
523
+ ### Docs
524
+
525
+ - `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.
526
+
527
+ ---
528
+
529
+ ## [0.9.1] 2026-04-21
530
+
531
+ 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.
532
+
533
+ ### Fixed
534
+
535
+ - **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.
536
+ - **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.
537
+ - **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.
538
+
539
+ ### Added / hardened
540
+
541
+ - **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`.
542
+ - **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.
543
+ - **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.
544
+ - **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.
545
+ - **Extracted `LINK_COLOR_DEFAULT`** constant in `src/rich-text.ts`.
546
+
547
+ ### Tests
548
+
549
+ - `test/rich-text.test.ts` 20 → 23 (+3): block-start leading whitespace preserved; leading whitespace after hard break preserved; ZWJ preservation.
550
+ - `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.
551
+ - Full suite: 624 tests, 100% pass.
552
+
553
+ ### Chore / docs
554
+
555
+ - Removed `brain/learnings/*.md`, `docs/PLAN-v0.6-v1.0.md`, `test/paginate.test.ts.archive` internal dev artifacts not for the public repo.
556
+ - Stripped `Phase N:` nomenclature from `src/` comments (pure rename — no logic delta).
557
+ - Added `demo/stackblitz/.stackblitzrc`, `docs/articles/pretext-pdf-vs-pdfmake-2026.md` (draft).
558
+ - 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.
559
+ - README test badge corrected `650+ → 600+` (verified: 624 tests total).
560
+
561
+ ---
562
+
563
+ ## [0.9.0]2026-04-20
564
+
565
+ Three additive enhancements that broaden the package's surface without growing its mandatory dependency footprint.
566
+
567
+ ### Added
568
+
569
+ - **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).
570
+ - **`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.
571
+ - **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.
572
+ - **Markdown: GFM task lists** — `- [x] done` and `- [ ] todo` render with ☑ / ☐ Unicode markers prepended to the item text.
573
+
574
+ ### Tests
575
+
576
+ - 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).
577
+
578
+ ### Notes
579
+
580
+ - 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.
581
+ - `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.
582
+
583
+ ---
584
+
585
+ ## [0.8.3] — 2026-04-20
586
+
587
+ ### Security
588
+
589
+ - **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 `::`.
590
+ - **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.
591
+
592
+ ### Fixed
593
+
594
+ - **`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"`.
595
+ - **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.
596
+ - **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.
597
+
598
+ ### Tests
599
+
600
+ - New `test/v0.8.3-ssrf.test.ts` covers 11 IPv4-mapped IPv6 bypass cases, IPv6 unspecified/loopback regressions, and HTTP rejection.
601
+ - Extended `test/phase-10c-markdown.test.ts` with regressions for 3-level nesting and paragraph-typed list items.
602
+ - Extended `test/phase-10d-templates.test.ts` with the sub-rupee amount-in-words case.
603
+
604
+ ---
605
+
606
+ ## [0.8.2]2026-04-20
607
+
608
+ ### Fixed
609
+
610
+ - **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.
611
+ - The fast path (no trailing whitespace) is unchanged single pretext call. Slow path adds two pretext calls per affected token, with one cached.
612
+
613
+ ### Tests
614
+
615
+ - 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.
616
+
617
+ ---
618
+
619
+ ## [0.8.1] — 2026-04-20
620
+
621
+ ### Fixed
622
+
623
+ - **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.
624
+ - **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`).
625
+
626
+ ### Notes for browser users
627
+
628
+ - 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.
629
+ - 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.
630
+
631
+ ---
632
+
633
+ ## [0.8.0]2026-04-19
634
+
635
+ ### Added
636
+
637
+ - **`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.
638
+ - **`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.
639
+ - **`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.
640
+ - **`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.
641
+ - **`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).
642
+ - **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`.
643
+
644
+ ---
645
+
646
+ ## [0.7.2] — 2026-04-20
647
+
648
+ Phase 11 cross-cutting enhancements. Retroactively attributed to 0.7.2; these features were
649
+ originally left as `[Unreleased]` and published out of chronological order after 0.7.1.
650
+
651
+ ### Added
652
+
653
+ - **`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).
654
+ - **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'`.
655
+ - **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.
656
+ - **`onFormFieldError` callback** — `doc.onFormFieldError: (name, err) => 'skip' | 'throw'` mirrors `onImageLoadError`. Controls render behaviour when a form field fails.
657
+ - **`createFootnoteSet(defs)`** — helper exported from `pretext-pdf` that generates footnote definition/reference pairs with globally unique IDs. Returns `Array<{ id, def }>`.
658
+ - **`renderDate` field** — `doc.renderDate: Date | string` overrides the PDF creation date. Useful for reproducible builds and testing.
659
+ - **`{{date}}` and `{{author}}` tokens** in header/footer text — join existing `{{pageNumber}}` / `{{totalPages}}`. `{{date}}` resolves from `renderDate`; `{{author}}` resolves from `doc.metadata.author`.
660
+ - **`tabularNumbers`** on `rich-paragraph` digits rendered at uniform slot width (widest digit in font), so columns of numbers align without OpenType TNUM feature.
661
+ - **`smallCaps` + `letterSpacing` per span** — `InlineSpan.smallCaps` and `InlineSpan.letterSpacing` now respected in `rich-paragraph` rendering.
662
+ - **Per-span `fontSize`** — `InlineSpan.fontSize` overrides the element-level font size for that span. Enables mixed-size text in a single paragraph.
663
+
664
+ ### Fixed
665
+
666
+ - `resolveTokens()` used `.replace()` (replaces first occurrence only) — changed to `.replaceAll()` for all four tokens.
667
+ - Table span grid: continuation-row cursor was advancing by 1 instead of `colspan` when skipping a spanned column — now advances by full span width.
668
+ - Font family names now validated for safe characters (`/^[a-zA-Z0-9 _-]+$/`) in `requireFamily()` — rejects null bytes and control characters.
669
+ - Annotation `color` and `author` fields now validated in `validateElement()` for both `paragraph` and `heading` annotations.
670
+ - `buildOutlineTree` memoizes `parentIdxOf()` into a pre-computed array — eliminates O(n²) scan for documents with large heading counts.
671
+ - Table grid-line renderer pre-computes active boundary set — eliminates O(rows × cols) inner loop for large tables.
672
+ - `addLinkAnnotation()` re-validates URL scheme at render time (defense-in-depth; `validate.ts` is the primary gate).
673
+
674
+ ---
675
+
676
+ ## [0.7.1] — 2026-04-19
677
+
678
+ ### Changed
679
+
680
+ - **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.
681
+
682
+ ### Fixed
683
+
684
+ - **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.
685
+ - **3 phase-11 list tests corrected** — test data incorrectly contained 3-level nesting while named "2-level"; data trimmed to match documented contract.
686
+
687
+ ---
688
+
689
+ ## [0.7.0] — 2026-04-17
690
+
691
+ ### Added
692
+
693
+ - **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.
694
+ - **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.
695
+ - **`## Performance` section in README** — measured render times and PDF sizes for 1-page, 10-page, and mixed-element documents. Font subsetting behaviour documented.
696
+ - **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.
697
+ - **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.
698
+
699
+ ### Changed
700
+
701
+ - **`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.
702
+ - **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.
703
+ - **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.
704
+
705
+ ### Fixed
706
+
707
+ - **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 } }`.
708
+ - **StackBlitz integration** — added `.stackblitzrc` so WebContainer auto-runs `npm start` and opens the browser preview on port 3000.
709
+
710
+ ---
711
+
712
+ ## [0.5.3]2026-04-16
713
+
714
+ ### Changed
715
+
716
+ - **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).
717
+
718
+ ---
719
+
720
+ ## [0.5.2]2026-04-13
721
+
722
+ ### Added
723
+
724
+ - **`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.
725
+
726
+ ```typescript
727
+ await render({
728
+ content: [...],
729
+ onImageLoadError: (src, error) => {
730
+ myLogger.warn('Image skipped', { src, error })
731
+ return 'skip' // or 'throw' to abort
732
+ }
733
+ })
734
+ ```
735
+
736
+ ---
737
+
738
+ ## [0.4.0] 2026-04-08
739
+
740
+ ### Breaking Changes
741
+
742
+ - **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.
743
+ - **`ENCRYPTION_NOT_AVAILABLE` error code removed** encryption is now always available. Update any `switch` statements that handled this code.
744
+
745
+ ### Why this change
746
+
747
+ `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.
748
+
749
+ ### Added
750
+
751
+ - `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.
752
+ - `docs/ROADMAP.md`public multi-phase development plan
753
+
754
+ ### Changed
755
+
756
+ - `@chenglou/pretext` version pinned to exact `0.0.3` (no caret) — prevents surprise breaking changes from upstream auto-updates
757
+ - `test:contract` script added runs the pretext API contract test before the full test suite
758
+ - All internal comments updated from `pdf-lib` to `@cantoo/pdf-lib`
759
+
760
+ ---
761
+
762
+ ## [0.3.1] 2026-04-08
763
+
764
+ ### Fixed
765
+
766
+ - **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.
767
+
768
+ ---
769
+
770
+ ## [0.3.0] 2026-04-08
771
+
772
+ ### Added (Phase 8BInteractive Forms)
773
+
774
+ - New `form-field` element typecreates interactive AcroForm fields in PDFs
775
+ - `fieldType: 'text' | 'checkbox' | 'radio' | 'dropdown' | 'button'`
776
+ - `label` renders above the field as static text
777
+ - Text fields: `defaultValue`, `multiline`, `placeholder`, `maxLength`
778
+ - Checkboxes: `checked` initial state
779
+ - Radio groups and dropdowns: `options` array, `defaultSelected`
780
+ - `doc.flattenForms: true` — bakes all fields into static content
781
+ - Custom `borderColor`, `backgroundColor`, `width`, `height`, `fontSize` per field
782
+ - New error codes: `FORM_FIELD_NAME_DUPLICATE` (duplicate `name` across fields), `FORM_FLATTEN_FAILED`
783
+ - Post-render `form.updateFieldAppearances()` ensures proper display in all PDF readers
784
+ - 10 comprehensive tests covering all form field types
785
+
786
+ ### Added (Phase 8E Signature Placeholder)
787
+
788
+ - `doc.signature` — visual signature box drawn on a specified page
789
+ - Fields: `signerName`, `reason`, `location`, `x`, `y`, `width`, `height`, `page`, `borderColor`, `fontSize`
790
+ - Draws signature line, date line, and optional text inside a bordered rectangle
791
+ - `page` is 0-indexed, defaults to last page, clamps gracefully if out of range
792
+ - 6 comprehensive tests
793
+
794
+ ### Added (Phase 8D Callout Boxes)
795
+
796
+ - New `callout` element type styled highlight box with optional title
797
+ - Preset styles: `style: 'info'` (blue), `'warning'` (amber), `'tip'` (green), `'note'` (gray)
798
+ - Optional `title` rendered bold above content with left border accent
799
+ - Fully customizable: `backgroundColor`, `borderColor`, `color`, `titleColor`, `padding`
800
+ - Paginates correctly across pages (reuses blockquote pagination logic)
801
+ - 8 comprehensive tests
802
+
803
+ ### Added (Phase 8F Document Metadata Extensions)
804
+
805
+ - `doc.metadata.language` sets PDF `/Lang` catalog entry (BCP47 tag e.g. `'en-US'`, `'hi'`)
806
+ - `doc.metadata.producer` — sets PDF producer field (e.g. `'MyApp v2.1'`)
807
+ - Both fields validate as non-empty strings
808
+ - 5 comprehensive tests
809
+
810
+ ---
811
+
812
+ ## [0.2.0] 2026-04-08
813
+
814
+ ### Added (Phase 8H — Inline Formatting)
815
+
816
+ - `verticalAlign: 'superscript' | 'subscript'` on `InlineSpan` in rich-paragraphs
817
+ - Superscript renders at 65% font size, baseline shifted up by 40% of font size
818
+ - Subscript renders at 65% font size, baseline shifted down by 20% of font size
819
+ - `letterSpacing?: number` on `ParagraphElement`, `HeadingElement`, `RichParagraphElement` — extra pt between characters
820
+ - `smallCaps?: boolean` on those same three element types — simulated via uppercase + 80% fontSize
821
+ - Character-by-character rendering for letterSpacing (pdf-lib has no native spacing param)
822
+ - 8 comprehensive tests covering all inline formatting functionality
823
+
824
+ ### Added (Phase 8AAnnotations/Comments)
825
+
826
+ - New `comment` element type sticky note annotation at position in document
827
+ - `annotation?: AnnotationSpec` on `ParagraphElement` and `HeadingElement` attach note to element
828
+ - Supports: `contents`, `author`, `color` (hex), `open` (popup default state)
829
+ - Uses PDF `Subtype: 'Text'` annotation (sticky note icon in PDF viewers)
830
+ - 8 comprehensive tests covering all annotation functionality
831
+
832
+ ### Added (Phase 8CDocument Assembly)
833
+
834
+ - New `merge(pdfs: Uint8Array[])` exported function combine pre-rendered PDFs
835
+ - New `assemble(parts: AssemblyPart[])` exported function — mix rendered docs + existing PDFs
836
+ - `AssemblyPart` interface: `{ doc?: PdfDocument, pdf?: Uint8Array }`
837
+ - New error codes: `ASSEMBLY_EMPTY`, `ASSEMBLY_FAILED`
838
+ - 8 comprehensive tests covering all assembly functionality
839
+
840
+ ### Fixed
841
+
842
+ - **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`.
843
+
844
+ ---
845
+
846
+ ## [0.1.1] 2026-04-08
847
+
848
+ ### Added
849
+
850
+ - **Phase 8G: Hyperlinks** Complete link annotation support:
851
+ - `paragraph.url` for external URI links on paragraphs
852
+ - `heading.url` for external URI links on headings
853
+ - `heading.anchor` for named PDF destinations (internal cross-references)
854
+ - `InlineSpan.href` for external and internal `#anchorId` links in rich-paragraphs
855
+ - `mailto:` scheme support for email links
856
+ - GoTo annotations for internal anchor references
857
+ - 9 comprehensive tests covering all hyperlink functionality
858
+
859
+ ### Fixed
860
+
861
+ - **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).
862
+ - **Node.js version compatibility**: Replaced `--experimental-strip-types` with `tsx` runner to support Node.js 18.x, 20.x, and 22.x in CI
863
+ - **Broken CI examples**: Removed references to non-existent Phase 8 example scripts from GitHub Actions workflow
864
+ - **README examples mismatch**: Updated Examples section to only list 5 existing Phase 7 examples (watermark, bookmarks, toc, rtl, encryption)
865
+ - **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
866
+
867
+ ### Changed
868
+
869
+ - `test:unit` now runs only `test/paginate-basic.test.ts` (fast, no canvas overhead)
870
+ - Reorganized test scripts: `test:unit`, `test:validate`, `test:e2e`, `test:phases` for better memory management
871
+ - Moved internal planning documentation to archive (preserved, not published)
872
+ - `devDependencies`: Added `@napi-rs/canvas` explicitly (was missing, causing CI failures)
873
+
874
+ ### Added
875
+
876
+ - `CONTRIBUTING.md`: Development setup, TDD workflow, PR process guide
877
+ - `CHENG_LOU_EMAIL_DRAFT.md`: Template for requesting endorsement from pretext creator
878
+ - `examples/comparison-pdfmake.ts`: pdfmake version of invoice for typography comparison
879
+
880
+ ---
881
+
882
+ ## [0.1.0] 2026-04-07
883
+
884
+ ### Added (Phase 7GEncryption)
885
+
886
+ - `doc.encryption` configuration for password-protecting PDFs
887
+ - User password and owner password support
888
+ - Granular permission restrictions: printing, copying, modifying, annotating
889
+ - Lazy-loads `@cantoo/pdf-lib` (optional peer dependency) — zero cost when not used
890
+ - Error code: `ENCRYPTION_NOT_AVAILABLE` when encryption is requested but dependency not installed
891
+
892
+ ### Added (Phase 7F — RTL Text Support)
893
+
894
+ - Right-to-left text support for Arabic, Hebrew, and other RTL languages
895
+ - Unicode bidirectional text algorithm via `bidi-js`
896
+ - `dir` attribute on text elements: `'ltr'` | `'rtl'` | `'auto'` for per-element control
897
+ - RTL text works with headings, paragraphs, lists, tables, and all text elements
898
+ - Automatic detection of mixed LTR/RTL content
899
+
900
+ ### Added (Phase 7E SVG Support)
901
+
902
+ - `{ type: 'svg', svg: '<...' }` element for embedding SVG graphics
903
+ - SVG rasterization via `@napi-rs/canvas`
904
+ - ViewBox auto-sizing: automatic height calculation from viewBox aspect ratio
905
+ - Explicit sizing: `width` and `height` parameters for precise control
906
+ - Alignment options: `align: 'left' | 'center' | 'right'`
907
+ - Multi-page support: SVGs paginate correctly across page breaks
908
+ - Error code: `SVG_RENDER_FAILED` for SVG rasterization errors
909
+
910
+ ### Added (Phase 7D Table of Contents)
911
+
912
+ - `{ type: 'toc' }` element for automatic TOC generation
913
+ - Two-pass rendering pipeline ensures accurate page numbers
914
+ - Configurable: `title`, `showTitle`, `minLevel`/`maxLevel`, dot leaders, level indentation
915
+ - Auto-indexed from heading structure (H1, H2, H3, etc.)
916
+ - Supports custom formatting via `fontSize`, `color`, `spaceAfter` parameters
917
+
918
+ ### Added (Phase 7C — Hyphenation)
919
+
920
+ - Automatic word hyphenation for better justified text layout
921
+ - `doc.hyphenation: { language: 'en-US' }` for document-level config
922
+ - Liang's algorithm via `hypher` package for accurate break points
923
+ - Configurable: `minWordLength`, `leftMin`, `rightMin`, per-element `hyphenate: false` opt-out
924
+ - Language support: includes `hyphenation.en-us` (additional languages via npm packages)
925
+ - Error code: `UNSUPPORTED_LANGUAGE` when language not available
926
+
927
+ ### Added (Phase 7B — Watermarks)
928
+
929
+ - `doc.watermark` for text or image watermarks on every page
930
+ - Text watermarks: `text`, `fontSize`, `fontWeight`, `color`, `opacity`, `rotation`
931
+ - Image watermarks: `image` (Uint8Array), `opacity`, `rotation`, `color` (tint)
932
+ - Watermarks render behind content (lower z-index)
933
+ - Rotation bounds: -360 ≤ rotation ≤ 360 degrees
934
+ - Validation: must provide either text or image, never both required
935
+
936
+ ### Added (Phase 7A — Bookmarks / PDF Outline)
937
+
938
+ - PDF sidebar bookmarks auto-generated from heading structure
939
+ - Enabled by default: `bookmarks: true` or `bookmarks: { minLevel: 1, maxLevel: 3 }`
940
+ - Level filtering: include/exclude heading levels from outline
941
+ - Per-heading opt-out: `bookmark: false` on heading elements
942
+ - Keyboard navigation: Cmd/Ctrl+Opt/Alt+O in PDF readers to toggle bookmark sidebar
943
+
944
+ ### Added (Phase 6 — Advanced Features)
945
+
946
+ - Header and footer support with {{pageNumber}} and {{totalPages}} tokens
947
+ - Text decoration: strikethrough, underline
948
+ - Text alignment: left, center, right, justify
949
+ - Line height control: custom line-height multipliers
950
+ - Column layout with multi-column content flow
951
+ - Tables with colspan/rowspan support
952
+
953
+ ### Added (Phase 5 — Rich Text / Builder API)
954
+
955
+ - Fluent builder API for programmatic document construction
956
+ - Rich text element with nested formatting (bold, italic, links)
957
+ - Inline code and code blocks with syntax highlighting
958
+ - Block quotes with custom styling
959
+ - Horizontal rules (hr element)
960
+ - Numbered and bulleted lists with nesting
961
+
962
+ ### Added (Phases 1–4 — Core Engine)
963
+
964
+ - Core PDF generation via `pdf-lib`
965
+ - Element types: paragraph, heading, table, image, list, code, blockquote
966
+ - Font support: Inter bundled, custom TTF embedding
967
+ - Document metadata: title, author, subject, keywords, created date
968
+ - Page sizing: A4, A3, A5, Letter, Legal, or custom dimensions
969
+ - Margins: top, bottom, left, right per page
970
+ - Multi-page pagination with orphan/widow control
971
+ - Image formats: PNG, JPG, WebP
972
+ - Table features: custom column widths, cell padding, borders, header styling
973
+ - Colors: hex color codes throughout (text, backgrounds, borders)