From 9c28482bcc485928d9cc1682acb3f8b6d4ed348a Mon Sep 17 00:00:00 2001 From: Yousef Date: Mon, 31 Aug 2026 12:14:06 +0200 Subject: [PATCH 01/26] feat: Typst-powered accessible PDF export (PDF/UA-1) (#2992) * poc * improve typst exporter * update tests * feat(typst): math & diagram mappings, SVG diagrams, @cantoo/pdf-lib - @blocknote/math-block/typst-exporter: math renders as native Typst equations (LaTeX converted with tex2typst, KaTeX-validated) with the LaTeX source as alt text - required by PDF/UA and enforced by compiling the tests under the node compiler's pdfStandard "ua-1" validation. - @blocknote/diagram-block/typst-exporter: diagrams embed as vector SVG figures with the Mermaid source as alt text (same ua-1 test gate). - Diagrams now render labels as SVG text everywhere (htmlLabels: false in the one global initialize): Mermaid's default HTML labels live in , which non-browser SVG consumers silently drop. Verified to keep wrapping,
and markdown-string formatting. - Diagram labels use the document font on every surface: the editor preview reads the editor's computed style, PNG exports default to BlockNote's UI font, the Typst export uses the exporter's fontFamily - applied by rewriting the rendered SVG's font declarations (Mermaid has no per-render config API). - TypstExporter: registerImageBytes for pre-rendered assets, fontFamily accepts a fallback list for CJK (mirroring the react-pdf exporter's fonts/fontFamily options), strLit/escStr exported for mapping authors. - pdf-lib replaced with the maintained @cantoo/pdf-lib fork (2.9.1); DisplayDocTitle now set via its ViewerPreferences API. Fixed the package's vite externals so the fork isn't inlined into the bundle. - Example 11 registers math & diagram blocks with the typst mappings; renderer-2 README/status refreshed; tsgo -> tsc for TS7. Co-Authored-By: Claude Fable 5 * fix(typst): review-driven hardening of the typst/pdf-ua pipeline Correctness: - Tables: colspan/rowspan support (merged cells kept their columns), all header rows in a single table.header (Typst rejects more than one), and a colspan-aware column count. - Asset paths are extension-less: Typst detects the image format from the bytes (verified against the engine); a wrongly guessed known extension was the only way to make good bytes fail. Deletes the mime/sniffing/URL machinery and the image-meta dependency. - Valid-KaTeX commands tex2typst can't translate fail the export loudly; documented in the README with a measured comparison (tex2typst 28/32 vs mitex-wasm 14/32 on a KaTeX corpus; tylax has no npm artifact). - declarePdfUA: decodes filtered XMP streams, handles self-closing rdf:Description, $-safe splicing, and throws if the identifier can't be injected; rewritten with the UA declarations documented in place. - compileBrowser: honest singleton contract (option changes throw, config documented as fixed at first compile), serialized compiles, and real browser tests of the wasm path. - Image resolve failures fail exports loudly (repo error doctrine); the CORS proxy passes data:/blob: URLs through untouched. - Email exporter: column content renders flat (no double indent, no nesting-level bump); escStr treats lone CR as a line break; numbered lists keep start: 0; font rewrites are $-safe. - Diagram labels use the exporter's full font list (CJK + emoji fallbacks). Options & API: - TypstDocumentOptions gains paper/margin; blocksToPdfUA documents the tagged-but-unclaimed composition; escStr/imageExtension/PT dropped from the public API (PIXELS_PER_POINT is per-mapping, like the pdf exporter). Test document & fixtures: - Shared doc gains an advanced table (two header rows, colspan, rowspan), a hard line break, and an empty paragraph - surfacing that odt/react-pdf/ email exporters silently drop cell merges (follow-up). - The shared test resolver serves a real decodable JPEG; typst tests are network-free; NodeCompiler test boilerplate lives in one shared helper; binary snapshots share the common util with an .actual dump on mismatch. - Generated example copies moved into src/ (the docs generator only copies src/**) and the docs site builds the pdf-ua demo (Turbopack resolveAlias + a version-derived CDN wasm stub; portable new URL font assets). Infra: - vp task input specs fixed for shared/ (root-level sources) and gen (shared test doc + generated copies) - stale cache replays clobbered fresh builds. - pdfua test gates throw when verapdf/pdftoppm are missing instead of silently passing; CI installs poppler and the official veraPDF container image, pinned by digest, via tests/scripts/install-pdf-tooling.sh. - Playground gains the typst deps + Vercel build aliases; minimum release age exclusions narrowed to the exact typst packages. - Example 11: usePdfUA hook (effect-with-cleanup idiom), font fetch retry, single object-URL lifecycle. * fix(typst): review pass-3 fixes, fully offline fonts, pinned poppler rasterizer - exporter: append-only asset registry (create a fresh exporter per export; removes the clear/fetch-cache machinery and its races), blank-line sentinel for empty paragraphs, alignment scoped to a block's own content (nested children keep theirs), core getColspan/getRowspan in the table mapping, asset-key collision throw in blocksToPdfUA - compile: actually disable CDN font assets (typst.ts force-loads its 'text' fonts from jsdelivr without the explicit opt-out marker); font compatibility check by content fingerprint (length + first 256 bytes) instead of reference identity; PDF/UA identifier spliced at the first rdf:Description open tag - fonts: bundle NewCMMath (Typst's math font) in the example and shared assets; per-file `new URL` literals (Turbopack collapses template paths to a single asset); genDocs copies binary example files byte-for-byte (UTF-8 round-trip corrupted every byte >= 0x80) - docs: serve the compiler wasm as a local static asset instead of jsdelivr; drop the dead webpack config (Next 16 builds with Turbopack) - docx/odt: hard line breaks (shift+enter) emit resp. instead of raw LF that viewers ignore/collapse - tests: rasterize pdf/ua visual snapshots through a digest-pinned poppler container (byte-stable across environments; regenerate baselines once docker is available), verapdf shim zero-arg and .pdf-suffix fixes, shared isPdf/PNG test utils, NodeCompiler reuse in typstTestUtil - example: debounce before reading editor.document, diagram preview reads the editor font at render time * refactor(typst): split into xl-typst-exporter + Typst-based PDFExporter in xl-pdf-exporter - new @blocknote/xl-typst-exporter: the pure Typst layer (TypstExporter, default mappings, custom-mapping helpers) with no compile/PDF deps - the same mappings serve .typ export and PDF export - @blocknote/xl-pdf-exporter root is now the Typst-based PDFExporter (toBytes/toBlob; asset merge + collision check + compile + declare folded into toBytes, blocksToPdfUA no longer exported). declarePdfUA is a default-on opt-out (a tagged-but-unclaimed PDF is honest when a document is known not to conform); compileTypstToTaggedPdf/declarePdfUA stay exported for custom/server-side composition - the react-pdf exporter moved to the /react-pdf subpath (only exposure, so its dependency tree tree-shakes out of the root graph), with @deprecated tags on it, its mappings, and math/diagram's react-pdf mappings; the react-pdf example is marked deprecated - xl-pdf-renderer-2 removed (never published); all imports, deps, aliases, docs deps, CI tooling script updated; pdf-ua example and tests use the new PDFExporter API - build task outputs now declare types/** (a cache replay restoring only dist/ left consumers without declarations); xl-pdf-exporter tsconfig gets node types for the moved pdfua suites * docs(typst): export docs + review-driven packaging and API hardening - docs: rewrite the PDF export page for the Typst-based PDFExporter (simple-first CDN path, fonts & offline use, PDF/UA guidance, custom Typst mappings, deprecated react-pdf section), add the Typst export page, update supported formats and the math/diagram block pages - inline mappings now return Typst *markup* (leading #), so inline results compose by plain concatenation: block mappings use exporter.transformInlineContent(content).join("") - the same base Exporter seam as the other exporters - and the joinInline helper is gone (output byte-identical, verified by unchanged snapshots) - packaging (from design review): LICENSE for xl-typst-exporter; react/react-dom/@react-pdf/renderer become optional peers (react-pdf serves only the deprecated subpath); dead ./style.css export removed; xl-pdf-exporter manifest describes the actual product; stale types/ output cleaned; missing xl-multi-column test alias + stale externals fixed in xl-typst-exporter's vite config - API polish: toBytes/toBlob options optional, TypstExporterOptions exported, stale toBlob references removed from react-pdf docstrings, react-pdf example marked deprecated * fix(docs): demo page still imported the react-pdf exporter from the package root The /demo page (outside the generated example trees) kept the old root import; local builds masked it by resolving a stale workspace dist. Point it at the deprecated /react-pdf subpath - migrating the demo's PDF action to the Typst-based PDFExporter rides the examples/docs follow-up pass. * fix(ci): e2e failure artifacts were never uploaded .vitest-attachments is a dot-directory and upload-artifact excludes hidden files by default, so the failure-artifact step always warned 'No files were found' and uploaded nothing - exactly when the actual screenshots are needed to inspect (or adopt) a changed baseline. * fix: address CodeRabbit review findings - compileBrowser: compare getModule's resolved module by canonical key (a fresh `new URL(...)` per call - the docstring's own recommended pattern - spuriously failed the identity check on every compile after the first) - declarePdfUA: only accept an existing pdfuaid:part=1 claim; any other existing identifier (part 2, empty) now throws instead of being silently endorsed (+ regression tests) - typst tables: honor headerCols like the other exporters (header-column cells get the header treatment; Typst has no per-cell TH tag) - TypstExporter: default options per key so an explicitly-undefined `colors` can't crash color lookups (+ test) - pdfua visual snapshot rasterizes the *declared* PDF users receive, not the pre-declaration compile output - binaryFileSnapshotUtil: create the baseline dir before the .actual diagnostic write so it can't ENOENT-mask the mismatch error - @deprecated on mathBlockMapping (react-pdf) for parity; example download control is a real + +
+ {pdfUrl ? ( +