diff --git a/.claude/skills/docs-skill/SKILL.md b/.claude/skills/docs-skill/SKILL.md index ad44bea9c5..e0791f408f 100644 --- a/.claude/skills/docs-skill/SKILL.md +++ b/.claude/skills/docs-skill/SKILL.md @@ -18,8 +18,16 @@ The documentation site lives in `docs/content/docs` (fumadocs + Next.js). These - **Live examples cap the setup they demonstrate.** Place an `` embed after the prose has introduced everything the example's code uses — an embed whose `App.tsx` shows APIs the page never mentions teaches by confusion. Introduced means named, given a purpose, and linked — the example itself (and the linked component pages) can carry the full wiring; expanding every integration inline pushes the example too far down the page. Not at the top (a demo without context motivates but doesn't teach) and not at the bottom (readers rarely reach it); at the end of the "getting it working" narrative, before advanced/optional topics. If an example uses more API than the page should cover, simplify the example rather than the rule. - **Signature snippets tell the truth for one API.** A snippet formatted as a type signature documents exactly that export — never fold variant differences ("only for the X subpath…") into a doc-comment inside it. Show the signature that is true everywhere, and describe variant-specific options as prose in the variant's own section. +- **Docs follow the dependency direction.** When a package builds on another, the base layer's page owns the shared mechanisms (mappings, formats, behaviors) and never points "up" to a consumer page for its own concepts; consumer pages link down. Consumer pages still document their own API surfaces (option lists, signatures) explicitly rather than delegating them, per the signature rule below. Name sections for every layer they serve ("Typst / PDF", not just "PDF") so the base layer isn't erased. +- **Confine a cross-cutting concept to one designated section per page.** A page's headline concept (a conformance standard, an offline guarantee) gets one owning section; the intro may link to it once, and other sections mention it only where the reader must act on it there. Sprinkled re-mentions read as emphasis when written but age as duplication. +- **The hero snippet is the happy path only.** The first usage snippet shows the shortest end-to-end flow and nothing else; auxiliary mechanisms (asset maps, secondary outputs, tuning knobs) move to their own short sections even when genuinely relevant. Same for prose caveats aimed at a niche audience (a live-preview memory note): put them where that audience looks, not in the getting-started flow. - **Caveats must be actionable.** Only note a limitation if the reader can do something with it (install a package, avoid a pattern, pass an option). Speculative hedges ("rare X may behave differently") and defensive implementation details (what a function guards against internally) erode trust without helping anyone act — cut them. +## Prose style + +- Avoid em-dash-heavy prose; prefer commas, colons, semicolons, periods, or parentheses, choosing per sentence rather than substituting mechanically. An em-dash is fine occasionally; several per section reads as filler. +- Before listing something as a _requirement_, verify the reader can actually fail it. A "requirement" the implementation always satisfies automatically (e.g. auto-derived alt text) is at most a quality tip, phrased as one. + ## Verifying content - **Verify snippets against the actual package exports, not memory or existing docs.** APIs drift; grep the package source for every symbol a snippet imports (`export function X` / `export const X`) and check option names and shapes. Content copied forward without this check stays wrong after refactors. diff --git a/.dockerignore b/.dockerignore index 87697e5b29..87239fb0a2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,5 +13,9 @@ .git **/test-results **/blob-report +# The Typst compiler's Rust build tree (GBs) and wasm output: never image +# content - docker-run.sh bind-mounts pkg/ at run time. +**/rust/target +packages/xl-typst-compiler/pkg **/playwright-report tests/.vitest-attachments diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6a941508b..cc5b7941bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,15 +31,16 @@ jobs: - name: Install Dependencies run: vp install + # `vp run -r build` includes @blocknote/xl-typst-compiler, whose build + # task compiles its Rust wasm when missing or stale (rustup is + # preinstalled on the runners; the pinned toolchain + wasm32 target + # auto-provision from rust/rust-toolchain.toml). - name: Build packages run: vp run -r build - name: Lint packages run: vp lint - - name: Install PDF tooling (veraPDF conformance gate + poppler visual snapshots) - run: bash tests/scripts/install-pdf-tooling.sh - - name: Run unit tests run: vp run test @@ -55,7 +56,58 @@ jobs: - name: Soft release id: soft-release - run: vp dlx pkg-pr-new publish './packages/*' # TODO disabled only for AI branch--compact + # xl-typst-compiler is excluded: its wasm makes the package ~25MB, + # which pkg.pr.new rejects (413; multipart uploads are whitelisted - + # https://github.com/stackblitz-labs/pkg.pr.new/blob/main/.whitelist). + # Preview installs of the PDF exporter therefore lack the compiler + # until the repo is whitelisted or the wasm ships separately. + # TODO disabled only for AI branch--compact + run: | + packages=() + for dir in ./packages/*/; do + if [[ "$dir" != *"/xl-typst-compiler/"* ]]; then + packages+=("${dir%/}") + fi + done + vp dlx pkg-pr-new publish "${packages[@]}" + + build-typst-compiler: + # The one package the e2e suite consumes through build outputs instead of + # src/ aliases: @blocknote/xl-typst-compiler (its /pkg and /wasm subpaths + # are wasm build artifacts - see tests/vite.config.browser.ts). Built once + # on a bare runner - the Playwright container has no C toolchain for the + # Rust build - and shared with the shards as an artifact. + name: Build Typst compiler + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 100 + persist-credentials: false + + - uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 + with: + node-version-file: ".node-version" + cache: true + + - name: Install dependencies + run: vp install + + # Compiles the Rust wasm when missing/stale (rustup is bootstrapped by + # the build task itself) plus the TS wrapper's dist/types. + - name: Build Typst compiler package + run: vp run --filter @blocknote/xl-typst-compiler build + + - name: Upload compiler build outputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: typst-compiler-build + path: | + packages/xl-typst-compiler/pkg + packages/xl-typst-compiler/dist + packages/xl-typst-compiler/types + include-hidden-files: true e2e: # Vitest Browser Mode runs in the Playwright Linux container — the same @@ -63,10 +115,13 @@ jobs: # The suite resolves every `@blocknote/*` import to its `src/` via the # aliases in `tests/vite.config.browser.ts` (vite transpiles the package # sources on the fly), so the packages do NOT need to be built to `dist` - # first — `vp install` + the checked-out sources are enough. + # first — `vp install` + the checked-out sources are enough (the one + # exception, xl-typst-compiler, arrives prebuilt from the + # build-typst-compiler job). name: "E2E - ${{ matrix.browser }} (${{ matrix.shardIndex }}/${{ matrix.shardTotal }})" runs-on: ubuntu-latest timeout-minutes: 30 + needs: build-typst-compiler container: image: mcr.microsoft.com/playwright:v1.60.0-noble strategy: @@ -89,6 +144,12 @@ jobs: - name: Install dependencies run: vp install + - name: Download Typst compiler build outputs + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: typst-compiler-build + path: packages/xl-typst-compiler + # No preview server: Vitest Browser Mode serves the tests + mounted example # apps itself. `--browser` selects this matrix job's browser and # `--shard=/` splits that browser's test files across two diff --git a/.github/workflows/fresh-install-tests.yml b/.github/workflows/fresh-install-tests.yml index b713b4a785..edf45382d0 100644 --- a/.github/workflows/fresh-install-tests.yml +++ b/.github/workflows/fresh-install-tests.yml @@ -82,9 +82,6 @@ jobs: name: Build packages run: vp run -r build - - name: Install PDF tooling (veraPDF conformance gate + poppler visual snapshots) - run: bash tests/scripts/install-pdf-tooling.sh - - id: run_unit_tests name: Run unit tests run: vp run test diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 01bb86785f..59e58c459d 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -42,6 +42,10 @@ jobs: - name: Prebuild run: cp README.md packages/core/README.md && cp README.md packages/react/README.md + # `vp run -r build` includes @blocknote/xl-typst-compiler, whose build + # task compiles its Rust wasm (published inside the package as pkg/); + # rustup auto-provisions the pinned toolchain from + # rust/rust-toolchain.toml. - name: Build packages run: vp run -r build diff --git a/docs/app/demo/_components/DemoEditor.tsx b/docs/app/demo/_components/DemoEditor.tsx index 41762e9da4..c4e690582c 100644 --- a/docs/app/demo/_components/DemoEditor.tsx +++ b/docs/app/demo/_components/DemoEditor.tsx @@ -50,7 +50,7 @@ import { } from "@blocknote/xl-pdf-exporter"; // Bundle the Typst compiler wasm (resolved to a local asset in // next.config.ts) instead of loading it from a CDN. -import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url"; +import compilerWasmUrl from "@blocknote/xl-typst-compiler/wasm?url"; import { DefaultChatTransport } from "ai"; import { useTheme } from "next-themes"; import { useEffect, useMemo, useState } from "react"; @@ -352,12 +352,20 @@ function DemoEditorInner({ const exporter = new PDFExporter( editor.schema, typstDefaultSchemaMappings, + { wasm: compilerWasmUrl }, ); - blob = await exporter.toBlob( - editor.document, - { getModule: () => compilerWasmUrl }, - { title: "BlockNote demo document" }, - ); + const result = await exporter.toPDF(editor.document, { + title: "BlockNote demo document", + lang: "en", + }); + if (result.error) { + throw new Error( + `PDF export failed: ${result.compileErrors + .map((d) => d.message) + .join("; ")}`, + ); + } + blob = result.blob; } else if (format === "docx") { const exporter = new DOCXExporter( editor.schema, diff --git a/docs/components/typstCompilerWasmUrl.ts b/docs/components/typstCompilerWasmUrl.ts index dc3ec24e6e..8d8a113291 100644 --- a/docs/components/typstCompilerWasmUrl.ts +++ b/docs/components/typstCompilerWasmUrl.ts @@ -1,11 +1,11 @@ // The Typst compiler wasm for the docs build (see next.config.ts): the // pdf-ua example bundles it via Vite's `?url`; under the docs site the // import is aliased here. `new URL(..., import.meta.url)` is Turbopack's -// static-asset reference: the ~29MB wasm is emitted as a hashed static +// static-asset reference: the ~25MB wasm is emitted as a hashed static // file and served by the site itself - no CDN involved, and the version // can't skew from the installed package because it IS the installed file. const url = new URL( - "../node_modules/@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm", + "../node_modules/@blocknote/xl-typst-compiler/pkg/blocknote_typst_wasm_bg.wasm", import.meta.url, ).href; export default url; diff --git a/docs/components/typstRendererStub.ts b/docs/components/typstRendererStub.ts deleted file mode 100644 index a0a636f2d6..0000000000 --- a/docs/components/typstRendererStub.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Stub for @myriaddreamin/typst-ts-renderer (see next.config.ts): the -// pdf-ua demo only compiles Typst, never canvas-renders, so the optional -// renderer peer is not installed. -export {}; diff --git a/docs/content/docs/features/blocks/math.mdx b/docs/content/docs/features/blocks/math.mdx index 3bf753c1da..83378c6aa6 100644 --- a/docs/content/docs/features/blocks/math.mdx +++ b/docs/content/docs/features/blocks/math.mdx @@ -119,12 +119,13 @@ import { // Spread into the ODTExporter's mappings exactly as for DOCX above. ``` -### PDF +### Typst / PDF -With the [PDF exporter](/docs/features/export/pdf) (or the standalone -[Typst exporter](/docs/features/export/typst) — they share mappings), math -blocks and inline math export as **native Typst equations**: real, selectable -text, no rasterization, so they also work server-side out of the box: +With the [Typst exporter](/docs/features/export/typst) and the +[PDF exporter](/docs/features/export/pdf) built on it (they share mappings), +math blocks and inline math export as **native Typst equations**: real, +selectable text, no rasterization, so they also work server-side out of the +box: ```typescript import { diff --git a/docs/content/docs/features/export/pdf.mdx b/docs/content/docs/features/export/pdf.mdx index 3d0dec3328..1d096961da 100644 --- a/docs/content/docs/features/export/pdf.mdx +++ b/docs/content/docs/features/export/pdf.mdx @@ -9,9 +9,8 @@ path: /docs/export-to-pdf It's possible to export BlockNote documents to PDF, completely client-side. The exporter is powered by the [Typst](https://typst.app) typesetting engine -(compiled to WebAssembly) and produces **accessible, tagged PDF/UA-1** -documents: the PDF carries a logical structure tree (headings, paragraphs, -lists, tables, figures with alt text, links) that screen readers can navigate. +(compiled to WebAssembly) and produces accessible, tagged documents; see +[PDF/UA conformance](#pdfua-conformance). This feature is provided by the `@blocknote/xl-pdf-exporter`. `xl-` packages @@ -37,24 +36,20 @@ import { // Create the exporter const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings); -// Export the document to a PDF Blob (there's also `toBytes` for a Uint8Array) -const blob = await exporter.toBlob( - editor.document, - {}, - { title: "My document", lang: "en" }, -); +// Export the document; the result carries the PDF as bytes and as a Blob +const result = await exporter.toPDF(editor.document, { + title: "My document", + lang: "en", +}); +if (!result.error) { + const url = URL.createObjectURL(result.blob); + // e.g. open or download `url` +} ``` -This works out of the box: the export matches the editor's look (Inter body -text, Geist Mono code) and handles math blocks and emoji — the default fonts -ship inside the package and load lazily on the first export, with no CDN -involved. The Typst compiler itself (a ~30MB wasm file) is the one thing -loaded from a CDN by default; for production you'll want to bundle it: see -[Fonts & offline use](#fonts--offline-use). - -When repeatedly exporting changing content (e.g. a live preview), create a -fresh exporter per export — construction is cheap, and an exporter instance -accumulates the image assets it has resolved for as long as it lives. +This works out of the box, fully offline: exports match the editor's look, +and everything needed (the default fonts and the compiler itself) ships +inside the package. Nothing is fetched from a CDN. See the [full example](/examples/interoperability/converting-blocks-to-pdf-ua) with a live PDF preview below: @@ -63,13 +58,11 @@ with a live PDF preview below: ### Customizing the PDF -The second parameter of `toBlob` / `toBytes` takes the compile options -(covered in [Fonts & offline use](#fonts--offline-use)); the third takes -per-document options: +`toPDF` takes per-export options: the document metadata and page setup. ```typescript -const blob = await exporter.toBlob(editor.document, {}, { - // Document title - required for PDF/UA (also shown in the viewer's title bar) +const result = await exporter.toPDF(editor.document, { + // Document title, shown in the viewer's title bar title: "My document", // Document author, written to the PDF metadata author: "John Doe", @@ -86,141 +79,128 @@ const blob = await exporter.toBlob(editor.document, {}, { }); ``` -### Custom mappings / custom schemas +The remaining export options are +[`tryDeclarePdfUA`](#pdfua-conformance), `assets` (extra files for +caller-supplied header/footer markup), and `creationTimestamp` (a fixed +Unix timestamp for byte-reproducible output). -The `PDFExporter` constructor takes a `schema` and `mappings` parameter. A -_mapping_ defines how to convert a BlockNote schema element (a Block, Inline -Content, or Style) — for this exporter, into a **Typst markup string**. The -same mappings drive the standalone [Typst export](/docs/features/export/typst), -so one custom-block mapping serves both formats. +A document that fails to compile (e.g. text in a script the loaded fonts +don't cover) is reported in the result as `{ error: "compile-failed" }` +with the compiler's diagnostics, rather than thrown. -If you're using a [custom schema](/docs/features/custom-schemas) in your -editor, or if you want to overwrite how default BlockNote elements are -converted, you can pass your own `mappings`: +### PDF/UA conformance -```typescript -import { - PDFExporter, - typstDefaultSchemaMappings, - strLit, -} from "@blocknote/xl-pdf-exporter"; +The produced PDF is always _tagged_: it carries a logical structure tree +(headings, paragraphs, lists, tables, figures with alt text, links) that +screen readers can navigate. On top of that, the exporter **declares +PDF/UA-1 conformance when, and only when, the document earns it**: Typst +validates conformance during the compile, and a nonconforming document is +exported as tagged-but-unclaimed instead, with the violations reported in +the result: -new PDFExporter(schema, { - ...typstDefaultSchemaMappings, - blockMapping: { - ...typstDefaultSchemaMappings.blockMapping, - myCustomBlock: (block, exporter) => { - // Return Typst markup; `strLit` safely embeds user text as a - // Typst string literal. - return `#${strLit("My custom block")}`; - }, - }, +```typescript +const result = await exporter.toPDF(editor.document, { + title: "My document", + lang: "en", }); +if ( + !result.error && + !result.pdfUA.declared && + result.pdfUA.reason === "nonconforming" +) { + // e.g. "PDF/UA-1 error: the first heading must be of level 1" + console.info(result.pdfUA.violations.map((v) => v.message)); +} ``` -For a block with inline content, render it the way the default mappings do: -`exporter.transformInlineContent(block.content).join("")` (inline results are -markup strings, so plain concatenation composes them). +What conformance requires of the document: -### Math & diagram blocks +- **Title and language**: pass `title` and `lang` in the export options. + `lang` is required to attempt the claim at all: a wrong language + declaration is an accessibility defect no validator can catch, so + exporting without it throws. +- **Headings**: the first heading must be level 1, and levels must be + consecutive (no jumping from H1 to H3). -The [math](/docs/features/blocks/math) and -[diagram](/docs/features/blocks/diagrams) blocks ship Typst mappings — math -exports as native Typst equations (real text, not images), diagrams as -embedded vector SVG — both carrying alt text, as PDF/UA requires: +Images always get the alt text PDF/UA requires, derived from the caption +with the file name as fallback. Captions make far better alt text, so +encourage them. -```typescript -import { diagramBlockMapping } from "@blocknote/diagram-block/typst-exporter"; -import { - inlineMathMapping, - mathBlockMapping, -} from "@blocknote/math-block/typst-exporter"; - -new PDFExporter(editor.schema, { - ...typstDefaultSchemaMappings, - blockMapping: { - ...typstDefaultSchemaMappings.blockMapping, - mathBlock: mathBlockMapping, - diagram: diagramBlockMapping, - }, - inlineContentMapping: { - ...typstDefaultSchemaMappings.inlineContentMapping, - math: inlineMathMapping, - }, -}); -``` +Pass `tryDeclarePdfUA: false` in the export options to skip the validation +and claim entirely, e.g. for a live preview, where the validation compile +would be wasted work. For live previews, also create a fresh exporter per +export, since an instance accumulates the image assets it resolves. -### Fonts & offline use +### Custom mappings / custom schemas -By default, exports use a font set matching the editor — Inter (body), Geist -Mono (code), New Computer Modern Math (math blocks) and Noto Color Emoji -(emoji, [required for PDF/UA](#pdfua-conformance)) — embedded in the package -and loaded lazily on the first export. Fonts never touch a CDN; the compiler -wasm is the only CDN default, and bundling it makes the export fully -offline. The compile options (the second parameter of `toBlob` / `toBytes`) -control all of it: +The `PDFExporter` constructor takes a `schema` and `mappings` parameter. A +_mapping_ converts a BlockNote schema element into a **Typst markup +string**, and the mappings are shared with the standalone Typst export. See +[custom mappings](/docs/features/export/typst#custom-mappings--custom-schemas) +there; everything on writing them applies to this exporter unchanged. -```typescript -import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url"; +### Math & diagram blocks -const blob = await exporter.toBlob(editor.document, { - // The compiler wasm, bundled by your bundler (Vite shown here) instead - // of loaded from a CDN. Install @myriaddreamin/typst-ts-web-compiler to - // import it. - getModule: () => compilerWasmUrl, -}); -``` +The [math](/docs/features/blocks/math) and +[diagram](/docs/features/blocks/diagrams) blocks ship their own Typst +mappings: math exports as native Typst equations (real text, not images), +diagrams as embedded vector graphics. See +[exporting math](/docs/features/blocks/math#typst--pdf) and +[exporting diagrams](/docs/features/blocks/diagrams#exporting) for the setup. -To take full control of fonts (e.g. a different look, or trimming the lazily -loaded defaults — the emoji font alone is ~5MB), pass your own font bytes. -Each option independently replaces its bundled default: supplying `fonts` -keeps the default emoji font (and vice versa), and an explicit empty array -disables one entirely: +### Fonts + +By default, exports use a bundled font set matching the editor: Inter +(body), Geist Mono (code), New Computer Modern Math (math), and Noto Color +Emoji. To use your own fonts instead, pass the font bytes and the matching +family names to the constructor: ```typescript -const blob = await exporter.toBlob(editor.document, { - // Font bytes (Uint8Array) to load into the compiler. - fonts: [myBodyFont, myMonoFont], - // An emoji-capable font. Browsers give the compiler no access to OS - // fonts, so without one emoji render as missing glyphs (and fail PDF/UA). - emojiFont: myEmojiFont, - // Optionally also preload Typst's stock fonts (from its CDN) as - // fallback faces for glyphs your fonts don't cover. - preloadDefaultFonts: true, +const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, { + fontFamily: "My Font", + monoFontFamily: "My Mono Font", + // Uint8Array[], or a promise of them + fonts: [myFontBytes, myMonoFontBytes], + // An emoji-capable font; replaces the default independently of `fonts` + emojiFont: myEmojiFontBytes, }); ``` -The wasm and fonts are loaded once, on the page's first export, and reused -afterwards — pass every font the page will need on that first call. The -[example](/examples/interoperability/converting-blocks-to-pdf-ua) bundles -everything explicitly and works fully offline. +Typst selects fonts by the family name embedded in the font file itself, so +the `fontFamily` options must match what your files declare. A mismatch +shows up as an `unknown font family` entry in the result's +`compileWarnings`. -When passing custom fonts, set the exporter's font families to match -(defaults: `"Inter 18pt"` body, `"Geist Mono"` code, `"Noto Color Emoji"` -emoji). This is also how you cover scripts the primary font doesn't, e.g. -CJK — load the extra font's bytes and declare a fallback list: +To extend the defaults rather than replace them (e.g. adding a CJK font as +a per-glyph fallback), spread the exported default loaders: ```typescript +import { + DEFAULT_FONT_FAMILY, + loadDefaultBodyFonts, + PDFExporter, + typstDefaultSchemaMappings, +} from "@blocknote/xl-pdf-exporter"; + const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, { - fontFamily: ["Inter 18pt", "Noto Sans SC"], + fontFamily: [DEFAULT_FONT_FAMILY, "Noto Sans SC"], + fonts: loadDefaultBodyFonts().then((fonts) => [...fonts, notoSansSCBytes]), }); ``` -### PDF/UA conformance +### Self-hosting the compiler -The produced PDF is tagged and **declares** PDF/UA-1 conformance. Two things -to know: +The Typst compiler is a ~25MB wasm file from `@blocknote/xl-typst-compiler`, +by default emitted as an asset by your bundler. To control where it is +served from (e.g. with caching headers), pass its URL or bytes: -- **Alt text**: every image needs it. BlockNote's image block has no dedicated - alt field yet, so the caption (or file name) is used — give images captions. -- **Headings**: PDF/UA requires the document's first heading to be level 1 — - start documents with an H1. +```typescript +import compilerWasmUrl from "@blocknote/xl-typst-compiler/wasm?url"; -The declaration doesn't itself guarantee conformance of arbitrary input, so -validate exports with [veraPDF](https://verapdf.org) (`--flavour ua1`) if -conformance matters to you. For a document known not to conform, pass -`declarePdfUA: false` in the compile options to produce an honest -tagged-but-unclaimed PDF instead of a false claim. +const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, { + wasm: compilerWasmUrl, +}); +``` ### Exporter options @@ -237,18 +217,23 @@ const defaultOptions = { dictionary: locales.en, // the colors used for highlighting, background colors and font colors colors: COLORS_DEFAULT, // defaults from @blocknote/core - // font families, see "Fonts & offline use" above + // font families (DEFAULT_FONT_FAMILY / DEFAULT_MONO_FONT_FAMILY) and the + // font bytes they resolve against - see "Fonts" above fontFamily: "Inter 18pt", monoFontFamily: "Geist Mono", + fonts: loadDefaultBodyFonts(), + emojiFont: loadDefaultEmojiFont(), // base font size in points fontSize: 12, + // where the compiler wasm loads from - see "Self-hosting the compiler" + wasm: undefined, // the packaged wasm }; ``` ### Exporting Typst markup The underlying Typst source export is available standalone (e.g. to compile -with your own Typst toolchain, including server-side) — see +with your own Typst toolchain, including server-side); see [Typst export](/docs/features/export/typst). ### Deprecated: the react-pdf exporter @@ -266,6 +251,6 @@ import { } from "@blocknote/xl-pdf-exporter/react-pdf"; ``` -Note that its mappings are react-pdf mappings — when migrating to the new +Note that its mappings are react-pdf mappings; when migrating to the new exporter, custom blocks need a [Typst mapping](#custom-mappings--custom-schemas) instead. diff --git a/docs/content/docs/features/export/typst.mdx b/docs/content/docs/features/export/typst.mdx index b498e25194..1f545fc38a 100644 --- a/docs/content/docs/features/export/typst.mdx +++ b/docs/content/docs/features/export/typst.mdx @@ -8,7 +8,7 @@ path: /docs/export-to-typst # Typst Export It's possible to export BlockNote documents to [Typst](https://typst.app) -markup — for example to post-process documents in a Typst workflow, or to +markup, for example to post-process documents in a Typst workflow, or to compile them to PDF with your own Typst toolchain (including server-side). This exporter is also the foundation of the [PDF exporter](/docs/features/export/pdf), which compiles its output to @@ -43,23 +43,112 @@ const typst = await exporter.toTypst(editor.document, { title: "My document", lang: "en", }); +``` + +### Images & assets + +The markup references the document's images by virtual paths like +`/assets/asset-0`. After `toTypst`, the exporter's `assetFiles` map holds +the bytes for those paths. Map them into your Typst compiler's filesystem +before compiling: -// The document's images, keyed by the virtual path referenced in the markup - -// map these into your Typst compiler's filesystem before compiling -const assets = exporter.assetFiles; +```typescript +const assets = exporter.assetFiles; // Map ``` -`toTypst` takes the same per-document options as the PDF exporter's export -methods (title, author, language, paper size, margins, page header/footer) — -see [customizing the PDF](/docs/features/export/pdf#customizing-the-pdf) — -and the constructor takes the same -[exporter options](/docs/features/export/pdf#exporter-options). +(The [PDF exporter](/docs/features/export/pdf) does this automatically.) + +### Customizing the output -### Custom mappings +`toTypst` takes per-export options: the document metadata and page setup. -Mappings convert schema elements into Typst markup strings, and they are -shared with the PDF exporter — one custom-block mapping serves both formats. -See [custom mappings](/docs/features/export/pdf#custom-mappings--custom-schemas) -on the PDF page; the [math](/docs/features/blocks/math#pdf) and -[diagram](/docs/features/blocks/diagrams) blocks ship ready-made Typst -mappings from their `typst-exporter` subpaths. +```typescript +const typst = await exporter.toTypst(editor.document, { + // Document title, written to the document metadata + title: "My document", + // Document author + author: "John Doe", + // BCP-47 language tag of the document's natural language + lang: "en", + // Typst paper name, e.g. "a4" (default) or "us-letter" + paper: "a4", + // Page margin as a Typst length + margin: "48pt", + // Raw Typst markup for the running page header / footer, e.g. a + // page counter: "#context counter(page).display()" + header: "My document", + footer: "#context counter(page).display()", +}); +``` + +Only the options you pass are emitted; the markup declares no title, +author, or language on its own. + +### Custom mappings / custom schemas + +A _mapping_ defines how to convert a BlockNote schema element (a Block, +Inline Content, or Style) into a **Typst markup string**. The same mappings +drive the [PDF exporter](/docs/features/export/pdf), so one custom-block +mapping serves both formats. + +If you're using a [custom schema](/docs/features/custom-schemas) in your +editor, or if you want to overwrite how default BlockNote elements are +converted, you can pass your own `mappings`: + +```typescript +import { + TypstExporter, + typstDefaultSchemaMappings, + strLit, +} from "@blocknote/xl-typst-exporter"; + +new TypstExporter(schema, { + ...typstDefaultSchemaMappings, + blockMapping: { + ...typstDefaultSchemaMappings.blockMapping, + myCustomBlock: (block, exporter) => { + // Return Typst markup; `strLit` safely embeds user text as a + // Typst string literal. + return `#${strLit("My custom block")}`; + }, + }, +}); +``` + +For a block with inline content, render it the way the default mappings do: +`exporter.transformInlineContent(block.content).join("")` (inline results are +markup strings, so plain concatenation composes them). + + +### Math & diagram blocks + +The [math](/docs/features/blocks/math) and +[diagram](/docs/features/blocks/diagrams) blocks ship their own Typst +mappings. See [exporting math](/docs/features/blocks/math#typst--pdf) and +[exporting diagrams](/docs/features/blocks/diagrams#exporting) for the +setup. + +### Exporter options + +The `TypstExporter` constructor takes an optional third `options` parameter: + +```typescript +const defaultOptions = { + // a function to resolve external resources (e.g. images) in order to avoid + // CORS issues; by default, this calls a BlockNote hosted server-side proxy + resolveFileUrl: corsProxyResolveFileUrl, + // the strings rendered into the exported document (file link texts, error + // placeholders); pass a locale from @blocknote/core/locales (or your + // editor's dictionary) to export in another language + dictionary: locales.en, + // the colors used for highlighting, background colors and font colors + colors: COLORS_DEFAULT, // defaults from @blocknote/core + // the font families the markup references - they must match fonts loaded + // into whichever Typst compiler you use (exported as DEFAULT_FONT_FAMILY + // and DEFAULT_MONO_FONT_FAMILY) + fontFamily: "Inter 18pt", + monoFontFamily: "Geist Mono", + // base font size in points + fontSize: 12, +}; +``` diff --git a/docs/next.config.ts b/docs/next.config.ts index 02981ea446..e5b5773b4b 100644 --- a/docs/next.config.ts +++ b/docs/next.config.ts @@ -19,19 +19,16 @@ const config = { redirects, // `next build` runs Turbopack; the demo examples are written for Vite, so // give their `?url` asset imports (fonts, the Typst compiler wasm) the - // same URL-string semantics here, and stub the compiler's optional - // canvas-renderer peer (the pdf-ua demo only compiles, never renders to - // canvas, so the peer isn't installed). + // same URL-string semantics here. turbopack: { resolveAlias: { - "@myriaddreamin/typst-ts-renderer": "./components/typstRendererStub.ts", // Both key forms on purpose: Turbopack matches the alias key against // the request with its query in some resolution paths and without it // in others, so covering `/wasm` and `/wasm?url` makes the demo's // `?url` import resolve in both. - "@myriaddreamin/typst-ts-web-compiler/wasm": + "@blocknote/xl-typst-compiler/wasm": "./components/typstCompilerWasmUrl.ts", - "@myriaddreamin/typst-ts-web-compiler/wasm?url": + "@blocknote/xl-typst-compiler/wasm?url": "./components/typstCompilerWasmUrl.ts", }, }, diff --git a/docs/package.json b/docs/package.json index b3d3cd7313..9e4e282da9 100644 --- a/docs/package.json +++ b/docs/package.json @@ -47,8 +47,6 @@ "@marsidev/react-turnstile": "^1.4.2", "@mui/icons-material": "^5.16.1", "@mui/material": "^5.16.1", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3", - "@myriaddreamin/typst.ts": "^0.8.0-rc3", "@orama/orama": "^3.1.18", "@polar-sh/better-auth": "^1.6.4", "@polar-sh/sdk": "^0.42.2", @@ -115,7 +113,8 @@ "y-websocket": "^2.1.0", "yjs": "^13.6.27", "zod": "^4.3.5", - "@blocknote/xl-typst-exporter": "workspace:*" + "@blocknote/xl-typst-exporter": "workspace:*", + "@blocknote/xl-typst-compiler": "workspace:*" }, "devDependencies": { "@blocknote/code-block": "workspace:*", diff --git a/examples/05-interoperability/11-converting-blocks-to-pdf-ua/.bnexample.json b/examples/05-interoperability/11-converting-blocks-to-pdf-ua/.bnexample.json index 596cc792ba..0a39b03402 100644 --- a/examples/05-interoperability/11-converting-blocks-to-pdf-ua/.bnexample.json +++ b/examples/05-interoperability/11-converting-blocks-to-pdf-ua/.bnexample.json @@ -9,8 +9,7 @@ "@blocknote/xl-multi-column": "latest", "@blocknote/math-block": "latest", "@blocknote/diagram-block": "latest", - "@myriaddreamin/typst.ts": "^0.8.0-rc3", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-compiler": "latest" }, "pro": true } diff --git a/examples/05-interoperability/11-converting-blocks-to-pdf-ua/package.json b/examples/05-interoperability/11-converting-blocks-to-pdf-ua/package.json index a81fc36580..515f5b808e 100644 --- a/examples/05-interoperability/11-converting-blocks-to-pdf-ua/package.json +++ b/examples/05-interoperability/11-converting-blocks-to-pdf-ua/package.json @@ -24,8 +24,7 @@ "@blocknote/xl-multi-column": "latest", "@blocknote/math-block": "latest", "@blocknote/diagram-block": "latest", - "@myriaddreamin/typst.ts": "^0.8.0-rc3", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-compiler": "latest" }, "devDependencies": { "@types/react": "^19.2.3", diff --git a/examples/05-interoperability/11-converting-blocks-to-pdf-ua/src/App.tsx b/examples/05-interoperability/11-converting-blocks-to-pdf-ua/src/App.tsx index 7632d1b559..bfce34f37a 100644 --- a/examples/05-interoperability/11-converting-blocks-to-pdf-ua/src/App.tsx +++ b/examples/05-interoperability/11-converting-blocks-to-pdf-ua/src/App.tsx @@ -36,10 +36,12 @@ import { multiColumnDropCursor, withMultiColumn, } from "@blocknote/xl-multi-column"; -// Bundle the Typst compiler wasm so it resolves locally (no CDN / importer). +// Bundle the Typst compiler wasm explicitly (it would otherwise load from +// the package's own files - also CDN-free - but an explicit URL keeps the +// bundling visible in this example). // Fonts need no setup: the exporter's bundled defaults (Inter, Geist Mono, // math, emoji - matching the editor) load lazily from the package. -import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url"; +import compilerWasmUrl from "@blocknote/xl-typst-compiler/wasm?url"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import "./styles.css"; @@ -60,25 +62,45 @@ function usePdfUA( blocks: Block[], ) { const [pdfUrl, setPdfUrl] = useState(); - const [status, setStatus] = useState<"loading" | "ready" | "error">( - "loading", - ); + const [status, setStatus] = useState< + "loading" | "ready" | "unclaimed" | "error" + >("loading"); useEffect(() => { let stale = false; setStatus("loading"); void (async () => { try { - const blob = await makeExporter().toBlob( - blocks, - { getModule: () => compilerWasmUrl }, - { title: "BlockNote document", lang: "en" }, - ); + const result = await makeExporter().toPDF(blocks, { + title: "BlockNote document", + lang: "en", + }); if (stale) { return; } - setPdfUrl(URL.createObjectURL(blob)); - setStatus("ready"); + // A document that fails to compile (e.g. text no supplied font + // covers) is an expected outcome, reported in the result. + if (result.error) { + // eslint-disable-next-line no-console + console.error( + "PDF export failed:", + result.compileErrors.map((d) => d.message).join("; "), + ); + setStatus("error"); + return; + } + // A nonconforming document (e.g. one not starting with an H1) still + // exports - tagged but without the PDF/UA-1 claim; surface why, and + // show a distinct status instead of claiming conformance. + if (!result.pdfUA.declared && result.pdfUA.reason === "nonconforming") { + // eslint-disable-next-line no-console + console.info( + "Exported without PDF/UA-1 declaration:", + result.pdfUA.violations.map((v) => v.message).join("; "), + ); + } + setPdfUrl(URL.createObjectURL(result.blob)); + setStatus(result.pdfUA.declared ? "ready" : "unclaimed"); } catch (e) { if (stale) { return; @@ -182,20 +204,28 @@ export default function App() { // every image/diagram variant it has ever rendered. const makeExporter = useCallback( () => - new PDFExporter(editor.schema, { - ...typstDefaultSchemaMappings, - blockMapping: { - ...typstDefaultSchemaMappings.blockMapping, - // Renders math blocks as native Typst equations, and diagrams as - // embedded images - both carrying alt text for PDF/UA. - mathBlock: mathBlockMapping, - diagram: diagramBlockMapping, + new PDFExporter( + editor.schema, + { + ...typstDefaultSchemaMappings, + blockMapping: { + ...typstDefaultSchemaMappings.blockMapping, + // Renders math blocks as native Typst equations, and diagrams as + // embedded images - both carrying alt text for PDF/UA. + mathBlock: mathBlockMapping, + diagram: diagramBlockMapping, + }, + inlineContentMapping: { + ...typstDefaultSchemaMappings.inlineContentMapping, + math: inlineMathMapping, + }, }, - inlineContentMapping: { - ...typstDefaultSchemaMappings.inlineContentMapping, - math: inlineMathMapping, + { + // The bundled compiler wasm (see the import above) - engine setup + // belongs to the exporter, per-document facts go to toPDF. + wasm: compilerWasmUrl, }, - }), + ), [editor], ); @@ -230,7 +260,9 @@ export default function App() { ? "Generating…" : status === "error" ? "Export failed (see console)" - : "✓ Tagged PDF/UA-1"; + : status === "unclaimed" + ? "Tagged PDF, no UA-1 claim (see console)" + : "✓ Tagged PDF/UA-1"; return (
diff --git a/packages/diagram-block/package.json b/packages/diagram-block/package.json index 9f3bff700a..ae7bb72a2e 100644 --- a/packages/diagram-block/package.json +++ b/packages/diagram-block/package.json @@ -104,7 +104,7 @@ "typescript": "^7.0.2", "vite-plus": "catalog:", "@blocknote/xl-typst-exporter": "workspace:^", - "@myriaddreamin/typst-ts-node-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-compiler": "workspace:^" }, "peerDependencies": { "@blocknote/xl-docx-exporter": "workspace:^", diff --git a/packages/diagram-block/src/typst-exporter/__snapshots__/withDiagramMappings/diagramDocument.typ b/packages/diagram-block/src/typst-exporter/__snapshots__/withDiagramMappings/diagramDocument.typ index 9fa48da25a..49a19cac1e 100644 --- a/packages/diagram-block/src/typst-exporter/__snapshots__/withDiagramMappings/diagramDocument.typ +++ b/packages/diagram-block/src/typst-exporter/__snapshots__/withDiagramMappings/diagramDocument.typ @@ -1,5 +1,5 @@ -#set document(title: "Diagram", author: "") -#set text(font: "Inter 18pt", size: 12pt, lang: "en") +#set document(title: "Diagram") +#set text(font: "Inter 18pt", size: 12pt) #set page(paper: "a4", margin: 48pt) #set par(leading: 0.78em, spacing: 0pt, justify: false) #set block(spacing: 0pt) diff --git a/packages/math-block/package.json b/packages/math-block/package.json index 9099b586f3..5bea233906 100644 --- a/packages/math-block/package.json +++ b/packages/math-block/package.json @@ -113,7 +113,7 @@ "xml-formatter": "^3.6.7", "@blocknote/xl-typst-exporter": "workspace:^", "tex2typst": "^0.6.2", - "@myriaddreamin/typst-ts-node-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-compiler": "workspace:^" }, "peerDependencies": { "@blocknote/xl-docx-exporter": "workspace:^", diff --git a/packages/math-block/src/typst-exporter/__snapshots__/withMathMappings/mathDocument.typ b/packages/math-block/src/typst-exporter/__snapshots__/withMathMappings/mathDocument.typ index 1bad6f9a44..942f236cf8 100644 --- a/packages/math-block/src/typst-exporter/__snapshots__/withMathMappings/mathDocument.typ +++ b/packages/math-block/src/typst-exporter/__snapshots__/withMathMappings/mathDocument.typ @@ -1,5 +1,5 @@ -#set document(title: "Math", author: "") -#set text(font: "Inter 18pt", size: 12pt, lang: "en") +#set document(title: "Math") +#set text(font: "Inter 18pt", size: 12pt) #set page(paper: "a4", margin: 48pt) #set par(leading: 0.78em, spacing: 0pt, justify: false) #set block(spacing: 0pt) diff --git a/packages/xl-pdf-exporter/package.json b/packages/xl-pdf-exporter/package.json index 015da3cfa5..4099963cf4 100644 --- a/packages/xl-pdf-exporter/package.json +++ b/packages/xl-pdf-exporter/package.json @@ -56,9 +56,7 @@ "@blocknote/core": "workspace:^", "@blocknote/xl-multi-column": "workspace:^", "@blocknote/xl-typst-exporter": "workspace:^", - "@myriaddreamin/typst.ts": "^0.8.0-rc3", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3", - "@cantoo/pdf-lib": "^2.9.1" + "@blocknote/xl-typst-compiler": "workspace:^" }, "devDependencies": { "@blocknote/shared": "workspace:^", @@ -72,9 +70,9 @@ "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^7.0.2", "vite-plus": "catalog:", - "@myriaddreamin/typst-ts-node-compiler": "^0.8.0-rc3", "@types/node": "22.13.13", - "@react-pdf/renderer": "^4.5.1" + "@react-pdf/renderer": "^4.5.1", + "@cantoo/pdf-lib": "^2.9.1" }, "peerDependencies": { "react": "^18.0 || ^19.0 || >= 19.0.0-rc", diff --git a/packages/xl-pdf-exporter/src/index.ts b/packages/xl-pdf-exporter/src/index.ts index 063eb48e5b..987e657e20 100644 --- a/packages/xl-pdf-exporter/src/index.ts +++ b/packages/xl-pdf-exporter/src/index.ts @@ -1,179 +1,26 @@ -import { - Block, - BlockNoteSchema, - BlockSchema, - Exporter, - InlineContentSchema, - StyleSchema, -} from "@blocknote/core"; -import { - TypstDocumentOptions, - TypstExporter, - TypstExporterOptions, -} from "@blocknote/xl-typst-exporter"; -import { - compileTypstToTaggedPdf, - TypstCompileOptions, -} from "./pdfua/compileBrowser.js"; -import { - DEFAULT_EMOJI_FONT_FAMILY, - loadDefaultBodyFonts, - loadDefaultEmojiFont, -} from "./pdfua/defaultFonts.js"; -import { declarePdfUA } from "./pdfua/postProcess.js"; - // The Typst layer is re-exported so PDF consumers need only this package. // The mappings passed to `PDFExporter` ARE Typst mappings - a custom block's // single Typst mapping serves both the standalone `.typ` export // (@blocknote/xl-typst-exporter) and the PDF export. export * from "@blocknote/xl-typst-exporter"; -export { declarePdfUA } from "./pdfua/postProcess.js"; export { - compileTypstToTaggedPdf, + PDFExporter, + type PdfExporterOptions, + type PdfExportOptions, + type PdfExportResult, + type PdfUAResult, + type PdfUAViolation, +} from "./pdfExporter.js"; +export { + compileTypstToPdf, type TypstCompileOptions, -} from "./pdfua/compileBrowser.js"; - -/** Options for {@link PDFExporter}'s export methods. */ -export type PdfExportOptions = TypstCompileOptions & { - /** - * Declare PDF/UA-1 conformance on the produced document (the - * `pdfuaid:part` XMP identifier plus `/ViewerPreferences/DisplayDocTitle`; - * see {@link declarePdfUA}). - * - * The tagged structure tree is always emitted either way - this controls - * only the conformance *claim*. Opt out when a document is known not to - * conform (e.g. its first heading isn't level 1, or an image lacks any - * caption/name to use as alt text): a tagged-but-unclaimed PDF is honest, - * while a false claim fails validation. Either way, verify output with - * veraPDF (`--flavour ua1`) in CI - the declaration does not itself - * guarantee conformance of arbitrary input. - * - * @default true - */ - declarePdfUA?: boolean; -}; - -/** - * Exports BlockNote documents to accessible, tagged PDF/UA-1 files, powered - * by Typst (compiled to wasm - the whole pipeline runs client-side). - * - * The mappings are *Typst* mappings ({@link typstDefaultSchemaMappings}); - * this class is a {@link TypstExporter} plus the PDF compile step, so - * everything documented there applies - including that an exporter instance - * accumulates assets for its lifetime: create a fresh exporter per export - * when repeatedly exporting changing content (construction is cheap). - * - * Zero-config exports match the editor: the compile options' `fonts` and - * `emojiFont` each default (independently) to the bundled set - Inter, - * Geist Mono, New Computer Modern Math resp. Noto Color Emoji - loaded - * lazily from the package. Pass a value (or an explicit `[]` for none) to - * take over either. - * - * Concurrency: the shared compile stage is serialized internally (see - * `compileTypstToTaggedPdf`) and the asset registry is append-only, so - * overlapping exports are *safe* - but like any async calls, independent - * exports may complete out of call order. Callers that only want the newest - * result (e.g. a live preview exporting on every change) should guard for - * that themselves, as the pdf-ua example does. - * - * (The previous react-pdf based exporter is still available from - * `@blocknote/xl-pdf-exporter/react-pdf` during its deprecation window.) - */ -export class PDFExporter< - B extends BlockSchema, - S extends StyleSchema, - I extends InlineContentSchema, -> extends TypstExporter { - public constructor( - schema: BlockNoteSchema, - mappings: Exporter< - NoInfer, - NoInfer, - NoInfer, - string, - string, - (inner: string) => string, - string - >["mappings"], - options?: Partial, - ) { - super(schema, mappings, { - // The bundled default fonts (see `defaultFonts.ts`) include Noto - // Color Emoji; declaring the family by default lets multi-codepoint - // emoji shape correctly with zero config. Callers supplying their own - // fonts can override (or unset) it. - emojiFontFamily: DEFAULT_EMOJI_FONT_FAMILY, - ...options, - }); - } - - /** - * Export a document to PDF bytes: BlockNote document -> Typst -> tagged - * PDF (wasm) -> declared PDF/UA-1 (unless opted out via - * {@link PdfExportOptions.declarePdfUA}). - */ - public async toBytes( - blocks: Block[], - options: PdfExportOptions = {}, - documentOptions?: TypstDocumentOptions, - ): Promise { - const { declarePdfUA: withDeclaration = true, ...compileOptions } = options; - const typst = await this.toTypst(blocks, documentOptions); - // The images collected during the export are mapped into the compiler - // alongside (not instead of) any assets the caller supplied - e.g. an - // image referenced from the header/footer markup. A caller asset under - // the exporter's own key space would be silently shadowed by the merge, - // so that's rejected loudly instead. - for (const key of compileOptions.assets?.keys() ?? []) { - if (this.assetFiles.has(key)) { - throw new Error( - `PDFExporter: the caller-supplied asset "${key}" collides with an ` + - `asset registered by the exporter - use a path outside /assets/`, - ); - } - } - // Zero-config fonts: each option defaults independently to the bundled - // set matching the editor - `undefined` means "use the default", a - // supplied value (including an explicit `[]` for none) takes over. The - // independence keeps the bytes consistent with the constructor's - // independent `emojiFontFamily` default: custom body fonts don't - // silently drop emoji support, and vice versa. - const [fonts, emojiFont] = await Promise.all([ - compileOptions.fonts === undefined - ? loadDefaultBodyFonts() - : compileOptions.fonts, - compileOptions.emojiFont === undefined - ? loadDefaultEmojiFont() - : compileOptions.emojiFont, - ]); - const taggedPdf = await compileTypstToTaggedPdf(typst, { - ...compileOptions, - fonts, - emojiFont, - assets: new Map([...(compileOptions.assets ?? []), ...this.assetFiles]), - }); - // TODO: collapse this into one step once the web compiler supports native - // PDF/UA-1 export. typst's node compiler already accepts - // `pdfStandard: "ua-1"`, which emits a veraPDF-conformant PDF/UA-1 - // directly (verified: 0 failed checks) and validates accessibility at - // compile time (errors on missing alt text) — making `declarePdfUA` (and - // the `@cantoo/pdf-lib` dependency) unnecessary. `@myriaddreamin/ - // typst.ts`'s browser `pdf()` does not yet expose a PDF standard option, - // so the post-process is still required for the wasm path. - return withDeclaration ? declarePdfUA(taggedPdf) : taggedPdf; - } - - /** Export a document to a PDF Blob (e.g. for downloads / object URLs). */ - public async toBlob( - blocks: Block[], - options: PdfExportOptions = {}, - documentOptions?: TypstDocumentOptions, - ): Promise { - const bytes = await this.toBytes(blocks, options, documentOptions); - // The pipeline always returns a view over a plain (non-shared) buffer; - // the cast narrows `ArrayBufferLike` for `BlobPart`. - return new Blob([bytes as Uint8Array], { - type: "application/pdf", - }); - } -} +} from "./pdfua/compileTypst.js"; +export { + loadDefaultBodyFonts, + loadDefaultEmojiFont, +} from "./pdfua/defaultFonts.js"; +export { + isPdfStandardViolation, + type CompilePdfResult, + type TypstDiagnostic, +} from "@blocknote/xl-typst-compiler"; diff --git a/packages/xl-pdf-exporter/src/pdfExporter.test.ts b/packages/xl-pdf-exporter/src/pdfExporter.test.ts index ff3ad25d6e..fe5421bc01 100644 --- a/packages/xl-pdf-exporter/src/pdfExporter.test.ts +++ b/packages/xl-pdf-exporter/src/pdfExporter.test.ts @@ -6,8 +6,15 @@ import { import { typstDefaultSchemaMappings } from "@blocknote/xl-typst-exporter"; import { partialBlocksToBlocksForTesting } from "@shared/formatConversionTestUtil.js"; import { testResolveFileUrl } from "@shared/util/testFileResolver.js"; +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; import { describe, expect, it } from "vite-plus/test"; -import { PDFExporter } from "./index.js"; +import { + loadDefaultBodyFonts, + PDFExporter, + type PdfExporterOptions, + type PdfExportResult, +} from "./index.js"; const schema = BlockNoteSchema.create({ blockSpecs: { @@ -16,25 +23,254 @@ const schema = BlockNoteSchema.create({ }, }); +function read(p: string) { + return new Uint8Array(readFileSync(resolve(process.cwd(), p))); +} + +// Node has no URL-relative wasm loading, so the module bytes are passed +// explicitly. A minimal font set keeps the compiles fast; tests about the +// bundled defaults leave `fonts`/`emojiFont` unset instead. +const fontDir = "../../shared/assets/fonts"; +function wasmAndFonts(): Pick< + Partial, + "wasm" | "fonts" | "emojiFont" +> { + return { + wasm: read("../xl-typst-compiler/pkg/blocknote_typst_wasm_bg.wasm"), + fonts: [ + read(join(fontDir, "inter/Inter_18pt-Regular.ttf")), + read(join(fontDir, "inter/Inter_18pt-Bold.ttf")), + ], + emojiFont: [], + }; +} + +function exporter(options?: Partial) { + return new PDFExporter(schema, typstDefaultSchemaMappings, { + resolveFileUrl: testResolveFileUrl, + ...wasmAndFonts(), + ...options, + }); +} + +const latin1 = (bytes: Uint8Array) => new TextDecoder("latin1").decode(bytes); + +function expectExported( + result: PdfExportResult, +): Extract { + if (result.error) { + throw new Error( + `expected an export, got: ${result.compileErrors + .map((d) => d.message) + .join(" | ")}`, + ); + } + return result; +} + describe("PDFExporter", () => { it("rejects caller assets that collide with exporter-registered ones", async () => { - const exporter = new PDFExporter(schema, typstDefaultSchemaMappings, { - resolveFileUrl: testResolveFileUrl, - }); - // The document's image registers `/assets/asset-0`; a caller asset under // the same key would be silently shadowed by the merge, so the export // must fail loudly instead (before ever reaching the compiler). await expect( - exporter.toBytes( + exporter().toPDF( partialBlocksToBlocksForTesting(schema, [ { type: "image", props: { url: "https://placehold.co/60x60.png", caption: "Cap" }, }, ]), - { assets: new Map([["/assets/asset-0", new Uint8Array([1])]]) }, + { + assets: new Map([["/assets/asset-0", new Uint8Array([1])]]), + lang: "en", + }, ), ).rejects.toThrow('the caller-supplied asset "/assets/asset-0" collides'); }); }); + +describe("PDFExporter PDF/UA declaration", () => { + const conforming = () => + partialBlocksToBlocksForTesting(schema, [ + { type: "heading", props: { level: 1 }, content: "Title" }, + { type: "paragraph", content: "Body" }, + ]); + + it("declares PDF/UA-1 for a conforming document", async () => { + const { bytes, pdfUA } = expectExported( + await exporter().toPDF(conforming(), { title: "Doc", lang: "en" }), + ); + expect(pdfUA).toEqual({ declared: true }); + expect(latin1(bytes)).toContain("pdfuaid"); + expect(latin1(bytes)).toContain("StructTreeRoot"); + }); + + it("falls back to an unclaimed export for a nonconforming document", async () => { + // First heading is level 2: a PDF/UA-1 violation Typst catches at + // compile time. The export still succeeds - tagged, but without the + // conformance claim - and reports why. + const blocks = partialBlocksToBlocksForTesting(schema, [ + { type: "heading", props: { level: 2 }, content: "Not level one" }, + ]); + const { bytes, pdfUA } = expectExported( + await exporter().toPDF(blocks, { title: "Doc", lang: "en" }), + ); + expect(pdfUA.declared).toBe(false); + if (!pdfUA.declared && pdfUA.reason === "nonconforming") { + expect(pdfUA.violations.length).toBeGreaterThan(0); + expect(pdfUA.violations[0].message).toContain( + "first heading must be of level 1", + ); + } else { + throw new Error(`unexpected pdfUA result: ${JSON.stringify(pdfUA)}`); + } + expect(latin1(bytes)).not.toContain("pdfuaid"); + expect(latin1(bytes)).toContain("StructTreeRoot"); + }); + + it("rejects declaring without a document language (caller-args error)", async () => { + // A no-lang export would still carry Typst's silently defaulted + // language (English) in the PDF - a wrong `/Lang` no validator can + // catch. Unlike content violations, the language is an integration + // decision only the caller can make, so this fails loudly instead of + // quietly producing forever-unclaimed exports. + await expect( + exporter().toPDF(conforming(), { title: "Doc" }), + ).rejects.toThrow("requires the document's language"); + // Opting out of the claim makes the language optional again. + const { pdfUA } = expectExported( + await exporter().toPDF(conforming(), { + tryDeclarePdfUA: false, + title: "Doc", + }), + ); + expect(pdfUA).toEqual({ + declared: false, + reason: "tryDeclarePdfUA-disabled", + }); + }); + + it("skips validation and claim with tryDeclarePdfUA: false", async () => { + const { bytes, pdfUA } = expectExported( + await exporter().toPDF(conforming(), { + tryDeclarePdfUA: false, + title: "Doc", + lang: "en", + }), + ); + expect(pdfUA).toEqual({ + declared: false, + reason: "tryDeclarePdfUA-disabled", + }); + expect(latin1(bytes)).not.toContain("pdfuaid"); + expect(latin1(bytes)).toContain("StructTreeRoot"); + }); + + it("forwards compile warnings alongside a successful export", async () => { + // An unknown font family is a warning, not an error - the export + // succeeds (Typst falls back across the loaded fonts), and the result + // surfaces what an integrator would want to know. + const warned = exporter({ fontFamily: "No Such Family" }); + const result = expectExported( + await warned.toPDF(conforming(), { title: "Doc", lang: "en" }), + ); + expect( + result.compileWarnings.some((w) => + w.message.includes("unknown font family"), + ), + ).toBe(true); + }); + + it("keeps the bundled default fonts in sync with the referenced families", async () => { + // Two things can drift independently: the family-name defaults + // (TypstExporter's fontFamily/monoFontFamily, PDFExporter's + // emojiFontFamily) and the bundled font files in defaultFonts.ts. + // Either drift - a renamed default or a swapped file - surfaces as an + // `unknown font family` compile warning, so a zero-config export of a + // document exercising body (regular/bold/italic), code, and emoji must + // warn about nothing. (The math pairing is pinned the same way by + // math-block's typst-exporter test.) + const blocks = partialBlocksToBlocksForTesting(schema, [ + { type: "heading", props: { level: 1 }, content: "Title" }, + { + type: "paragraph", + content: [ + { type: "text", text: "plain ", styles: {} }, + { type: "text", text: "bold ", styles: { bold: true } }, + { type: "text", text: "italic ", styles: { italic: true } }, + { type: "text", text: "emoji 😀🧑‍💻", styles: {} }, + ], + }, + { type: "codeBlock", content: "const mono = true;" }, + ]); + // Only the wasm is supplied (node can't URL-load it); fonts and + // emojiFont stay unset so the real bundled defaults load. + const zeroConfig = new PDFExporter(schema, typstDefaultSchemaMappings, { + resolveFileUrl: testResolveFileUrl, + wasm: wasmAndFonts().wasm, + }); + const result = expectExported( + await zeroConfig.toPDF(blocks, { title: "Doc", lang: "en" }), + ); + expect(result.compileWarnings).toEqual([]); + expect(result.pdfUA).toEqual({ declared: true }); + }); + + it("extends the defaults by spreading the exported loaders", async () => { + // `fonts` replaces the bundled default set; extension is composition, + // not API: spread the exported defaults and append - names and bytes + // configured side by side in the constructor. (The appended bytes here + // are already part of the defaults, so the compiler's dedup makes this + // a pure wiring test.) + const extended = exporter({ + fonts: loadDefaultBodyFonts().then((fonts) => [ + ...fonts, + read(join(fontDir, "inter/Inter_18pt-Regular.ttf")), + ]), + emojiFont: undefined, + }); + const result = expectExported( + await extended.toPDF(conforming(), { title: "Doc", lang: "en" }), + ); + expect(latin1(result.bytes)).toContain("pdfuaid"); + expect(result.compileWarnings).toEqual([]); + }); + + it("carries the PDF as a Blob alongside the bytes", async () => { + const result = await exporter().toPDF(conforming(), { + title: "Doc", + lang: "en", + }); + if (result.error) { + throw new Error(result.compileErrors.map((d) => d.message).join(" | ")); + } + expect(result.blob.type).toBe("application/pdf"); + expect(result.blob.size).toBe(result.bytes.byteLength); + // Lazily created, memoized: repeated access yields the same instance. + expect(result.blob).toBe(result.blob); + expect(result.pdfUA).toEqual({ declared: true }); + // The minimal test font set omits the preamble's code/emoji families, + // which surfaces as unknown-font-family warnings - nothing else. + expect( + result.compileWarnings.every((w) => + w.message.includes("unknown font family"), + ), + ).toBe(true); + }); + + it("reports compile failures as a typed error, not as violations", async () => { + // Broken raw markup (here via the caller-supplied header) is a compile + // failure - an expected outcome carried in the result, distinct from + // conformance violations (it must not degrade to an unclaimed export). + const result = await exporter().toPDF(conforming(), { + title: "Doc", + lang: "en", + header: "#thisFunctionDoesNotExist()", + }); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect(result.compileErrors.length).toBeGreaterThan(0); + } + }); +}); diff --git a/packages/xl-pdf-exporter/src/pdfExporter.ts b/packages/xl-pdf-exporter/src/pdfExporter.ts new file mode 100644 index 0000000000..a5057400f7 --- /dev/null +++ b/packages/xl-pdf-exporter/src/pdfExporter.ts @@ -0,0 +1,367 @@ +import { + Block, + BlockNoteSchema, + BlockSchema, + Exporter, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { + isPdfStandardViolation, + type TypstDiagnostic, +} from "@blocknote/xl-typst-compiler"; +import { + TypstDocumentOptions, + TypstExporter, + TypstExporterOptions, +} from "@blocknote/xl-typst-exporter"; +import { + compileTypstToPdf, + TypstCompileOptions, +} from "./pdfua/compileTypst.js"; +import { + DEFAULT_EMOJI_FONT_FAMILY, + loadDefaultBodyFonts, + loadDefaultEmojiFont, +} from "./pdfua/defaultFonts.js"; + +/** + * {@link PDFExporter}'s options: everything the *exporter* is (like the + * other exporters' option types) - the Typst styling options including the + * font *family names*, plus the engine inputs those names resolve against: + * the font *bytes* and the compiler wasm. Names and bytes side by side, so + * a custom font setup lives in one place. + * + * Like the other exporters, this is the full options shape; the + * constructor takes a `Partial` of it and fills the defaults. + */ +export type PdfExporterOptions = TypstExporterOptions & { + /** + * The compiler wasm module: a URL (string or `URL`) or the module bytes. + * Omitted, it loads from `@blocknote/xl-typst-compiler`'s own package + * files (no CDN). Loaded once per page; the first exporter's value wins. + * + * Optional even in this full options shape: absence is not an unfilled + * default but a complete configuration - resolution is delegated to the + * wasm-bindgen glue's own module-relative URL, the one pattern bundlers + * reliably detect and rewrite (a value computed here couldn't be). + */ + wasm?: TypstCompileOptions["wasm"]; + /** + * The body fonts (as bytes, or a promise of them) the exporter's + * `fontFamily` / `monoFontFamily` names resolve against. Defaults to the + * bundled set matching the editor; to *extend* it, spread the exported + * loader: `loadDefaultBodyFonts().then((f) => [...f, myCjkFont])`. + * Typst selects fonts by the family name embedded in each file's own + * name table; a referenced-but-missing family surfaces as an + * `unknown font family` entry in the result's `compileWarnings`. + */ + fonts: Uint8Array[] | Promise; + /** + * An emoji-capable font (or fonts) as bytes, or a promise of them - + * what {@link TypstExporterOptions.emojiFontFamily} resolves against. + * Defaults (independently of {@link fonts}) to the bundled Noto Color + * Emoji. Browsers give the compiler no access to OS fonts, so without + * one emoji render as missing glyphs (and fail PDF/UA). + */ + emojiFont: Uint8Array | Uint8Array[] | Promise; +}; + +/** + * Per-export options for {@link PDFExporter.toPDF} - one bag, like the + * other exporters' export methods: the per-document options (title, + * language, page setup - {@link TypstDocumentOptions}) plus the per-export + * compile inputs. + */ +export type PdfExportOptions = TypstDocumentOptions & { + /** + * Extra files to map into the compiler's virtual filesystem, keyed by + * the Typst path referenced in the source - e.g. an image used by a + * caller-supplied {@link TypstDocumentOptions.header}. The exporter's + * own collected assets are merged in automatically. + */ + assets?: TypstCompileOptions["assets"]; + /** + * PDF creation timestamp in seconds since the Unix epoch (UTC). Pass a + * fixed value for byte-reproducible output; omitted means no timestamp. + */ + creationTimestamp?: number; + /** + * Try to produce a *declared* PDF/UA-1 document - the claim is made only + * when the document earns it. Typst + * validates conformance at compile time (document title present, first + * heading level 1, consecutive heading levels, alt text on images and + * equations, ...); a conforming document gets the `pdfuaid` conformance + * claim, a nonconforming one is automatically re-exported as + * tagged-but-unclaimed - an honest output rather than a false claim - + * with the violations reported in the result's {@link PdfUAResult}. + * + * The tagged (accessible) structure tree is always emitted either way. + * Set `false` to skip the claim and its validation entirely (e.g. for a + * live preview, where the validation compile would be wasted work). + * + * Attempting the claim requires the document's language (the export + * options' `lang`) - the one conformance input only the caller can + * provide - and throws without it. + * + * @default true + */ + tryDeclarePdfUA?: boolean; +}; + +/** One PDF/UA-1 conformance violation, safe to surface in an app's UI. */ +export type PdfUAViolation = { + /** Typst's diagnostic message, e.g. "PDF/UA-1 error: missing alt text". */ + message: string; + /** Remediation hints accompanying the message. */ + hints: string[]; +}; + +/** Whether (and why not) the produced PDF declares PDF/UA-1 conformance. */ +export type PdfUAResult = + | { declared: true } + | { declared: false; reason: "tryDeclarePdfUA-disabled" } + | { + declared: false; + reason: "nonconforming"; + violations: PdfUAViolation[]; + }; + +/** + * The outcome of {@link PDFExporter.toPDF}. A document that fails to + * compile is an expected outcome (the source is generated from user + * content: text no supplied font covers, broken caller-supplied + * header/footer markup, ...), so it's a value carrying the compiler's + * diagnostics - not an exception. + */ +export type PdfExportResult = + | { + error?: undefined; + /** The PDF bytes. */ + bytes: Uint8Array; + /** + * The same PDF as a Blob (e.g. for downloads / object URLs). + * Created lazily on first access - constructing a Blob copies the + * bytes, so results that only use `bytes` never pay for it. + */ + readonly blob: Blob; + pdfUA: PdfUAResult; + compileWarnings: TypstDiagnostic[]; + } + | { + error: "compile-failed"; + compileErrors: TypstDiagnostic[]; + compileWarnings: TypstDiagnostic[]; + }; + +/** + * Exports BlockNote documents to accessible, tagged PDF files - PDF/UA-1 + * when the document conforms (see {@link PdfExportOptions.tryDeclarePdfUA}) - + * powered by Typst compiled to WebAssembly + * (`@blocknote/xl-typst-compiler`): the whole pipeline runs client-side, + * with no network access. + * + * The mappings are *Typst* mappings ({@link typstDefaultSchemaMappings}); + * this class is a {@link TypstExporter} plus the PDF compile step, so + * everything documented there applies - including that an exporter instance + * accumulates assets for its lifetime: create a fresh exporter per export + * when repeatedly exporting changing content (construction is cheap). + * + * Zero-config exports match the editor: the constructor's `fonts` and + * `emojiFont` each default (independently) to the bundled set - Inter, + * Geist Mono, New Computer Modern Math resp. Noto Color Emoji - loaded + * lazily from the package, and the compiler wasm loads from its own + * package's files. Pass a value (or an explicit `[]` for no fonts) to take + * over either; see {@link PdfExporterOptions}. + * + * (The previous react-pdf based exporter is still available from + * `@blocknote/xl-pdf-exporter/react-pdf` during its deprecation window.) + */ +export class PDFExporter< + B extends BlockSchema, + S extends StyleSchema, + I extends InlineContentSchema, +> extends TypstExporter { + private readonly wasm: TypstCompileOptions["wasm"]; + private readonly fonts: Promise; + private readonly emojiFont: Promise; + + public constructor( + schema: BlockNoteSchema, + mappings: Exporter< + NoInfer, + NoInfer, + NoInfer, + string, + string, + (inner: string) => string, + string + >["mappings"], + options?: Partial, + ) { + const { wasm, fonts, emojiFont, ...typstOptions } = options ?? {}; + super(schema, mappings, { + // The bundled default fonts (see `defaultFonts.ts`) include Noto + // Color Emoji; declaring the family by default lets multi-codepoint + // emoji shape correctly with zero config. Spread-defaulting is + // deliberate here (unlike the base's per-key defaults): an explicit + // `emojiFontFamily: undefined` *unsets* the family - a legitimate + // state when supplying custom fonts without an emoji face, where a + // dangling default reference would only produce warning noise. + emojiFontFamily: DEFAULT_EMOJI_FONT_FAMILY, + ...typstOptions, + }); + this.wasm = wasm; + // Zero-config fonts: each option defaults independently to the bundled + // set matching the editor - `undefined` means "use the default", a + // supplied value (including an explicit `[]` for none) takes over. The + // independence keeps the bytes consistent with the constructor's + // independent `emojiFontFamily` default: custom body fonts don't + // silently drop emoji support, and vice versa. Values may be promises + // (the constructor stays sync); they resolve on first export. + this.fonts = + fonts === undefined ? loadDefaultBodyFonts() : Promise.resolve(fonts); + this.emojiFont = + emojiFont === undefined + ? loadDefaultEmojiFont() + : Promise.resolve(emojiFont); + // A caller-supplied promise may reject before the first export awaits + // it; these no-op handlers keep that from surfacing as an unhandled + // rejection (the export still rejects with the real error). + this.fonts.catch(() => {}); + this.emojiFont.catch(() => {}); + } + + /** + * Export a document to PDF: BlockNote document -> Typst -> tagged PDF + * (wasm), declared PDF/UA-1 when the document conforms (see + * {@link PdfExportOptions.tryDeclarePdfUA}). The counterpart of the + * inherited {@link TypstExporter.toTypst}. + * + * The result carries the PDF as both bytes and a Blob, and reports + * whether the conformance claim was made (and the violations when it + * wasn't) - or, for a document that fails to compile at all, the + * compiler's diagnostics (see {@link PdfExportResult}). + */ + public async toPDF( + blocks: Block[], + options: PdfExportOptions = {}, + ): Promise { + const { + tryDeclarePdfUA = true, + assets, + creationTimestamp, + ...documentOptions + } = options; + // Attempting the claim without a document language is a caller-args + // error (like the asset collision below), not a document + // nonconformity: the PDF would otherwise carry Typst's silently + // defaulted language (English), and a wrong `/Lang` (e.g. "en" on a + // German document) is an accessibility defect no validator can catch. + // Unlike heading structure or alt text - content the end user can fix - + // the language is an integration-time decision only the caller can + // make, so it fails loudly (and first) at development time instead of + // quietly producing forever-unclaimed exports. + if (tryDeclarePdfUA && !options.lang) { + throw new Error( + "PDFExporter: declaring PDF/UA-1 (tryDeclarePdfUA, the default) " + + "requires the document's language - pass its BCP-47 tag in the " + + 'export options (e.g. { lang: "en" }), or opt out of the ' + + "claim with tryDeclarePdfUA: false.", + ); + } + const typst = await this.toTypst(blocks, documentOptions); + // The images collected during the export are mapped into the compiler + // alongside (not instead of) any assets the caller supplied - e.g. an + // image referenced from the header/footer markup. A caller asset under + // the exporter's own key space would be silently shadowed by the merge, + // so that's rejected loudly instead (a programmer error, not an + // expected outcome - hence a throw, unlike compile failures). + for (const key of assets?.keys() ?? []) { + if (this.assetFiles.has(key)) { + throw new Error( + `PDFExporter: the caller-supplied asset "${key}" collides with an ` + + `asset registered by the exporter - use a path outside /assets/`, + ); + } + } + const [fonts, emojiFont] = await Promise.all([this.fonts, this.emojiFont]); + const resolved: TypstCompileOptions = { + wasm: this.wasm, + fonts, + emojiFont, + creationTimestamp, + assets: new Map([...(assets ?? []), ...this.assetFiles]), + }; + + if (!tryDeclarePdfUA) { + return withPdfUA(await compileTypstToPdf(typst, resolved), { + declared: false, + reason: "tryDeclarePdfUA-disabled", + }); + } + + const declared = await compileTypstToPdf(typst, { + ...resolved, + pdfStandard: "ua-1", + }); + if (!declared.error) { + return exported( + declared.pdf, + { declared: true }, + declared.compileWarnings, + ); + } + // Only conformance violations fall back to an unclaimed export; any + // other failure (broken markup, missing font) would fail the fallback + // compile too, so it surfaces as the compile failure it is. (Violations + // are always *errors* - Typst emits every validator finding as an + // error, never a warning - so classifying compileErrors alone is + // complete. The declared attempt's compileWarnings are dropped here, + // but nothing is lost: the fallback compiles the same document with + // the same fonts, so the result below carries the identical warnings.) + if (!declared.compileErrors.every(isPdfStandardViolation)) { + return declared; + } + return withPdfUA(await compileTypstToPdf(typst, resolved), { + declared: false, + reason: "nonconforming", + violations: declared.compileErrors.map(({ message, hints }) => ({ + message, + hints, + })), + }); + } +} + +function withPdfUA( + compiled: Awaited>, + pdfUA: PdfUAResult, +): PdfExportResult { + if (compiled.error) { + return compiled; + } + return exported(compiled.pdf, pdfUA, compiled.compileWarnings); +} + +function exported( + bytes: Uint8Array, + pdfUA: PdfUAResult, + compileWarnings: TypstDiagnostic[], +): PdfExportResult { + let blob: Blob | undefined; + return { + bytes, + // Constructing a Blob copies the bytes, so it's deferred to first + // access (and memoized for a stable identity). + get blob() { + // The pipeline always returns a view over a plain (non-shared) + // buffer; the cast narrows `ArrayBufferLike` for `BlobPart`. + return (blob ??= new Blob([bytes as Uint8Array], { + type: "application/pdf", + })); + }, + pdfUA, + compileWarnings, + }; +} diff --git a/packages/xl-pdf-exporter/src/pdfua/__fixtures__/tagged.pdf b/packages/xl-pdf-exporter/src/pdfua/__fixtures__/tagged.pdf deleted file mode 100644 index 9a68ad240f..0000000000 Binary files a/packages/xl-pdf-exporter/src/pdfua/__fixtures__/tagged.pdf and /dev/null differ diff --git a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/.gitignore b/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/.gitignore deleted file mode 100644 index a458bdff29..0000000000 --- a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Debug output written when a page differs from its baseline. -*.actual.png diff --git a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-1.png b/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-1.png deleted file mode 100644 index 2b24b12393..0000000000 Binary files a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-1.png and /dev/null differ diff --git a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-2.png b/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-2.png deleted file mode 100644 index f0cf22fb10..0000000000 Binary files a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-2.png and /dev/null differ diff --git a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-3.png b/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-3.png deleted file mode 100644 index 9bd4124417..0000000000 Binary files a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-3.png and /dev/null differ diff --git a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-4.png b/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-4.png deleted file mode 100644 index 039bed2b84..0000000000 Binary files a/packages/xl-pdf-exporter/src/pdfua/__snapshots__/render/testDocument-4.png and /dev/null differ diff --git a/packages/xl-pdf-exporter/src/pdfua/compileBrowser.browser.test.ts b/packages/xl-pdf-exporter/src/pdfua/compileBrowser.browser.test.ts deleted file mode 100644 index fa2403dfad..0000000000 --- a/packages/xl-pdf-exporter/src/pdfua/compileBrowser.browser.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { isPdf, TEST_PNG_BYTES } from "@shared/util/testBytesUtil.js"; -import { describe, expect, test } from "vite-plus/test"; - -import { compileTypstToTaggedPdf } from "./compileBrowser.js"; - -// Bundled wasm so the compiler initializes fully offline (no CDN). -// eslint-disable-next-line import/no-unresolved -import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url"; - -// Browser unit tests for the actual production compile path - the wasm -// compiler the exporters ship to the browser. The node suites substitute -// @myriaddreamin/typst-ts-node-compiler, so a broken wasm wrapper (init, -// shadow-file handling, serialization) would pass every node test and only -// fail here. Runs in the tests package's browser suite. Font-less on -// purpose (`preloadDefaultFonts: false`, no font bytes): glyphs don't -// matter here, and it keeps the suite offline. -const OPTIONS = { - // A fresh `URL` per call - the documented bundler pattern - // (`() => new URL(..., import.meta.url)`). The compiler compares - // getModule results across compiles to detect configuration changes, so - // every later test in this suite doubles as a regression test that - // equivalent-but-fresh URL objects are compared by value, not identity - // (identity comparison would reject each compile after the first as - // "already initialized with different options"). - getModule: () => new URL(compilerWasmUrl, document.baseURI), - preloadDefaultFonts: false, -} as const; - -describe("compileTypstToTaggedPdf", () => { - test("compiles markup with mapped assets", { timeout: 60000 }, async () => { - const pdf = await compileTypstToTaggedPdf( - `#set document(title: "t")\nHello #image("/assets/a.png", width: 10pt)`, - { ...OPTIONS, assets: new Map([["/assets/a.png", TEST_PNG_BYTES]]) }, - ); - expect(isPdf(pdf)).toBe(true); - }); - - test( - "serializes concurrent compiles with different assets", - { timeout: 60000 }, - async () => { - // Shadow files are global compiler state; without serialization one - // compile's resetShadow lands mid-flight in the other and it fails - // with file-not-found (the debounced re-export scenario). - function compile(path: string) { - return compileTypstToTaggedPdf( - `#set document(title: "t")\n#image("${path}", width: 10pt)`, - { ...OPTIONS, assets: new Map([[path, TEST_PNG_BYTES]]) }, - ); - } - const [a, b] = await Promise.all([ - compile("/assets/one.png"), - compile("/assets/two.png"), - ]); - expect(isPdf(a)).toBe(true); - expect(isPdf(b)).toBe(true); - }, - ); - - test( - "rejects a later compile that changes the init-time options", - { timeout: 60000 }, - async () => { - // The wasm module and fonts load once, on the first compile; silently - // ignoring different options would leave e.g. new fonts unloaded. - await compileTypstToTaggedPdf("ok", OPTIONS); - await expect( - compileTypstToTaggedPdf("ok", { - ...OPTIONS, - fonts: [new Uint8Array([1, 2, 3])], - }), - ).rejects.toThrow("already initialized"); - }, - ); - - test( - "surfaces compile errors instead of returning output", - { timeout: 60000 }, - async () => { - await expect( - compileTypstToTaggedPdf("#image(unknownvariable)", OPTIONS), - ).rejects.toThrow(); - }, - ); -}); diff --git a/packages/xl-pdf-exporter/src/pdfua/compileBrowser.ts b/packages/xl-pdf-exporter/src/pdfua/compileBrowser.ts deleted file mode 100644 index 2b4bb5c3df..0000000000 --- a/packages/xl-pdf-exporter/src/pdfua/compileBrowser.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { TypstSnippet } from "@myriaddreamin/typst.ts/contrib/snippet"; -import type { InitOptions } from "@myriaddreamin/typst.ts/options.init"; - -export interface TypstCompileOptions { - /** - * Returns the wasm module for the Typst compiler. In a browser bundler this - * is typically the imported `.wasm` URL/bytes from - * `@myriaddreamin/typst-ts-web-compiler`, e.g. - * `getModule: () => new URL("...", import.meta.url)`. - * - * If omitted, Typst.ts loads its matching compiler wasm from its CDN — handy - * for demos, but bundle/self-host it for production. - */ - getModule?: InitOptions["getModule"]; - /** - * Extra fonts (as bytes) to load into the compiler — typically the body fonts - * the exporter references (e.g. Inter, Geist Mono). - */ - fonts?: Uint8Array[]; - /** - * The emoji source: an emoji-capable font (or fonts) as bytes. Unlike - * react-pdf's image-based `emojiSource`, Typst renders emoji from a font, so - * this is how you supply one. Required for *fully* PDF/UA-1 conformant output - * that contains emoji — the browser has no OS font access, so without it emoji - * render as `.notdef` and fail ISO 14289-1 clauses 7.21.7 / 7.21.8. Loaded - * alongside {@link fonts} (this is just a clearer, dedicated channel for it). - */ - emojiFont?: Uint8Array | Uint8Array[]; - /** - * Additionally fetch Typst's stock font assets (Libertinus Serif, DejaVu - * Sans Mono, etc.) from the jsdelivr CDN, as fallback faces for glyphs - * the supplied fonts don't cover. Off by default: `PDFExporter` already - * loads a bundled default font set (body, code, math, emoji), so - * zero-config exports make no font CDN requests at all. - * @default false - */ - preloadDefaultFonts?: boolean; - /** - * Image/asset files to map into the compiler's virtual filesystem, keyed by - * the Typst path referenced in the source (e.g. `/assets/asset-0`). - * Populate from `TypstExporter.assetFiles`. - */ - assets?: ReadonlyMap; -} - -function fontList(options: TypstCompileOptions): Uint8Array[] { - const emojiFonts = - options.emojiFont === undefined - ? [] - : Array.isArray(options.emojiFont) - ? options.emojiFont - : [options.emojiFont]; - return [...(options.fonts ?? []), ...emojiFonts]; -} - -// A cheap content fingerprint for font byte arrays, so byte-identical fonts -// pass the compatibility check even as fresh Uint8Array instances (a caller -// refetching the same files per export, test retries, HMR) - reference -// identity would spuriously reject them. Length plus the first bytes is -// discriminating enough: a font file's table directory (offsets, checksums) -// sits at the start, so different fonts diverge immediately. -function fontFingerprint(bytes: Uint8Array): string { - let hash = 0; - const end = Math.min(bytes.length, 256); - for (let i = 0; i < end; i++) { - hash = (hash * 31 + bytes[i]) | 0; - } - return `${bytes.length}:${hash}`; -} - -// Canonicalizes a getModule result for the compatibility check below. -// Strings compare by value on their own, but the equally-documented -// `getModule: () => new URL(..., import.meta.url)` pattern returns a fresh -// `URL` instance per call - comparing those by identity would spuriously -// reject every compile after the first. URLs therefore compare by href; -// other module forms (bytes, WebAssembly.Module, Response) keep reference -// identity, as callers hold onto those. -function moduleSourceKey(source: unknown): unknown { - return source instanceof URL ? source.href : source; -} - -// The wasm compiler is a page-level singleton (typst.ts's own `$typst` is -// module-global), so its init-time inputs — wasm module, font set, default -// font preloading — are fixed by the first compile. A later compile that -// tries to CHANGE them throws a descriptive error instead of silently -// compiling with the first call's configuration: pass every font the page -// will need on the first call. (These constraints — and that a failed wasm -// load can only be recovered by a page reload — are inherent to the -// `$typst` snippet singleton; lifting them means moving to typst.ts's -// per-instance compiler API.) -let configured: - | { - snippet: TypstSnippet; - fonts: Set; - moduleSource: unknown; - preloadDefaultFonts: boolean; - } - | undefined; - -// Compiles are serialized: shadow files (the per-document image assets) are -// state on the shared compiler, so overlapping compiles would reset or read -// each other's assets mid-flight. The chain also covers initialization, so -// two concurrent first compiles cannot double-initialize. A failed compile -// must not break the chain for the next caller, hence the swallow-and-await -// structure below (the failure still rejects that caller's own promise). -let queue: Promise = Promise.resolve(); - -async function getSnippet(options: TypstCompileOptions): Promise { - if (configured) { - const newFonts = fontList(options).filter( - (f) => !configured!.fonts.has(fontFingerprint(f)), - ); - // getModule is compared by its *resolved* source (the URL/module it - // returns, canonicalized via `moduleSourceKey`), not by function - // identity - callers typically pass a fresh closure per call around the - // same URL. Omitting it after a first call that had one means "reuse - // what's loaded" and is fine. - const moduleChanged = - options.getModule !== undefined && - moduleSourceKey(options.getModule()) !== configured.moduleSource; - if ( - newFonts.length > 0 || - moduleChanged || - (options.preloadDefaultFonts === true) !== configured.preloadDefaultFonts - ) { - throw new Error( - "The Typst compiler is already initialized with different options. " + - "Its wasm module and fonts are loaded once, on the first compile - " + - "pass every font (and the getModule/preloadDefaultFonts settings) " + - "the page will need on the first compileTypstToTaggedPdf call.", - ); - } - return configured.snippet; - } - - const { $typst, TypstSnippet } = - await import("@myriaddreamin/typst.ts/contrib/snippet"); - if (options.getModule) { - $typst.setCompilerInitOptions({ getModule: options.getModule }); - } - const providers = []; - if (options.preloadDefaultFonts === true) { - providers.push(TypstSnippet.preloadFontAssets()); - } else { - // The explicit opt-out marker matters: without a provider whose options - // say `assets: false`, the compiler driver force-loads its stock 'text' - // font assets (Libertinus, New CM Math, ...) from the jsdelivr CDN even - // when no preload provider was given - which would silently reintroduce - // a CDN dependency behind this off-by-default option. - providers.push(TypstSnippet.disableDefaultFontAssets()); - } - const fonts = fontList(options); - for (const font of fonts) { - providers.push(TypstSnippet.preloadFontData(font)); - } - if (providers.length) { - $typst.use(...providers); - } - configured = { - snippet: $typst, - fonts: new Set(fonts.map(fontFingerprint)), - moduleSource: moduleSourceKey(options.getModule?.()), - preloadDefaultFonts: options.preloadDefaultFonts === true, - }; - return configured.snippet; -} - -/** - * Compile Typst source to a *tagged* PDF using the browser (wasm) engine. - * The compiler is initialized once and reused; concurrent calls are - * serialized (see above). - */ -export async function compileTypstToTaggedPdf( - typst: string, - options: TypstCompileOptions, -): Promise { - const run = queue.then(async () => { - const $typst = await getSnippet(options); - // Shadow files are per-compile; reset so a previous document's assets - // don't leak into this one. - await $typst.resetShadow(); - for (const [path, bytes] of options.assets ?? []) { - await $typst.mapShadow(path, bytes); - } - const pdf = await $typst.pdf({ mainContent: typst }); - if (!pdf) { - throw new Error("Typst wasm compilation produced no output"); - } - return pdf; - }); - // Keep the chain alive whether this compile succeeds or fails (the failure - // still propagates through `run` to this caller) - and resolve it to - // undefined either way, so the module-global chain doesn't pin the last - // compile's multi-MB PDF bytes in memory for the page's lifetime. - queue = run.then( - () => undefined, - () => undefined, - ); - return run; -} diff --git a/packages/xl-pdf-exporter/src/pdfua/compileTypst.browser.test.ts b/packages/xl-pdf-exporter/src/pdfua/compileTypst.browser.test.ts new file mode 100644 index 0000000000..b95072ba26 --- /dev/null +++ b/packages/xl-pdf-exporter/src/pdfua/compileTypst.browser.test.ts @@ -0,0 +1,112 @@ +import { isPdf, TEST_PNG_BYTES } from "@shared/util/testBytesUtil.js"; +import { describe, expect, test } from "vite-plus/test"; + +import { compileTypstToPdf } from "./compileTypst.js"; + +// Bundled wasm so the compiler initializes fully offline. +// eslint-disable-next-line import/no-unresolved +import compilerWasmUrl from "@blocknote/xl-typst-compiler/wasm?url"; + +// Browser unit tests for the actual production compile path - the wasm +// compiler the exporters ship to the browser, loaded through the bundler +// like an app would. The node suites drive the same wasm from bytes, so +// what only this suite can catch is URL-based module loading and +// browser-side glue. Font-less on purpose: glyphs don't matter here, and +// it keeps the suite fast. +const OPTIONS = { + wasm: new URL(compilerWasmUrl, document.baseURI), +} as const; + +function expectPdf( + result: Awaited>, +): Uint8Array { + if (result.error) { + throw new Error(result.compileErrors.map((d) => d.message).join(" | ")); + } + return result.pdf; +} + +describe("compileTypstToPdf", () => { + test("compiles markup with mapped assets", { timeout: 60000 }, async () => { + const pdf = expectPdf( + await compileTypstToPdf( + `#set document(title: "t")\nHello #image("/assets/a.png", width: 10pt, alt: "a")`, + { ...OPTIONS, assets: new Map([["/assets/a.png", TEST_PNG_BYTES]]) }, + ), + ); + expect(isPdf(pdf)).toBe(true); + }); + + test( + "keeps concurrent compiles with different assets independent", + { timeout: 60000 }, + async () => { + // Assets are per-compile inputs (not shared compiler state), so + // overlapping exports - the debounced re-export scenario - cannot + // read each other's files. + async function compile(path: string) { + return expectPdf( + await compileTypstToPdf( + `#set document(title: "t")\n#image("${path}", width: 10pt, alt: "a")`, + { ...OPTIONS, assets: new Map([[path, TEST_PNG_BYTES]]) }, + ), + ); + } + const [a, b] = await Promise.all([ + compile("/assets/one.png"), + compile("/assets/two.png"), + ]); + expect(isPdf(a)).toBe(true); + expect(isPdf(b)).toBe(true); + }, + ); + + test( + "accepts different fonts on later compiles", + { timeout: 60000 }, + async () => { + // A per-compile font set was impossible under the previous + // page-singleton engine (fonts were fixed by the first compile); + // compilers are now per-font-set instances, so this must just work. + // Invalid font bytes are ignored (0 faces), not fatal. + const first = expectPdf(await compileTypstToPdf("one", OPTIONS)); + const second = expectPdf( + await compileTypstToPdf("two", { + ...OPTIONS, + fonts: [new Uint8Array([1, 2, 3])], + }), + ); + expect(isPdf(first)).toBe(true); + expect(isPdf(second)).toBe(true); + }, + ); + + test( + "returns typed diagnostics instead of output for broken markup", + { timeout: 60000 }, + async () => { + const result = await compileTypstToPdf( + "#image(unknownvariable)", + OPTIONS, + ); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect(result.compileErrors[0].message).toContain("unknown variable"); + } + }, + ); + + test( + "produces declared PDF/UA-1 with the ua-1 standard", + { timeout: 60000 }, + async () => { + const pdf = expectPdf( + await compileTypstToPdf(`#set document(title: "t")\nHello`, { + ...OPTIONS, + pdfStandard: "ua-1", + }), + ); + expect(new TextDecoder("latin1").decode(pdf)).toContain("pdfuaid"); + }, + ); +}); diff --git a/packages/xl-pdf-exporter/src/pdfua/compileTypst.ts b/packages/xl-pdf-exporter/src/pdfua/compileTypst.ts new file mode 100644 index 0000000000..cf982c60d4 --- /dev/null +++ b/packages/xl-pdf-exporter/src/pdfua/compileTypst.ts @@ -0,0 +1,96 @@ +import { + TypstCompiler, + type CompilePdfResult, + type TypstCompilerOptions, +} from "@blocknote/xl-typst-compiler"; + +export interface TypstCompileOptions { + /** + * The compiler wasm module: a URL (string or `URL`) or the module bytes. + * Omitted, it loads from `@blocknote/xl-typst-compiler`'s own package + * files - bundlers emit it as an asset, so zero-config exports involve no + * CDN. The module is loaded once per page; only the first compile's value + * is used. + */ + wasm?: TypstCompilerOptions["wasm"]; + /** + * Fonts (as bytes) to load into the compiler - the body fonts the + * source references (e.g. Inter, Geist Mono). This low-level function + * has no defaults (`PDFExporter` adds those); the compiler ships no + * fonts of its own: text renders only with supplied fonts, and a + * document needing a missing one fails to compile (loudly, not with + * substituted glyphs). + * + * Fonts are selected by the family name embedded in each file's name + * table (array order and file names play no role) - the exporter's + * `fontFamily` options must match it exactly. A mismatch surfaces as an + * `unknown font family` entry in the result's `compileWarnings`; + * `TypstCompiler.fontFamilies` (from `@blocknote/xl-typst-compiler`) + * lists what a font file actually declares. + */ + fonts?: Uint8Array[]; + /** + * The emoji source: an emoji-capable font (or fonts) as bytes. Unlike + * react-pdf's image-based `emojiSource`, Typst renders emoji from a font, + * so this is how you supply one. Required for PDF/UA-1 conformant output + * that contains emoji - the browser has no OS font access, so without it + * emoji render as `.notdef` and fail ISO 14289-1 clauses 7.21.7 / 7.21.8. + * Loaded alongside {@link fonts} (this is just a clearer, dedicated + * channel for it). + */ + emojiFont?: Uint8Array | Uint8Array[]; + /** + * Image/asset files to map into the compiler's virtual filesystem, keyed + * by the Typst path referenced in the source (e.g. `/assets/asset-0`). + * Populate from `TypstExporter.assetFiles`. + */ + assets?: ReadonlyMap; + /** + * PDF creation timestamp in seconds since the Unix epoch (UTC). Pass a + * fixed value for byte-reproducible output; omitted means no timestamp. + */ + creationTimestamp?: number; +} + +function fontList(options: TypstCompileOptions): Uint8Array[] { + const emojiFonts = + options.emojiFont === undefined + ? [] + : Array.isArray(options.emojiFont) + ? options.emojiFont + : [options.emojiFont]; + return [...(options.fonts ?? []), ...emojiFonts]; +} + +/** + * Compile Typst source to PDF with the wasm engine + * (`@blocknote/xl-typst-compiler`). Compilation failure - invalid markup, + * a missing font, or PDF-standard validation - is part of the contract and + * comes back as a typed result, not an exception. + * + * Pass `pdfStandard: "ua-1"` for a *validated*, declared PDF/UA-1; without + * it the output is tagged (accessible structure tree) but carries no + * conformance claim. + */ +export async function compileTypstToPdf( + typst: string, + options: TypstCompileOptions & { + pdfStandard?: string; + tagged?: boolean; + } = {}, +): Promise { + // A fresh instance per compile: the wasm module is loaded once globally + // and Typst's caches are module-wide, so instance creation (including + // parsing the fonts) costs single-digit milliseconds - not worth any + // reuse scheme, and it keeps compiles fully independent. + const compiler = await TypstCompiler.create({ + wasm: options.wasm, + fonts: fontList(options), + }); + return compiler.compilePdf(typst, { + assets: options.assets, + pdfStandard: options.pdfStandard, + tagged: options.tagged, + creationTimestamp: options.creationTimestamp, + }); +} diff --git a/packages/xl-pdf-exporter/src/pdfua/defaultFonts.ts b/packages/xl-pdf-exporter/src/pdfua/defaultFonts.ts index b93d01eeb4..e0c1db1a9b 100644 --- a/packages/xl-pdf-exporter/src/pdfua/defaultFonts.ts +++ b/packages/xl-pdf-exporter/src/pdfua/defaultFonts.ts @@ -35,13 +35,21 @@ function cached(load: () => Promise): () => Promise { /** * The default body font set for zero-config exports, matching the editor: + * these files must declare (in their name tables) exactly the family names + * the exporters reference by default - TypstExporter's fontFamily / + * monoFontFamily and {@link DEFAULT_EMOJI_FONT_FAMILY}. The pairing is + * pinned by pdfExporter.test.ts's "keeps the bundled default fonts in + * sync" test: swapping a file (or renaming a default) fails it. + * * Inter (body, 4 faces), Geist Mono (code), and New Computer Modern Math * (Typst's math family - the compiler wasm embeds no fonts, so without it * a document with math blocks fails to compile). Embedded in the package * as lazily-imported chunks (the same mechanism the react-pdf exporter * uses), so nothing loads until a zero-config export runs, and exports * work offline. Used when the compile options' `fonts` is undefined - pass - * your own (or an explicit `[]` for none) to skip it. + * your own (or an explicit `[]` for none) to skip it, or spread this + * loader's result to *extend* it (e.g. `[...(await loadDefaultBodyFonts()), + * myCjkFont]`). */ export const loadDefaultBodyFonts: () => Promise = cached(() => Promise.all([ diff --git a/packages/xl-pdf-exporter/src/pdfua/pdfua.test.ts b/packages/xl-pdf-exporter/src/pdfua/pdfua.test.ts index 31dc70f432..053dcd8440 100644 --- a/packages/xl-pdf-exporter/src/pdfua/pdfua.test.ts +++ b/packages/xl-pdf-exporter/src/pdfua/pdfua.test.ts @@ -6,24 +6,20 @@ import { import { PDFDict, PDFDocument, PDFName } from "@cantoo/pdf-lib"; import { ColumnBlock, ColumnListBlock } from "@blocknote/xl-multi-column"; import { testDocument } from "@shared/testDocument.js"; -import { toMatchBinaryFileSnapshot } from "@shared/util/binaryFileSnapshotUtil.js"; import { testResolveFileUrl } from "@shared/util/testFileResolver.js"; import { compileTypstForTesting } from "@shared/util/typstTestUtil.js"; -import { execFileSync } from "node:child_process"; -import { mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { beforeAll, describe, expect, it } from "vite-plus/test"; import { TypstExporter, typstDefaultSchemaMappings, } from "@blocknote/xl-typst-exporter"; -import { declarePdfUA } from "./postProcess.js"; // The fonts the exporter references (Inter 18pt + Geist Mono), from the shared // assets, plus a color emoji font (Noto Color Emoji, pure-COLRv1) so emoji // render in color rather than as `.notdef` — matching the example. -function fontBlobs(): Buffer[] { +function fontBlobs(): Uint8Array[] { const shared = "../../shared/assets/fonts"; const paths = [ `${shared}/inter/Inter_18pt-Regular.ttf`, @@ -33,91 +29,26 @@ function fontBlobs(): Buffer[] { `${shared}/GeistMono-Regular.ttf`, `${shared}/noto/Noto-COLRv1.ttf`, ]; - return paths.map((p) => Buffer.from(readFileSync(resolve(process.cwd(), p)))); -} - -// These tests are gates, and a gate whose tooling is missing must fail -// loudly - a silent pass would report conformance/visual coverage that was -// never checked. -function requireTool(command: string, args: string[], installHint: string) { - try { - execFileSync(command, args, { stdio: "ignore" }); - } catch { - throw new Error( - `\`${command} ${args.join(" ")}\` failed - it is required by this test. ${installHint}`, - ); - } -} - -// The rasterizer for the visual snapshots, as a digest-pinned container -// (mirroring the veraPDF image in tests/scripts/install-pdf-tooling.sh): -// the PNG baselines are compared byte-exactly, so every environment - a -// macOS laptop, CI, next year's CI - must run the *identical* poppler -// build. `--platform linux/amd64` below keeps even the binary the same on -// Apple Silicon (via Rosetta). Repoint deliberately, regenerating the -// baselines (`vp test --run src/pdfua/pdfua.test.ts -u`) in the same -// change. install-pdf-tooling.sh greps this constant to pre-pull the image -// on CI. -const POPPLER_IMAGE = - "minidocks/poppler@sha256:0817047a6d6078f1af9931860646e8c21234c0c1777d2f05b79df8434cb194de"; // :latest as of 2026-08-20 - -/** Rasterizes each page to `page-N.png` in `dir`, returning the file names. */ -function rasterizePdf(pdf: Buffer, dir: string): string[] { - writeFileSync(join(dir, "doc.pdf"), pdf); - // The host user's uid/gid, so the container-written PNGs are readable and - // cleanable by the test process on Linux bind mounts. - const user = process.getuid - ? ["--user", `${process.getuid()}:${process.getgid!()}`] - : []; - execFileSync( - "docker", - [ - "run", - "--rm", - "--platform", - "linux/amd64", - ...user, - "-v", - `${dir}:/data`, - POPPLER_IMAGE, - "pdftoppm", - "-png", - "-r", - "96", - "/data/doc.pdf", - "/data/page", - ], - { stdio: "ignore" }, + return paths.map( + (p) => new Uint8Array(readFileSync(resolve(process.cwd(), p))), ); - return readdirSync(dir) - .filter((f) => f.endsWith(".png")) - .sort(); } -function veraPdfVerdict(pdf: Uint8Array): string { - requireTool( - "verapdf", - ["--version"], - "Install veraPDF (https://verapdf.org) - it is the PDF/UA conformance gate.", - ); - const dir = mkdtempSync(join(tmpdir(), "pdfua-")); - const file = join(dir, "out.pdf"); - writeFileSync(file, pdf); - try { - return execFileSync("verapdf", ["--flavour", "ua1", file], { - encoding: "utf8", - maxBuffer: 1e8, - }); - } catch (e: any) { - return e.stdout?.toString() ?? ""; // non-zero exit on non-conformance - } -} - -// Compiles the shared test document once (with the network-free test image -// resolver), then asserts both PDF/UA-1 conformance and a per-page visual -// snapshot off the same render. -describe("pdf/ua-1: BlockNote -> Typst -> PDF (conformance + visual)", () => { - let tagged: Buffer; +// Compiles the shared test document (with the network-free test image +// resolver) natively as PDF/UA-1 and asserts the structural essentials. +// +// Conformance itself is validated *during* the compile: requesting the +// `ua-1` standard makes Typst/krilla check the document (heading structure, +// alt text, title, ...) and refuse to emit a nonconforming PDF — so a +// successful compile IS the conformance statement, guaranteed by the +// engine rather than re-verified here. (Independent verification with +// veraPDF — `verapdf --flavour ua1` — remains a manual option; the +// declared output has been validated against it: 0 failed checks.) +// +// Visual regression of the rendered pages lives in the browser e2e suite +// (tests/src/end-to-end/exporters/exporterImages.test.tsx), which +// rasterizes the produced PDF with pdf.js and screenshots each page. +describe("pdf/ua-1: BlockNote -> Typst -> PDF", () => { let ua: Uint8Array; beforeAll(async () => { @@ -145,56 +76,29 @@ describe("pdf/ua-1: BlockNote -> Typst -> PDF (conformance + visual)", () => { author: "BlockNote", }); - // Compile to a *tagged* PDF (no UA flag), mirroring the browser path. - tagged = Buffer.from( - await compileTypstForTesting(typ, { - assets: exporter.assetFiles, - fontBlobs: fontBlobs(), - creationTimestamp: 1_700_000_000, - }), - ); - // Declare PDF/UA-1 via our post-process. - ua = await declarePdfUA(new Uint8Array(tagged)); + ua = await compileTypstForTesting(typ, { + assets: exporter.assetFiles, + fontBlobs: fontBlobs(), + creationTimestamp: 1_700_000_000, + pdfStandard: "ua-1", + }); }, 30000); - it("produces a declared, structurally-tagged, veraPDF-conformant document", async () => { - // Deterministic structural assertions (no external tooling). + it("produces a declared, structurally-tagged document", async () => { + // The conformance claim in the XMP metadata. + expect(new TextDecoder("latin1").decode(ua)).toContain("pdfuaid"); + + // Structure that lives in compressed objects, via a real PDF parser: + // the tag tree root and the DisplayDocTitle viewer preference PDF/UA + // requires. const doc = await PDFDocument.load(ua, { updateMetadata: false }); expect(doc.catalog.get(PDFName.of("StructTreeRoot"))).toBeDefined(); const vp = doc.catalog.lookup(PDFName.of("ViewerPreferences"), PDFDict); expect(String(vp!.get(PDFName.of("DisplayDocTitle")))).toBe("true"); - - // Full conformance gate. - const verdict = veraPdfVerdict(ua); - expect(verdict).toContain('isCompliant="true"'); - expect(verdict).toContain('failedChecks="0"'); }); - it("matches the per-page visual snapshot", async () => { - requireTool( - "docker", - ["info"], - "Start Docker - the visual snapshot rasterizes through a digest-pinned poppler container so the PNG baselines are byte-stable across environments.", - ); - const dir = mkdtempSync(join(tmpdir(), "bn-visual-")); - // Rasterize the *final* PDF/UA artifact (post `declarePdfUA`), not the - // intermediate tagged PDF - the post-process is metadata-only, so the - // raster is identical, and this way a post-process regression that did - // alter page content would fail the visual snapshot too. - const pages = rasterizePdf(Buffer.from(ua), dir); - expect(pages.length).toBeGreaterThan(0); - - // Compare each page against its committed baseline PNG; on mismatch the - // received render is dumped next to the baseline for inspection. - for (let i = 0; i < pages.length; i++) { - await toMatchBinaryFileSnapshot( - readFileSync(join(dir, pages[i])), - resolve( - process.cwd(), - `src/pdfua/__snapshots__/render/testDocument-${i + 1}.png`, - ), - { writeActualOnMismatch: true }, - ); - } + it("carries the fixed creation timestamp (deterministic output)", () => { + // Byte-reproducible output is what the e2e visual baselines rely on. + expect(new TextDecoder("latin1").decode(ua)).toContain("2023-11-14"); }); }); diff --git a/packages/xl-pdf-exporter/src/pdfua/postProcess.test.ts b/packages/xl-pdf-exporter/src/pdfua/postProcess.test.ts deleted file mode 100644 index c90c5229fd..0000000000 --- a/packages/xl-pdf-exporter/src/pdfua/postProcess.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { PDFDict, PDFDocument, PDFName, PDFRawStream } from "@cantoo/pdf-lib"; -import { describe, expect, it } from "vite-plus/test"; -import { declarePdfUA } from "./postProcess.js"; - -// `tagged.pdf` is a Typst-generated, tagged-but-NOT-declared PDF/UA document -// (the same kind the wasm browser engine emits). -function loadFixture(): Uint8Array { - const candidates = [ - resolve(process.cwd(), "src/pdfua/__fixtures__/tagged.pdf"), - resolve(process.cwd(), "__fixtures__/tagged.pdf"), - ]; - const path = candidates.find((p) => existsSync(p)); - if (!path) { - throw new Error("tagged.pdf fixture not found; cwd=" + process.cwd()); - } - return new Uint8Array(readFileSync(path)); -} - -// A minimal one-page PDF whose /Metadata stream holds the given XMP packet, -// for exercising declarePdfUA against XMP shapes the Typst fixture cannot -// produce (e.g. a pre-existing pdfuaid:part claim). -async function pdfWithXmp(xmp: string): Promise { - const doc = await PDFDocument.create(); - doc.addPage(); - const bytes = new TextEncoder().encode(xmp); - doc.catalog.set( - PDFName.of("Metadata"), - doc.context.register( - PDFRawStream.of( - doc.context.obj({ - Type: "Metadata", - Subtype: "XML", - Length: bytes.length, - }), - bytes, - ), - ), - ); - return doc.save({ useObjectStreams: false }); -} - -function xmpWithDescription(content: string): string { - return `${content}`; -} - -describe("declarePdfUA", () => { - it("adds DisplayDocTitle + pdfuaid:part=1 to a tagged PDF", async () => { - const out = await declarePdfUA(loadFixture()); - - const doc = await PDFDocument.load(out, { updateMetadata: false }); - - // 1) ViewerPreferences / DisplayDocTitle = true - const vp = doc.catalog.lookup(PDFName.of("ViewerPreferences"), PDFDict); - expect(vp).toBeDefined(); - expect(String(vp!.get(PDFName.of("DisplayDocTitle")))).toBe("true"); - - // 2) PDF/UA identification in XMP - const meta = doc.context.lookup( - doc.catalog.get(PDFName.of("Metadata")), - ) as PDFRawStream; - const xmp = new TextDecoder().decode(meta.getContents()); - expect(xmp).toContain("pdfuaid"); - expect(xmp).toMatch(/1<\/pdfuaid:part>/); - - // structure is preserved (still a tagged PDF) - expect(doc.catalog.get(PDFName.of("StructTreeRoot"))).toBeDefined(); - }); - - it("is idempotent", async () => { - const once = await declarePdfUA(loadFixture()); - const twice = await declarePdfUA(once); - const doc = await PDFDocument.load(twice, { updateMetadata: false }); - const meta = doc.context.lookup( - doc.catalog.get(PDFName.of("Metadata")), - ) as PDFRawStream; - const xmp = new TextDecoder().decode(meta.getContents()); - // not doubled - expect(xmp.match(//g)?.length).toBe(1); - }); - - it("keeps an existing PDF/UA-1 claim without doubling it", async () => { - const out = await declarePdfUA( - await pdfWithXmp(xmpWithDescription("1")), - ); - const doc = await PDFDocument.load(out, { updateMetadata: false }); - const meta = doc.context.lookup( - doc.catalog.get(PDFName.of("Metadata")), - ) as PDFRawStream; - const xmp = new TextDecoder().decode(meta.getContents()); - expect(xmp.match(//g)?.length).toBe(1); - }); - - it("rejects an existing claim of a different PDF/UA part", async () => { - await expect( - declarePdfUA( - await pdfWithXmp(xmpWithDescription("2")), - ), - ).rejects.toThrow("pdfuaid:part"); - }); - - it("rejects an existing empty pdfuaid:part element", async () => { - await expect( - declarePdfUA( - await pdfWithXmp(xmpWithDescription("")), - ), - ).rejects.toThrow("pdfuaid:part"); - }); -}); diff --git a/packages/xl-pdf-exporter/src/pdfua/postProcess.ts b/packages/xl-pdf-exporter/src/pdfua/postProcess.ts deleted file mode 100644 index 45d8548590..0000000000 --- a/packages/xl-pdf-exporter/src/pdfua/postProcess.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { - decodePDFRawStream, - PDFDocument, - PDFName, - PDFRawStream, -} from "@cantoo/pdf-lib"; - -// The XMP namespace of the PDF/UA identification schema (ISO 14289-1, -// clause 5). Its one required property, `pdfuaid:part`, holds the part -// number of the ISO standard the file claims conformance with - `1` for -// PDF/UA-1. This is the analogue of PDF/A's `pdfaid:part`. -const PDFUA_NS = "http://www.aiim.org/pdfua/ns/id/"; - -/** - * Promote a *tagged* PDF (as produced by the Typst engine) to a declared - * PDF/UA-1 document. - * - * Typst 0.15 already emits the hard part - a UA-conformant tag tree - by - * default. What its `--pdf-standard ua-1` export flag *additionally* writes, - * and what the published wasm binding does not expose, are two declarations: - * - * 1. `/ViewerPreferences << /DisplayDocTitle true >>` in the catalog: - * viewers must show the document title, not the filename - * (ISO 14289-1, 7.1). - * 2. The conformance claim in the document's XMP metadata - the XML - * packet in the catalog's `/Metadata` stream where title/producer - * etc. live (clause 5): - * - * ```xml - * - * 1 - * - * ``` - * - * Validators check the claim first: veraPDF's `ua1` flavour fails a file - * without the identifier no matter how well-tagged it is. Both declarations - * are added here in pure JS (@cantoo/pdf-lib runs in the browser), so the - * whole pipeline stays client-side. - * - * A document whose XMP cannot be safely rewritten makes this function throw - * rather than return an undeclared (or corrupted) PDF as success. - * - * NOTE: this *declares* conformance; it does not create it. Garbage in (an - * untagged PDF, a figure missing alt text, ...) stays non-conformant - and - * now falsely claims otherwise - which is why veraPDF is the gate, not this - * function. To produce a tagged-but-unclaimed PDF instead, compile with - * `compileTypstToTaggedPdf` and skip this step. - */ -export async function declarePdfUA(pdfBytes: Uint8Array): Promise { - const doc = await PDFDocument.load(pdfBytes, { updateMetadata: false }); - - // Declaration 1: DisplayDocTitle. - doc.catalog.getOrCreateViewerPreferences().setDisplayDocTitle(true); - - // Declaration 2: `pdfuaid:part` in the XMP packet. The existing packet is - // edited rather than replaced, preserving the metadata Typst wrote - // (title, producer, ...), then written back as a fresh /Metadata stream. - const xmp = withPdfUaIdentifier(readMetadataXmp(doc)); - const xmpBytes = new TextEncoder().encode(xmp); - doc.catalog.set( - PDFName.of("Metadata"), - doc.context.register( - PDFRawStream.of( - doc.context.obj({ - Type: "Metadata", - Subtype: "XML", - Length: xmpBytes.length, - }), - xmpBytes, - ), - ), - ); - - return doc.save({ useObjectStreams: false }); -} - -/** - * The document's XMP packet as a string - or, for a document without one, a - * minimal empty packet (one blank `rdf:Description`) to inject into. - */ -function readMetadataXmp(doc: PDFDocument): string { - const ref = doc.catalog.get(PDFName.of("Metadata")); - if (!ref) { - return ``; - } - const stream = doc.context.lookup(ref); - if (!(stream instanceof PDFRawStream)) { - throw new Error( - "declarePdfUA: the document's /Metadata is not a stream object", - ); - } - // Run the stream through its declared filters. XMP is usually stored - // uncompressed, but a Flate-compressed packet is legal - decoding raw - // contents as text would silently corrupt it. - const bytes = - stream.dict.get(PDFName.of("Filter")) === undefined - ? stream.getContents() - : decodePDFRawStream(stream).decode(); - return new TextDecoder().decode(bytes); -} - -/** - * Splices `1` (plus its namespace declaration) - * into the packet's first `rdf:Description` element: - * - * before: ... - * after: - * 1... - * - * Regex-based on purpose: a DOM round-trip isn't available in node and - * re-serializing the whole packet risks perturbing metadata this code - * doesn't understand. A packet without any description throws (as does one - * already claiming a pdfuaid:part other than 1); the - * long-term replacement is the compiler's own `--pdf-standard ua-1` flag - * once the wasm binding exposes it (see the TODO in index.ts). - */ -function withPdfUaIdentifier(xmp: string): string { - // Already identified as PDF/UA-1 (element or attribute form) - nothing to - // do. Matched as the actual element/attribute form, not as a bare - // substring (document text merely *mentioning* "pdfuaid:part" must not - // skip the declaration). - if ( - /\s*1\s*<\/pdfuaid:part>|pdfuaid:part\s*=\s*["']\s*1\s*["']/.test( - xmp, - ) - ) { - return xmp; - } - - // Any other existing identifier - a different part, or an empty or - // otherwise unreadable value - is a conflicting claim. Keeping it would - // return a document that does not declare PDF/UA-1 as success, and - // rewriting a foreign value in place is exactly the kind of splice the - // regex approach must not attempt (see above) - so throw, per this - // module's contract. - if (/]|pdfuaid:part\s*=/.test(xmp)) { - throw new Error( - "declarePdfUA: the document's XMP metadata already contains a " + - "pdfuaid:part identifier that does not declare PDF/UA-1", - ); - } - - // Find the first (outermost) description's open tag. Group 1 = its - // attributes, group 2 = "/" when the element is self-closing - // (attribute-only XMP, common from non-Typst producers). - const openTag = /]*?)(\/?)>/.exec(xmp); - if (!openTag) { - throw new Error( - "declarePdfUA: no found in the document's XMP metadata", - ); - } - const [match, attrs, selfClosing] = openTag; - - // Bind the `pdfuaid` prefix on the element, then rebuild it with the - // identifier as its FIRST child (property order in RDF is insignificant, - // and inserting at the open tag keeps this a single replace - appending - // before a closing tag could land inside a *nested* description, e.g. an - // XMP struct value). A self-closing element is expanded so the property - // has somewhere to live. The replacement goes through a function because - // the spliced text is document-derived - as a replacement *string*, - // `$`-patterns in it (e.g. `$&`) would be expanded by String.replace. - const attrsWithNs = /pdfuaid/.test(attrs) - ? attrs - : `${attrs} xmlns:pdfuaid="${PDFUA_NS}"`; - const part = `1`; - return xmp.replace( - match, - () => - `${part}${ - selfClosing ? "" : "" - }`, - ); -} diff --git a/packages/xl-pdf-exporter/vite.config.ts b/packages/xl-pdf-exporter/vite.config.ts index 14be012e7f..3c3a7ad2f8 100644 --- a/packages/xl-pdf-exporter/vite.config.ts +++ b/packages/xl-pdf-exporter/vite.config.ts @@ -20,6 +20,9 @@ export default defineConfig( // dist/ leaves consumers without declarations (tsc is skipped). // Entries stored before this spec carried types/** replay stale // declarations; editing this config invalidates them. + // (Invalidated again 2026-08: the Typst-compiler migration + // replaced pdfua/compileBrowser with pdfua/compileTypst and the + // export surface, and cached pre-migration types resurfaced.) output: ["dist/**", "types/**", "!dist/*.tsbuildinfo"], }, }, diff --git a/packages/xl-typst-compiler/.gitignore b/packages/xl-typst-compiler/.gitignore new file mode 100644 index 0000000000..34aa146b52 --- /dev/null +++ b/packages/xl-typst-compiler/.gitignore @@ -0,0 +1,2 @@ +target/ +pkg/ diff --git a/packages/xl-typst-compiler/README.md b/packages/xl-typst-compiler/README.md new file mode 100644 index 0000000000..9812099d8a --- /dev/null +++ b/packages/xl-typst-compiler/README.md @@ -0,0 +1,66 @@ +# @blocknote/xl-typst-compiler + +The [Typst](https://typst.app) compiler - the official `typst` Rust crates, +compiled to WebAssembly - behind a minimal TypeScript API: Typst markup + +assets + fonts in, PDF bytes (or typed diagnostics) out. This is the engine +of BlockNote's [PDF export](https://www.blocknotejs.org/docs/features/export/pdf); +it also works standalone, in browsers and Node alike. + +Design points: + +- **No network access, ever.** The wasm embeds no fonts and downloads + nothing; text renders only with fonts you supply (`fonts` / + `addFont`), and a document needing a missing font fails loudly instead + of rendering substituted glyphs. +- **No singleton.** Create as many compiler instances as you like; each + owns only its font set. The wasm module loads once per page. +- **Native PDF standards.** `pdfStandard: "ua-1"` produces a _validated_, + declared, accessible PDF/UA-1 - typst checks conformance at compile time + (heading structure, alt text, document title, ...) and nonconforming + documents fail with recognizable diagnostics + (`isPdfStandardViolation`), never with a false conformance claim. +- **Failures are values.** `compilePdf` returns a result union; compile + errors (including standard violations) carry structured diagnostics with + messages, hints, and source byte ranges. + +```ts +import { TypstCompiler } from "@blocknote/xl-typst-compiler"; + +const compiler = await TypstCompiler.create({ fonts: [interBytes] }); +const result = compiler.compilePdf(source, { + assets: new Map([["/assets/img.png", pngBytes]]), + pdfStandard: "ua-1", +}); +if (result.error) { + console.log(result.diagnostics); +} else { + download(result.pdf); +} +``` + +## Building the wasm + +The wasm module (`pkg/`) is built from `rust/` (toolchain pinned in +`rust/rust-toolchain.toml`, `wasm32-unknown-unknown` target, +[wasm-pack](https://rustwasm.github.io/wasm-pack/)): + +```bash +pnpm exec vp run --filter @blocknote/xl-typst-compiler build +``` + +The build task self-provisions: `scripts/ensure-wasm.mjs` compiles `pkg/` +from `rust/` when it is missing or stale (content-hashed), before the +TypeScript build. It needs rustup locally (the pinned toolchain and wasm32 +target auto-install); on Vercel it bootstraps rustup itself and keeps the +cargo caches under `node_modules/.cache`, so only the first build on a +fresh cache pays the full compile. `npm run build:wasm` forces a rebuild. +`pkg/` is a build output (not checked in); the published npm package +includes it. + +## Loading the wasm + +By default the module loads from this package's own files, relative to the +wasm-bindgen glue - no CDN involved; bundlers (Vite, webpack) emit it as an +asset automatically. To control loading (self-hosting, caching, Node), pass +`wasm` to `TypstCompiler.create` - a URL or the module bytes. The module is +loaded once; later `create()` calls reuse it. diff --git a/packages/xl-typst-compiler/package.json b/packages/xl-typst-compiler/package.json new file mode 100644 index 0000000000..b5e8b0ae2c --- /dev/null +++ b/packages/xl-typst-compiler/package.json @@ -0,0 +1,67 @@ +{ + "name": "@blocknote/xl-typst-compiler", + "homepage": "https://github.com/TypeCellOS/BlockNote", + "private": false, + "sideEffects": false, + "repository": { + "type": "git", + "url": "git+https://github.com/TypeCellOS/BlockNote.git", + "directory": "packages/xl-typst-compiler" + }, + "license": "GPL-3.0 OR PROPRIETARY", + "version": "0.54.0", + "files": [ + "dist", + "types", + "src", + "pkg", + "rust", + "README.md", + "scripts" + ], + "keywords": [ + "typst", + "pdf", + "wasm", + "compiler", + "javascript", + "typescript" + ], + "description": "The Typst compiler (official typst crates) compiled to WebAssembly, with a minimal TypeScript API - the engine behind BlockNote's PDF export.", + "type": "module", + "source": "src/index.ts", + "types": "./types/src/index.d.ts", + "main": "./dist/blocknote-xl-typst-compiler.cjs", + "module": "./dist/blocknote-xl-typst-compiler.js", + "exports": { + ".": { + "types": "./types/src/index.d.ts", + "import": "./dist/blocknote-xl-typst-compiler.js", + "require": "./dist/blocknote-xl-typst-compiler.cjs" + }, + "./pkg": { + "types": "./pkg/blocknote_typst_wasm.d.ts", + "default": "./pkg/blocknote_typst_wasm.js" + }, + "./wasm": { + "default": "./pkg/blocknote_typst_wasm_bg.wasm" + } + }, + "scripts": { + "dev": "vp dev", + "build:wasm": "node scripts/ensure-wasm.mjs --force", + "lint": "vp lint src", + "test": "vp test --run", + "test-watch": "vp test watch", + "clean": "rimraf dist && rimraf types" + }, + "devDependencies": { + "@types/node": "22.13.13", + "rimraf": "^5.0.10", + "rollup-plugin-webpack-stats": "^0.2.6", + "typescript": "^7.0.2", + "vite-plus": "catalog:", + "wasm-pack": "^0.15.0" + }, + "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" +} diff --git a/packages/xl-typst-compiler/rust/Cargo.lock b/packages/xl-typst-compiler/rust/Cargo.lock new file mode 100644 index 0000000000..e467ae52fb --- /dev/null +++ b/packages/xl-typst-compiler/rust/Cargo.lock @@ -0,0 +1,3128 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "biblatex" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591eba69b7c085632e22ca03606f0bbea68d53bbe26c8962e2d549af20c6a561" +dependencies = [ + "paste", + "roman-numerals-rs", + "strum", + "unic-langid", + "unicode-normalization", + "unscanny", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blocknote-typst-wasm" +version = "0.1.0" +dependencies = [ + "comemo 0.4.0", + "console_error_panic_hook", + "js-sys", + "serde", + "serde-wasm-bindgen", + "typst", + "typst-layout", + "typst-pdf", + "wasm-bindgen", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chinese-number" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e964125508474a83c95eb935697abbeb446ff4e9d62c71ce880e3986d1c606b" +dependencies = [ + "chinese-variant", + "enum-ordinalize", + "num-bigint", + "num-traits", +] + +[[package]] +name = "chinese-variant" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d1275808335c5583ea45a4141d631dafcb36b79fa8a5e10b00e356b6af3e08a" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "citationberg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756ff1e3d43a9ecc8183932fb4d9fd3971236f3ce4acb62fe51d1cd43297547d" +dependencies = [ + "quick-xml 0.38.4", + "serde", + "serde_path_to_error", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "codex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0732ab1a27b4ea05e6f9f60a5122c9924dd5123defde0d8e907f58cf643d40e6" +dependencies = [ + "chinese-number", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "comemo" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6916408a724339aa77b18214233355f3eb04c42eb895e5f8909215bd8a7a91" +dependencies = [ + "comemo-macros 0.4.0", + "once_cell", + "parking_lot", + "siphasher", +] + +[[package]] +name = "comemo" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c963350b2b08aa4b725d7802593245380ab53dacfedcaa971385fc33306c0d4" +dependencies = [ + "comemo-macros 0.5.1", + "parking_lot", + "rustc-hash", + "siphasher", + "slab", +] + +[[package]] +name = "comemo-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8936e42f9b4f5bdfaf23700609ac1f11cb03ad4c1ec128a4ee4fd0903e228db" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "comemo-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c400139ba1389ef9e20ad2d87cda68b437a66483aa0da616bdf2cea7413853" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "ecow" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" +dependencies = [ + "serde", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693" + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glidesort" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" + +[[package]] +name = "glifo" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +dependencies = [ + "bytemuck", + "foldhash", + "hashbrown", + "log", + "peniko", + "png", + "skrifa", + "smallvec", + "vello_common 0.0.9", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hayagriva" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6c185c5c72546d50b25b70187f01ab9a1a2fa21c4db8691e2426fa5fce805c" +dependencies = [ + "biblatex", + "ciborium", + "citationberg", + "icu_collator", + "icu_locale", + "indexmap", + "paste", + "roman-numerals-rs", + "serde", + "serde_yaml", + "thiserror", + "unic-langid", + "unicode-segmentation", + "unscanny", + "url", +] + +[[package]] +name = "hayro" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4caa128ab87fd48ffb7490617cf93f77f606820dffbc9fd9ef6ab0ed077f56d" +dependencies = [ + "bytemuck", + "hayro-interpret", + "image", + "kurbo", + "pic-scale", + "vello_cpu", +] + +[[package]] +name = "hayro-ccitt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" + +[[package]] +name = "hayro-cmap" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d285dc30731c8485de5fa732fbdf2b3affdf01e4da7c2135022ca6fa664bf6" +dependencies = [ + "hayro-postscript", +] + +[[package]] +name = "hayro-interpret" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2613d0406898995042d0794c4245b2f2fba1246490c8ac593769bba6551129d" +dependencies = [ + "bitflags", + "hayro-cmap", + "hayro-syntax", + "kurbo", + "moxcms", + "phf", + "rustc-hash", + "siphasher", + "skrifa", + "smallvec", + "yoke", +] + +[[package]] +name = "hayro-jbig2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69374b3668dd45aeb3d3145cda68f2c7b4f223aaa2511e67d076f1c7d741388d" +dependencies = [ + "fearless_simd", + "hayro-ccitt", +] + +[[package]] +name = "hayro-jpeg2000" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" +dependencies = [ + "fearless_simd", +] + +[[package]] +name = "hayro-postscript" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" + +[[package]] +name = "hayro-svg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7434c89e920d84e077214a29e69b462a6518754610f732f1124af3948410cb8c" +dependencies = [ + "base64", + "hayro-interpret", + "image", + "kurbo", + "siphasher", + "xmlwriter", +] + +[[package]] +name = "hayro-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edeafd70aa2db743de8ede8637d07ec87db05efe69cde371d03f1b185fcef27" +dependencies = [ + "flate2", + "hayro-ccitt", + "hayro-jbig2", + "hayro-jpeg2000", + "memchr", + "rustc-hash", + "smallvec", + "zune-jpeg", +] + +[[package]] +name = "hayro-write" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3db92c4917aad24ff30d61413a9a7509ae88b27aee1ae34eff881e1d2723a" +dependencies = [ + "flate2", + "hayro-syntax", + "pdf-writer", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hypher" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef68590049bab63a464eee1a1158ac04c6f6613a546d8d90f78636b8b94f171" + +[[package]] +name = "icu_collator" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08984ed58ac439ebf3e13d2cf26b0c46a60afcd21721c1d14087c0b240344dda" +dependencies = [ + "icu_collator_data", + "icu_collections", + "icu_locale_core", + "icu_locale_fallback", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d7e54efdddeb1208c08dd5d32b53a879ac00d2d3d051b2255fd26821d16368" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "serde", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "785f595c61ef57169a467eeed7b4b6936a66f6cacbb116a96ee86da983251bf4" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_locale_fallback", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37d91460e362a5cf58907cd7ce871411775ee6294e49a5cc8e6cec16dfa75ea" + +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "serde", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "postcard", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_provider_blob" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916b44fac319d3f58e4d8d805a627df19e27937b582cd99fcde3bb6b7ce1b0e" +dependencies = [ + "icu_provider", + "postcard", + "serde", + "writeable", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_segmenter" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db" +dependencies = [ + "core_maths", + "icu_collections", + "icu_locale_fallback", + "icu_provider", + "icu_segmenter_data", + "potential_utf", + "serde", + "smallvec", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_segmenter_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", + "rayon", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "kamadak-exif" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "krilla" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27da593198b20eeba65caeb73c2bbeec3e53ab08fa549898312ce81c4fce5e33" +dependencies = [ + "base64", + "bumpalo", + "comemo 0.5.1", + "flate2", + "float-cmp", + "gif", + "hayro-write", + "image-webp", + "imagesize", + "indexmap", + "pdf-writer", + "png", + "rayon", + "rustc-hash", + "rustybuzz", + "siphasher", + "skrifa", + "smallvec", + "subsetter", + "tiny-skia-path", + "xmp-writer", + "yoke", + "zune-jpeg", +] + +[[package]] +name = "krilla-svg" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1237d7c37b16ca9fbc2e72dde13a10d321f9a44aceee35c062bc0a43bfc7ce16" +dependencies = [ + "flate2", + "fontdb", + "krilla", + "resvg", + "skrifa", + "smallvec", + "tiny-skia", + "usvg", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lipsum" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" +dependencies = [ + "rand", + "rand_chacha", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +dependencies = [ + "serde_core", +] + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "mutate_once" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pdf-writer" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e456864a7a304047bff84977dc6fb162bd956475d40ba50b2dcecaada7f753" +dependencies = [ + "bitflags", + "itoa", + "memchr", + "ryu", +] + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pic-scale" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c80d88d5c31215ceec2862abb2504860c715b8e2545a5ab15b62a3d097f30d" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64", + "indexmap", + "quick-xml 0.41.0", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "resvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be183ad6a216aa96f33e4c8033b0988b8b3ea6fd2359d19af5bac4643fd8e81" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "roman-numerals-rs" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85cd47a33a4510b1424fe796498e174c6a9cf94e606460ef022a19f3e4ff85e" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subsetter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38803281d1c23166c5ebcb455439a5d2afe711cc909cf88af72448c297756ad6" +dependencies = [ + "kurbo", + "rustc-hash", + "skrifa", + "write-fonts", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo", + "siphasher", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syntect" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925" +dependencies = [ + "bincode", + "fancy-regex", + "flate2", + "fnv", + "once_cell", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "thin-vec" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79def32ffcd477db1ff26f76dab9e3a91f0bd42a85ca96577089b24623056f9d" + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ffee5eaaf5527f630fb0e356b90ebdec84d5d18d937c5e440350f88c5a91ea" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca365c3faccca67d06593c5980fa6c57687de727a03131735bb85f01fdeeb9" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "two-face" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e51b6e60e545cfdae5a4639ff423818f52372211a8d9a3e892b4b0761f76b2" +dependencies = [ + "serde", + "serde_derive", + "syntect", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typst" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cb79435d128de5d5443a058a983f6dcc16738760fa55ed703762bc278b1fd3" +dependencies = [ + "arrayvec", + "comemo 0.5.1", + "ecow", + "rustc-hash", + "typst-eval", + "typst-html", + "typst-layout", + "typst-library", + "typst-macros", + "typst-realize", + "typst-syntax", + "typst-timing", + "typst-utils", +] + +[[package]] +name = "typst-assets" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcee505dac6702dd1c5e65aa2e94a6179d19ee09e2e5637d7313db91765dc4e0" +dependencies = [ + "bitflags", +] + +[[package]] +name = "typst-eval" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8823e1822bd181d8bee47b05632d16bf3a0a8ab042e01d815826350d7ae16c3" +dependencies = [ + "comemo 0.5.1", + "ecow", + "indexmap", + "rustc-hash", + "stacker", + "toml", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-segmentation", +] + +[[package]] +name = "typst-html" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41bcfbe480b640c4447d8bf9bb59020da610f239aa032d53328651a03572c09" +dependencies = [ + "az", + "bumpalo", + "comemo 0.5.1", + "ecow", + "palette", + "rustc-hash", + "time", + "typst-assets", + "typst-library", + "typst-macros", + "typst-svg", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-math-class", +] + +[[package]] +name = "typst-layout" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1916c407baf8f8815e5d50030eed6d5ac6a0702d927b3c156e38db7a618499e2" +dependencies = [ + "az", + "bumpalo", + "codex", + "comemo 0.5.1", + "ecow", + "either", + "hypher", + "icu_properties", + "icu_provider", + "icu_provider_blob", + "icu_segmenter", + "kurbo", + "libm", + "memchr", + "rustc-hash", + "rustybuzz", + "smallvec", + "ttf-parser", + "typst-assets", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-bidi", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "typst-library" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12becd02464849f1507f3c4ef3f401a4d28d5938cc74b6acc6f1d703458bf393" +dependencies = [ + "arrayvec", + "az", + "bitflags", + "bumpalo", + "ciborium", + "codex", + "comemo 0.5.1", + "csv", + "ecow", + "either", + "flate2", + "fontdb", + "glidesort", + "hayagriva", + "hayro-syntax", + "icu_properties", + "image", + "indexmap", + "kamadak-exif", + "kurbo", + "libm", + "lipsum", + "memchr", + "moxcms", + "palette", + "percent-encoding", + "phf", + "png", + "rayon", + "regex", + "regex-syntax", + "roxmltree 0.21.1", + "rust_decimal", + "rustc-hash", + "rustybuzz", + "serde", + "serde_json", + "serde_yaml", + "siphasher", + "smallvec", + "syntect", + "time", + "toml", + "ttf-parser", + "two-face", + "typed-arena", + "typst-assets", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", + "unicode-math-class", + "unicode-normalization", + "unicode-segmentation", + "unscanny", + "usvg", + "utf8_iter", + "wasmi", + "xmlwriter", +] + +[[package]] +name = "typst-macros" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033626e6fd43dc09170a9237c55a4dd308005fd751456a0c81b5864f822a97df" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "typst-pdf" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aef841ff3e606542f3057ce5dd0ff264d53a439e00ed4a91ec00ec8568b3b7f" +dependencies = [ + "az", + "bytemuck", + "codex", + "comemo 0.5.1", + "ecow", + "flate2", + "image", + "indexmap", + "infer", + "krilla", + "krilla-svg", + "rustc-hash", + "serde", + "smallvec", + "typst-assets", + "typst-layout", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", +] + +[[package]] +name = "typst-realize" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2eb73ab25add88cab03b26effabc657977fe850ff81556b28c6b137ff4bf1" +dependencies = [ + "arrayvec", + "bumpalo", + "comemo 0.5.1", + "ecow", + "regex", + "typst-html", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", +] + +[[package]] +name = "typst-svg" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df6dc785695aae085678bd6657aa04a1d26058a76ed042e738658078bca681e" +dependencies = [ + "base64", + "comemo 0.5.1", + "ecow", + "flate2", + "hayro", + "hayro-svg", + "image", + "indexmap", + "itoa", + "rustc-hash", + "ryu", + "ttf-parser", + "typst-assets", + "typst-layout", + "typst-library", + "typst-macros", + "typst-timing", + "typst-utils", + "xmlwriter", +] + +[[package]] +name = "typst-syntax" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a20d79df5d10c7fb0b4f7c18e4b2671edfc21e5ed9ad9894e18cccefd1c70f" +dependencies = [ + "ecow", + "rustc-hash", + "serde", + "toml", + "typst-timing", + "typst-utils", + "unicode-ident", + "unicode-math-class", + "unicode-script", + "unicode-segmentation", + "unscanny", +] + +[[package]] +name = "typst-timing" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab6ee5110f671a23164c61caa85dbc476043d503103dedd36f16d93f970f3f59" +dependencies = [ + "parking_lot", + "serde", + "serde_json", +] + +[[package]] +name = "typst-utils" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33ad1675497b45ae9d960a16e95207c35c2c46330d1a6249a9a977670c8c160" +dependencies = [ + "libm", + "once_cell", + "portable-atomic", + "rayon", + "rustc-hash", + "semver", + "siphasher", + "smallvec", + "thin-vec", + "unicode-math-class", +] + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", + "unic-langid-macros", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "serde", + "tinystr", +] + +[[package]] +name = "unic-langid-macros" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5957eb82e346d7add14182a3315a7e298f04e1ba4baac36f7f0dbfedba5fc25" +dependencies = [ + "proc-macro-hack", + "tinystr", + "unic-langid-impl", + "unic-langid-macros-impl", +] + +[[package]] +name = "unic-langid-macros-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1249a628de3ad34b821ecb1001355bca3940bcb2f88558f1a8bd82e977f75b5" +dependencies = [ + "proc-macro-hack", + "quote", + "syn 2.0.119", + "unic-langid-impl", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-math-class" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d246cf599d5fae3c8d56e04b20eb519adb89a8af8d0b0fbcded369aa3647d65" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "usvg" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "ttf-parser", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vello_common" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown", + "log", + "peniko", + "png", + "smallvec", + "thiserror", +] + +[[package]] +name = "vello_common" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +dependencies = [ + "bytemuck", + "fearless_simd", + "guillotiere", + "hashbrown", + "log", + "peniko", + "png", + "smallvec", + "thiserror", +] + +[[package]] +name = "vello_cpu" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0" +dependencies = [ + "bytemuck", + "glifo", + "hashbrown", + "png", + "vello_common 0.0.8", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasmi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2300d0f78cba12f14e29e8dd157ea64050c0a688179aefdb2050105805594a0c" +dependencies = [ + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser", +] + +[[package]] +name = "wasmi_collections" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a8c42a2a76148d43097b1d7cc2a5bf33d5c23bd4dd69015fc887e311767884" + +[[package]] +name = "wasmi_core" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9013136083d988725953390bf668b64b7a218fabf26f8b913bbc59546b97ee27" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1fa003f79156f406d62ef0e1464dc03e11ace37170e9fa7524299a75ad8f68" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "write-fonts" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb731d4c4d93eacc69a1ad2f270f905788a98e4a3438267bcafbe08d3431c8d8" +dependencies = [ + "font-types", + "indexmap", + "kurbo", + "log", + "read-fonts", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "xmp-writer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "litemap", + "serde_core", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/packages/xl-typst-compiler/rust/Cargo.toml b/packages/xl-typst-compiler/rust/Cargo.toml new file mode 100644 index 0000000000..c9011d428f --- /dev/null +++ b/packages/xl-typst-compiler/rust/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "blocknote-typst-wasm" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +typst = "0.15.1" +typst-pdf = "0.15.1" +comemo = "0.4" +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.6" +console_error_panic_hook = "0.1.7" +typst-layout = "0.15.1" + +# Size-focused release profile: the wasm is a shipped web asset. +[profile.release] +opt-level = "s" +lto = true +strip = true +codegen-units = 1 + +# wasm-pack's bundled wasm-opt (an old binaryen) fails validating modern +# rustc output; ship the direct rustc artifact. A current binaryen pass is +# a known size optimization to add later (see README). +[package.metadata.wasm-pack.profile.release] +wasm-opt = false diff --git a/packages/xl-typst-compiler/rust/rust-toolchain.toml b/packages/xl-typst-compiler/rust/rust-toolchain.toml new file mode 100644 index 0000000000..128dbd4e77 --- /dev/null +++ b/packages/xl-typst-compiler/rust/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.92.0" +targets = ["wasm32-unknown-unknown"] diff --git a/packages/xl-typst-compiler/rust/src/lib.rs b/packages/xl-typst-compiler/rust/src/lib.rs new file mode 100644 index 0000000000..b8275c7dd6 --- /dev/null +++ b/packages/xl-typst-compiler/rust/src/lib.rs @@ -0,0 +1,353 @@ +//! Minimal wasm binding over the official `typst` crates for BlockNote's PDF +//! export: an in-memory `World` (main source + asset files), fonts supplied as +//! bytes, and native PDF-standards support (`ua-1`) via `typst-pdf`. +//! +//! Deliberately excluded: package registry (BlockNote's generated markup +//! imports no packages), incremental rendering, SVG/vector output, system +//! font access, network access of any kind. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use typst::diag::{FileError, FileResult, SourceDiagnostic}; +use typst::foundations::{Bytes, Datetime, Duration}; +use typst::syntax::{DiagSpanKind, FileId, RootedPath, Source, VirtualPath, VirtualRoot}; +use typst::text::{Font, FontBook}; +use typst::utils::LazyHash; +use typst::{Library, LibraryExt, World}; +use typst_layout::PagedDocument; +use typst_pdf::{PdfOptions, PdfStandard, PdfStandards}; +use wasm_bindgen::prelude::*; + +/// An immutable in-memory world for a single compilation. +struct BnWorld { + library: LazyHash, + book: LazyHash, + fonts: Vec, + main: Source, + /// Additional `.typ` sources (imports), interned by path. + sources: HashMap, + files: HashMap, +} + +impl World for BnWorld { + fn library(&self) -> &LazyHash { + &self.library + } + + fn book(&self) -> &LazyHash { + &self.book + } + + fn main(&self) -> FileId { + self.main.id() + } + + fn source(&self, id: FileId) -> FileResult { + if id == self.main.id() { + Ok(self.main.clone()) + } else { + self.sources + .get(&id) + .cloned() + .ok_or_else(|| FileError::NotFound(id.vpath().as_rooted_path().into())) + } + } + + fn file(&self, id: FileId) -> FileResult { + self.files + .get(&id) + .cloned() + .ok_or_else(|| FileError::NotFound(id.vpath().as_rooted_path().into())) + } + + fn font(&self, index: usize) -> Option { + self.fonts.get(index).cloned() + } + + fn today(&self, _offset: Option) -> Option { + // Deterministic output: no wall clock in the sandbox. Documents using + // `datetime.today()` get no date rather than a nondeterministic one. + None + } +} + +/// Options for a single PDF compilation, mirroring `typst-pdf`'s knobs that +/// BlockNote needs. camelCase to match the JS caller. +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct CompileOptions { + /// A PDF standard to enforce, e.g. "ua-1". Enforcement includes typst's + /// compile-time validation; failures come back as diagnostics. + #[serde(default)] + pdf_standard: Option, + /// Emit the tagged (accessible) structure tree. typst's default is true. + #[serde(default)] + tagged: Option, + /// PDF creation timestamp in seconds since the Unix epoch (UTC). Pass a + /// fixed value for byte-reproducible output; omitted means no timestamp. + #[serde(default)] + creation_timestamp: Option, +} + +/// One diagnostic, serialized to JS. Whether it is an error or a warning +/// is expressed by which list it arrives in (`compileErrors` / +/// `compileWarnings`), not by a field. `message` keeps typst's original +/// text, including the "PDF/UA-1 error:" prefix on validation errors, so +/// callers can distinguish conformance failures from genuine compile +/// errors. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct JsDiagnostic { + message: String, + hints: Vec, + /// Byte range in the main source, when the span points there. + range: Option<(usize, usize)>, +} + +fn to_js_diags(main: &Source, diags: &[SourceDiagnostic]) -> Vec { + diags + .iter() + .map(|d| JsDiagnostic { + message: d.message.to_string(), + hints: d.hints.iter().map(|h| h.v.to_string()).collect(), + range: diag_range(main, d.span), + }) + .collect() +} + +/// Resolve a diagnostic's span to a byte range in the main source. +fn diag_range(main: &Source, span: typst::syntax::DiagSpan) -> Option<(usize, usize)> { + match span.get() { + DiagSpanKind::Number { id, num, sub_range } if id == main.id() => { + main.range(num, sub_range).map(|r| (r.start, r.end)) + } + DiagSpanKind::Range { id, range } if id == main.id() => { + Some((range.start, range.end)) + } + _ => None, + } +} + +/// The uniform result payload: `pdf` present on success, and both +/// diagnostic lists always present (empty when there is nothing to say). +/// Compile failure is an expected outcome, so it is *returned*, never +/// thrown - `Err` is reserved for caller mistakes (invalid options). +fn compile_result( + main: &Source, + pdf: Option<&[u8]>, + errors: &[SourceDiagnostic], + warnings: &[SourceDiagnostic], +) -> JsValue { + let result = js_sys::Object::new(); + if let Some(pdf) = pdf { + js_sys::Reflect::set( + &result, + &"pdf".into(), + &js_sys::Uint8Array::from(pdf).into(), + ) + .expect("plain object set"); + } + for (key, diags) in [("compileErrors", errors), ("compileWarnings", warnings)] { + js_sys::Reflect::set( + &result, + &key.into(), + &serde_wasm_bindgen::to_value(&to_js_diags(main, diags)) + .expect("diagnostics serialize"), + ) + .expect("plain object set"); + } + result.into() +} + +/// The compiler: owns the loaded fonts (the expensive, reusable part). +/// Each `compile_pdf` call builds a fresh world, so instances are cheap to +/// keep and there is no page-level singleton. +#[wasm_bindgen] +pub struct TypstCompiler { + fonts: Vec, + font_hashes: std::collections::HashSet, + library: LazyHash, +} + +#[wasm_bindgen] +impl TypstCompiler { + #[wasm_bindgen(constructor)] + pub fn new() -> TypstCompiler { + console_error_panic_hook::set_once(); + TypstCompiler { + fonts: Vec::new(), + font_hashes: std::collections::HashSet::new(), + library: LazyHash::new(Library::default()), + } + } + + /// Load every face of a font file (TTF/OTF/TTC). Byte-identical files + /// are deduplicated (repeat loads are no-ops). + /// Returns the number of faces added; 0 means the bytes were not a font + /// (or were already loaded). + pub fn add_font(&mut self, data: &[u8]) -> u32 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + data.hash(&mut hasher); + if !self.font_hashes.insert(hasher.finish()) { + return 0; + } + let bytes = Bytes::new(data.to_vec()); + let mut added = 0u32; + // Collections hold multiple faces; probe indices until one fails. + for index in 0..64 { + match Font::new(bytes.clone(), index) { + Some(font) => { + self.fonts.push(font); + added += 1; + } + None => break, + } + } + added + } + + /// The family names of the loaded fonts, in load order (deduplicated) - + /// what the Typst source can reference in `#set text(font: ...)`. + pub fn font_families(&self) -> Vec { + let mut seen = std::collections::HashSet::new(); + self.fonts + .iter() + .map(|f| f.info().family.to_string()) + .filter(|name| seen.insert(name.clone())) + .collect() + } + + /// Compile Typst markup to PDF bytes. + /// + /// `asset_paths[i]` (an absolute virtual path like "/assets/img0.png") + /// names the file whose bytes are `asset_data[i]`. + /// + /// On failure the error is `{ diagnostics: [...] }`; PDF-standard + /// validation failures are diagnostics whose message starts with the + /// validator prefix (e.g. "PDF/UA-1 error:"). + pub fn compile_pdf( + &self, + main_source: &str, + asset_paths: Vec, + asset_data: Vec, + options: JsValue, + ) -> Result { + let options: CompileOptions = if options.is_undefined() || options.is_null() { + CompileOptions::default() + } else { + serde_wasm_bindgen::from_value(options) + .map_err(|e| JsValue::from_str(&format!("invalid options: {e}")))? + }; + + if asset_paths.len() != asset_data.len() { + return Err(JsValue::from_str("asset_paths/asset_data length mismatch")); + } + let mut files = HashMap::new(); + let mut sources = HashMap::new(); + for (path, data) in asset_paths.into_iter().zip(asset_data) { + let id = project_file(&path)?; + if path.ends_with(".typ") { + // Importable source files must be registered as sources, not + // raw bytes, so `#import` finds them. + let text = String::from_utf8(data.to_vec()).map_err(|_| { + JsValue::from_str(&format!("{path}: .typ files must be UTF-8")) + })?; + sources.insert(id, Source::new(id, text)); + } else { + files.insert(id, Bytes::new(data.to_vec())); + } + } + + let world = BnWorld { + library: self.library.clone(), + book: LazyHash::new(FontBook::from_fonts(&self.fonts)), + fonts: self.fonts.clone(), + main: Source::new( + project_file("/main.typ").expect("static path is valid"), + main_source.to_string(), + ), + sources, + files, + }; + + let compiled = typst::compile::(&world); + let document = match compiled.output { + Ok(document) => document, + Err(errors) => { + return Ok(compile_result( + &world.main, + None, + &errors, + &compiled.warnings, + )); + } + }; + + let standards = match options.pdf_standard { + Some(standard) => PdfStandards::new(&[standard]) + .map_err(|e| JsValue::from_str(&format!("invalid PDF standard: {}", e.message())))?, + None => PdfStandards::default(), + }; + let pdf_options = PdfOptions { + standards, + tagged: options.tagged.unwrap_or(true), + timestamp: options + .creation_timestamp + .and_then(unix_seconds_to_datetime) + .map(typst_pdf::Timestamp::new_utc), + ..Default::default() + }; + let pdf = match typst_pdf::pdf(&document, &pdf_options) { + Ok(pdf) => pdf, + Err(errors) => { + return Ok(compile_result( + &world.main, + None, + &errors, + &compiled.warnings, + )); + } + }; + Ok(compile_result( + &world.main, + Some(&pdf), + &[], + &compiled.warnings, + )) + } +} + +/// Civil-from-days (Howard Hinnant's algorithm): Unix seconds -> Datetime. +/// Deterministic and dependency-free; returns `None` outside Datetime's +/// supported range. +fn unix_seconds_to_datetime(seconds: i64) -> Option { + let days = seconds.div_euclid(86_400); + let secs_of_day = seconds.rem_euclid(86_400); + let z = days + 719_468; + let era = z.div_euclid(146_097); + let doe = z.rem_euclid(146_097); + let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + Datetime::from_ymd_hms( + i32::try_from(y).ok()?, + u8::try_from(m).ok()?, + u8::try_from(d).ok()?, + u8::try_from(secs_of_day / 3600).ok()?, + u8::try_from((secs_of_day % 3600) / 60).ok()?, + u8::try_from(secs_of_day % 60).ok()?, + ) +} + +/// Interns an absolute virtual path in the project root. +fn project_file(path: &str) -> Result { + let vpath = VirtualPath::new(path) + .map_err(|e| JsValue::from_str(&format!("invalid path {path:?}: {e}")))?; + Ok(FileId::new(RootedPath::new(VirtualRoot::Project, vpath))) +} diff --git a/packages/xl-typst-compiler/scripts/ensure-wasm.mjs b/packages/xl-typst-compiler/scripts/ensure-wasm.mjs new file mode 100644 index 0000000000..666b993d9d --- /dev/null +++ b/packages/xl-typst-compiler/scripts/ensure-wasm.mjs @@ -0,0 +1,154 @@ +// Ensures pkg/ (the wasm-pack build output) exists and matches the Rust +// sources, building it when it doesn't. Runs as the first step of this +// package's build task, so every consumer - `vp run -r build`, CI, the +// Vercel builds (whose `--filter '...'` graphs include this package) - +// gets a correct wasm without a separate step. +// +// - Fresh (pkg/.build-hash matches the hashed Rust inputs): exits silently. +// - Stale/missing: builds with the wasm-pack devDependency. Needs a Rust +// toolchain; rustup auto-provisions the pinned version + wasm32 target +// from rust/rust-toolchain.toml. +// - No toolchain: on CI and Vercel, rustup is bootstrapped automatically +// (GitHub's ubuntu-24.04 runner images no longer ship Rust). On Vercel it +// installs under node_modules/.cache, which Vercel persists between +// builds - as are the cargo registry and target dirs, so warm builds take +// seconds. On a dev machine it fails with instructions instead of +// installing things. +// +// `--force` skips the freshness check (the `build:wasm` script). +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageDir = dirname(dirname(fileURLToPath(import.meta.url))); +const rustDir = join(packageDir, "rust"); +const pkgDir = join(packageDir, "pkg"); +const hashFile = join(pkgDir, ".build-hash"); + +function listRustInputs() { + const files = [ + join(rustDir, "Cargo.toml"), + join(rustDir, "Cargo.lock"), + join(rustDir, "rust-toolchain.toml"), + ]; + function walk(dir) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + walk(path); + } else { + files.push(path); + } + } + } + walk(join(rustDir, "src")); + return files.sort(); +} + +function inputHash() { + const hash = createHash("sha256"); + // The wasm-pack version participates: it drives the generated JS glue. + hash.update( + JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")) + .devDependencies["wasm-pack"], + ); + for (const file of listRustInputs()) { + hash.update(file); + hash.update(readFileSync(file)); + } + return hash.digest("hex"); +} + +function commandExists(command, env) { + try { + execFileSync(command, ["--version"], { stdio: "ignore", env }); + return true; + } catch { + return false; + } +} + +const force = process.argv.includes("--force"); +const currentHash = inputHash(); +if ( + !force && + existsSync(join(pkgDir, "blocknote_typst_wasm_bg.wasm")) && + existsSync(hashFile) && + readFileSync(hashFile, "utf8") === currentHash +) { + process.exit(0); +} + +const env = { ...process.env }; + +// On Vercel, keep every Rust artifact under node_modules/.cache: it is +// persisted between builds, so only the first build on a fresh cache pays +// the full compile. Elsewhere rustup's default home is fine (and gets on +// PATH explicitly, since a fresh install can't modify the current shell). +if (process.env.VERCEL) { + const cacheDir = join(packageDir, "node_modules", ".cache", "rust"); + mkdirSync(cacheDir, { recursive: true }); + env.CARGO_HOME = join(cacheDir, "cargo"); + env.RUSTUP_HOME = join(cacheDir, "rustup"); + env.CARGO_TARGET_DIR = join(cacheDir, "target"); + env.PATH = `${join(env.CARGO_HOME, "bin")}:${env.PATH}`; +} else { + env.PATH = `${join(homedir(), ".cargo", "bin")}:${env.PATH}`; +} + +if (!commandExists("cargo", env)) { + // CI covers GitHub Actions (whose ubuntu-24.04 images ship no Rust) and + // most other build services; only interactive dev machines get the + // do-it-yourself error. + if (!process.env.VERCEL && !process.env.CI) { + console.error( + "[ensure-wasm] pkg/ is missing or stale and no Rust toolchain was " + + "found. Install rustup (https://rustup.rs) - the pinned toolchain " + + "and wasm32 target auto-install from rust/rust-toolchain.toml - " + + "then re-run the build (or `npm run build:wasm` in " + + "packages/xl-typst-compiler).", + ); + process.exit(1); + } + console.log("[ensure-wasm] installing rustup (CI/Vercel build)…"); + execFileSync( + "sh", + [ + "-c", + "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain none --no-modify-path", + ], + { stdio: "inherit", env }, + ); +} + +console.log("[ensure-wasm] building the Typst compiler wasm (rust/ -> pkg/)…"); +execFileSync( + join(packageDir, "node_modules", ".bin", "wasm-pack"), + [ + "build", + ".", + "--target", + "web", + "--release", + "--out-dir", + "../pkg", + "--out-name", + "blocknote_typst_wasm", + ], + // cwd must be the crate directory: rustup resolves rust-toolchain.toml + // (the pinned toolchain + wasm32 target, auto-installed on first use) by + // walking up from the *working directory* - from packageDir a freshly + // bootstrapped rustup with no default toolchain finds nothing to run. + { stdio: "inherit", cwd: rustDir, env }, +); +writeFileSync(hashFile, currentHash); +console.log("[ensure-wasm] done."); diff --git a/packages/xl-typst-compiler/src/index.ts b/packages/xl-typst-compiler/src/index.ts new file mode 100644 index 0000000000..71b33ad69e --- /dev/null +++ b/packages/xl-typst-compiler/src/index.ts @@ -0,0 +1,8 @@ +export { + TypstCompiler, + isPdfStandardViolation, + type CompilePdfOptions, + type CompilePdfResult, + type TypstCompilerOptions, + type TypstDiagnostic, +} from "./typstCompiler.js"; diff --git a/packages/xl-typst-compiler/src/typstCompiler.test.ts b/packages/xl-typst-compiler/src/typstCompiler.test.ts new file mode 100644 index 0000000000..c01fe5a5b9 --- /dev/null +++ b/packages/xl-typst-compiler/src/typstCompiler.test.ts @@ -0,0 +1,262 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { beforeAll, describe, expect, it } from "vite-plus/test"; +import { + TypstCompiler, + isPdfStandardViolation, + type CompilePdfResult, +} from "./index.js"; + +const pkgDir = join(__dirname, ".."); +const fontDir = join(pkgDir, "..", "..", "shared", "assets", "fonts"); + +function font(rel: string): Uint8Array { + return new Uint8Array(readFileSync(join(fontDir, rel))); +} + +// Node has no URL-relative wasm loading from the test runner, so the module +// bytes are passed explicitly (the browser path is covered by the tests +// package's e2e suite, which exercises URL loading through a bundler). +const wasmBytes = new Uint8Array( + readFileSync(join(pkgDir, "pkg", "blocknote_typst_wasm_bg.wasm")), +); + +const latin1 = (bytes: Uint8Array) => new TextDecoder("latin1").decode(bytes); +const isPdf = (bytes: Uint8Array) => latin1(bytes.slice(0, 5)) === "%PDF-"; + +// A 1x1 red PNG. +const png = Uint8Array.from( + atob( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", + ), + (c) => c.charCodeAt(0), +); + +function expectPdf( + result: CompilePdfResult, +): Extract { + if (result.error) { + throw new Error( + `expected a PDF, got errors: ${result.compileErrors + .map((d) => d.message) + .join(" | ")}`, + ); + } + return result; +} + +let compiler: TypstCompiler; + +beforeAll(async () => { + compiler = await TypstCompiler.create({ + wasm: wasmBytes, + fonts: [ + font("inter/Inter_18pt-Regular.ttf"), + font("inter/Inter_18pt-Bold.ttf"), + font("newcm/NewCMMath-Regular.otf"), + font("newcm/NewCMMath-Book.otf"), + ], + }); +}); + +const preamble = `#set document(title: [Test])\n#set text(font: "Inter 18pt", lang: "en")\n`; +const conforming = `${preamble}= Heading +Body with math #math.equation(alt: "a squared", $a^2$). +#figure(image("/assets/dot.png", width: 10pt, alt: "A red dot"), caption: [A dot]) +`; +const assets = new Map([["/assets/dot.png", png]]); + +describe("compilePdf", () => { + it("compiles markup with fonts, math, and image assets to a PDF", () => { + const { pdf, compileWarnings } = expectPdf( + compiler.compilePdf(conforming, { assets }), + ); + expect(isPdf(pdf)).toBe(true); + expect(compileWarnings).toEqual([]); + // Tagged by default, but no conformance claim without a standard. + expect(latin1(pdf)).toContain("StructTreeRoot"); + expect(latin1(pdf)).not.toContain("pdfuaid"); + }); + + it("can disable tagging", () => { + const { pdf } = expectPdf( + compiler.compilePdf(conforming, { assets, tagged: false }), + ); + expect(latin1(pdf)).not.toContain("StructTreeRoot"); + }); + + it("returns typed diagnostics for invalid markup", () => { + const result = compiler.compilePdf(`${preamble}#broken(`); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect(result.compileErrors.length).toBeGreaterThan(0); + expect(result.compileErrors.some((d) => isPdfStandardViolation(d))).toBe( + false, + ); + } + }); + + it("fails loudly when a needed font is missing (no embedded fonts)", async () => { + const fontless = await TypstCompiler.create(); + const result = fontless.compilePdf(`$x$`); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect(result.compileErrors[0].message).toContain( + "no font could be found", + ); + } + }); + + it("surfaces compile warnings alongside the produced PDF", () => { + const { pdf, compileWarnings } = expectPdf( + compiler.compilePdf( + `${preamble}#set text(font: "No Such Family")\nHello`, + ), + ); + expect(isPdf(pdf)).toBe(true); + expect( + compileWarnings.some((w) => w.message.includes("unknown font family")), + ).toBe(true); + }); + + it("carries warnings on the failure branch too", () => { + // A document can accumulate warnings before hitting a hard error; the + // failure result keeps both, so callers don't lose context. (The error + // must occur past parsing - a parse error aborts before the stages + // that produce warnings.) + const result = compiler.compilePdf( + `${preamble}#set text(font: "No Such Family")\nHello\n#image("/nope.png")`, + ); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect(result.compileErrors.length).toBeGreaterThan(0); + expect( + result.compileWarnings.some((w) => + w.message.includes("unknown font family"), + ), + ).toBe(true); + } + }); + + it("throws (not a typed failure) for invalid caller options", () => { + // An unknown PDF standard is a caller mistake, not an expected + // document-driven outcome - it must not masquerade as a compile + // failure. + expect(() => + compiler.compilePdf(`${preamble}Hello`, { pdfStandard: "bogus" }), + ).toThrow(); + }); + + it("supports importable .typ assets", () => { + const result = expectPdf( + compiler.compilePdf(`${preamble}#import "/lib.typ": shout\n#shout[hi]`, { + assets: new Map([ + [ + "/lib.typ", + new TextEncoder().encode(`#let shout(body) = [#upper(body)!]`), + ], + ]), + }), + ); + expect(isPdf(result.pdf)).toBe(true); + }); + + it("produces byte-identical output for a fixed creationTimestamp", () => { + const opts = { assets, creationTimestamp: 0 }; + const first = expectPdf(compiler.compilePdf(conforming, opts)); + const second = expectPdf(compiler.compilePdf(conforming, opts)); + expect(latin1(first.pdf)).toContain("1970-01-01T00:00:00"); + expect(Buffer.from(first.pdf).equals(Buffer.from(second.pdf))).toBe(true); + }); +}); + +describe("PDF standards (ua-1)", () => { + it("declares validated PDF/UA-1 conformance", () => { + const { pdf } = expectPdf( + compiler.compilePdf(conforming, { assets, pdfStandard: "ua-1" }), + ); + expect(latin1(pdf)).toContain("pdfuaid"); + expect(latin1(pdf)).toContain("StructTreeRoot"); + }); + + it("fails nonconforming documents with recognizable violations", () => { + const nonconforming = `${preamble}== Not level one\nBody.\n`; + const result = compiler.compilePdf(nonconforming, { + pdfStandard: "ua-1", + }); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect(result.compileErrors.length).toBeGreaterThan(0); + // Every failure is a conformance violation (with a source range), so + // callers can distinguish "doesn't conform" from "broken document" + // and fall back to an unclaimed export. + expect(result.compileErrors.every(isPdfStandardViolation)).toBe(true); + expect(result.compileErrors.some((d) => d.range)).toBe(true); + } + const fallback = expectPdf(compiler.compilePdf(nonconforming, {})); + expect(latin1(fallback.pdf)).not.toContain("pdfuaid"); + expect(latin1(fallback.pdf)).toContain("StructTreeRoot"); + }); + + it("rejects violations for missing alt text", () => { + const noAlt = `${preamble}#figure(image("/assets/dot.png", width: 10pt), caption: [No alt])\n`; + const result = compiler.compilePdf(noAlt, { assets, pdfStandard: "ua-1" }); + expect(result.error).toBe("compile-failed"); + if (result.error) { + expect( + result.compileErrors.some((d) => + d.message.includes("missing alt text"), + ), + ).toBe(true); + } + }); +}); + +describe("fonts", () => { + it("lists loaded font families and deduplicates repeated files", async () => { + const own = await TypstCompiler.create({ wasm: wasmBytes }); + const inter = font("inter/Inter_18pt-Regular.ttf"); + expect(own.addFont(inter)).toBe(1); + expect(own.addFont(font("GeistMono-Regular.ttf"))).toBe(1); + // Byte-identical repeat is a no-op. + expect(own.addFont(font("inter/Inter_18pt-Regular.ttf"))).toBe(0); + expect(own.fontFamilies()).toEqual(["Inter 18pt", "Geist Mono"]); + }); +}); + +describe("the real exporter document", () => { + it("compiles the shared test document snapshot as declared ua-1", async () => { + const full = await TypstCompiler.create({ + wasm: wasmBytes, + fonts: [ + font("inter/Inter_18pt-Regular.ttf"), + font("inter/Inter_18pt-Italic.ttf"), + font("inter/Inter_18pt-Bold.ttf"), + font("inter/Inter_18pt-BoldItalic.ttf"), + font("GeistMono-Regular.ttf"), + font("newcm/NewCMMath-Regular.otf"), + font("newcm/NewCMMath-Book.otf"), + font("noto/Noto-COLRv1.ttf"), + ], + }); + const source = readFileSync( + join( + pkgDir, + "..", + "xl-typst-exporter", + "src", + "__snapshots__", + "testDocument.typ", + ), + "utf8", + ); + const { pdf } = expectPdf( + full.compilePdf(source, { + assets: new Map([["/assets/asset-0", png]]), + pdfStandard: "ua-1", + }), + ); + expect(isPdf(pdf)).toBe(true); + expect(latin1(pdf)).toContain("pdfuaid"); + }); +}); diff --git a/packages/xl-typst-compiler/src/typstCompiler.ts b/packages/xl-typst-compiler/src/typstCompiler.ts new file mode 100644 index 0000000000..474cba4c30 --- /dev/null +++ b/packages/xl-typst-compiler/src/typstCompiler.ts @@ -0,0 +1,191 @@ +/** + * A minimal TypeScript API over the Typst compiler (the official `typst` + * crates, compiled to WebAssembly - see `rust/`): Typst markup + assets + + * fonts in, PDF bytes (or typed diagnostics) out. + * + * Runs anywhere WebAssembly runs - browsers, Node, workers. There is no + * page-level singleton: create as many compilers as needed; each owns only + * its font set. The wasm module itself is loaded once per page/process. + */ +import initWasm, { + TypstCompiler as WasmCompiler, +} from "@blocknote/xl-typst-compiler/pkg"; + +/** + * One compiler diagnostic. Whether it is an error or a warning is expressed + * by which result list it arrives in (`compileErrors` / `compileWarnings`), + * so there is no severity field to cross-check. + */ +export type TypstDiagnostic = { + message: string; + hints: string[]; + /** Byte range in the compiled source, when the span points there. */ + range?: [number, number]; +}; + +/** + * Compilation failure is part of the contract (the source is generated from + * user content, and PDF-standard validation is expected to fail for + * nonconforming documents), so it's a value, not an exception. + */ +export type CompilePdfResult = + | { + error?: undefined; + pdf: Uint8Array; + compileWarnings: TypstDiagnostic[]; + } + | { + error: "compile-failed"; + compileErrors: TypstDiagnostic[]; + compileWarnings: TypstDiagnostic[]; + }; + +export type CompilePdfOptions = { + /** + * Files referenced from the markup, keyed by absolute virtual path (e.g. + * `/assets/asset-0`). `.typ` entries become importable sources; everything + * else raw bytes (images etc.). + */ + assets?: ReadonlyMap; + /** + * A PDF standard to enforce, e.g. `"ua-1"` for accessible, tagged + * PDF/UA-1. Typst validates conformance at compile time: a nonconforming + * document fails with diagnostics recognizable via + * {@link isPdfStandardViolation} (e.g. "the first heading must be of + * level 1"), rather than producing a false conformance claim. + */ + pdfStandard?: "ua-1" | "a-2b" | "a-3b" | (string & {}); + /** + * Emit the tagged (accessible) structure tree. + * @default true + */ + tagged?: boolean; + /** + * PDF creation timestamp in seconds since the Unix epoch (UTC). Pass a + * fixed value for byte-reproducible output; omitted means no timestamp. + */ + creationTimestamp?: number; +}; + +/** + * Whether a diagnostic is a PDF-standard conformance violation - an + * expected outcome for nonconforming documents - rather than a genuine + * compile error. Typst prefixes these with the validator name(s), e.g. + * `PDF/UA-1 error: missing alt text`. + */ +export function isPdfStandardViolation(diagnostic: TypstDiagnostic): boolean { + return /^PDF\/[A-Za-z0-9.-]+(?:, ?PDF\/[A-Za-z0-9.-]+)* error:/.test( + diagnostic.message, + ); +} + +let wasmReady: Promise | undefined; + +export type TypstCompilerOptions = { + /** + * The wasm module source: a URL (string or `URL`) or the module bytes. + * Omitted, the module loads from this package's own files - bundlers + * (and plain browsers) resolve it next to the wasm-bindgen glue, so + * zero-config use involves no CDN. In runtimes without URL-relative + * loading (or to control caching), pass bytes or an explicit URL. Only + * the first `create()` call's value is used; the module is loaded once. + */ + wasm?: string | URL | Uint8Array; + /** + * Font files to load (every face of each TTF/OTF/TTC). Typst selects + * fonts by the family name embedded in each file's own name table - + * array order and file names play no role - so a document must reference + * the family exactly as the font declares it. {@link + * TypstCompiler.fontFamilies} lists the loaded names for verification, + * and a referenced-but-missing family surfaces as an + * `unknown font family` entry in the result's `compileWarnings`. + */ + fonts?: readonly Uint8Array[]; +}; + +/** + * A Typst-to-PDF compiler instance owning a font set. + * + * The wasm module embeds no fonts and never touches the network: text + * renders only with the supplied fonts (a document needing a missing font + * fails to compile - loudly, not with substituted glyphs). + */ +export class TypstCompiler { + private readonly inner: WasmCompiler; + + private constructor(inner: WasmCompiler) { + this.inner = inner; + } + + static async create( + options: TypstCompilerOptions = {}, + ): Promise { + wasmReady ??= initWasm( + options.wasm === undefined ? undefined : { module_or_path: options.wasm }, + ); + await wasmReady; + const compiler = new TypstCompiler(new WasmCompiler()); + for (const font of options.fonts ?? []) { + compiler.addFont(font); + } + return compiler; + } + + /** + * Load a font file; every face of a collection is added, byte-identical + * repeats are no-ops. Returns the number of faces added. + */ + addFont(data: Uint8Array): number { + return this.inner.add_font(data); + } + + /** + * The family names of the loaded fonts (deduplicated, in load order) - + * what Typst's `#set text(font: ...)` can reference. Useful to verify a + * supplied font carries the family name the document asks for. + */ + fontFamilies(): string[] { + return this.inner.font_families(); + } + + compilePdf( + source: string, + options: CompilePdfOptions = {}, + ): CompilePdfResult { + const paths = [...(options.assets?.keys() ?? [])]; + const data = [...(options.assets?.values() ?? [])]; + // Compile failure is an expected outcome, so the wasm layer *returns* + // it - one uniform payload for both outcomes. It only throws for + // caller mistakes (invalid options), which propagate as the genuine + // errors they are. + const { pdf, compileErrors, compileWarnings } = this.inner.compile_pdf( + source, + paths, + data, + { + pdfStandard: options.pdfStandard, + tagged: options.tagged, + creationTimestamp: options.creationTimestamp, + }, + ) as { + pdf?: Uint8Array; + compileErrors: TypstDiagnostic[]; + compileWarnings: TypstDiagnostic[]; + }; + if (pdf !== undefined && compileErrors.length === 0) { + return { pdf, compileWarnings }; + } + if (pdf === undefined && compileErrors.length > 0) { + return { error: "compile-failed", compileErrors, compileWarnings }; + } + // Output and errors must be mutually exclusive; anything else means + // the compiler misreported its outcome - a bug to fail loudly on. + throw new Error( + `TypstCompiler invariant violated: ${ + pdf !== undefined + ? "produced a PDF alongside errors" + : "produced neither a PDF nor errors" + }`, + ); + } +} diff --git a/packages/xl-typst-compiler/tsconfig.json b/packages/xl-typst-compiler/tsconfig.json new file mode 100644 index 0000000000..cee556c3db --- /dev/null +++ b/packages/xl-typst-compiler/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Bundler", + "types": ["node"], + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "declaration": true, + "declarationDir": "types", + "composite": true, + "skipLibCheck": true, + "emitDeclarationOnly": true + }, + "include": ["src"] +} diff --git a/packages/xl-typst-compiler/vite.config.ts b/packages/xl-typst-compiler/vite.config.ts new file mode 100644 index 0000000000..d33b1657f1 --- /dev/null +++ b/packages/xl-typst-compiler/vite.config.ts @@ -0,0 +1,60 @@ +import * as path from "path"; +import { webpackStats } from "rollup-plugin-webpack-stats"; +import { defineConfig, type UserConfig } from "vite-plus"; + +// https://vitejs.dev/config/ +export default defineConfig( + () => + ({ + run: { + tasks: { + build: { + // ensure-wasm builds pkg/ from rust/ when missing or stale + // (content-hashed; see scripts/ensure-wasm.mjs) - so every + // consumer of this task, including the Vercel builds, gets the + // wasm without a separate step. Requires a Rust toolchain + // locally (pinned in rust/rust-toolchain.toml). + command: "node scripts/ensure-wasm.mjs && tsc && vp build", + input: [ + { auto: true }, + { pattern: "!**/*.tsbuildinfo", base: "workspace" }, + { pattern: "rust/**", base: "workspace" }, + { pattern: "!rust/target/**", base: "workspace" }, + ], + // `types/**` must be declared too: a cache replay that restores + // only dist/ leaves consumers without declarations. pkg/** is + // produced by ensure-wasm and replayed with the rest. + output: ["dist/**", "types/**", "pkg/**", "!dist/*.tsbuildinfo"], + }, + }, + }, + test: { + environment: "node", + testTimeout: 30000, + }, + plugins: [webpackStats() as any], + build: { + sourcemap: true, + lib: { + entry: { + "blocknote-xl-typst-compiler": path.resolve( + __dirname, + "src/index.ts", + ), + }, + name: "blocknote-xl-typst-compiler", + formats: ["es", "cjs"], + fileName: (format, entryName) => + format === "es" ? `${entryName}.js` : `${entryName}.cjs`, + }, + rollupOptions: { + // The wasm-bindgen glue (self-referenced as .../pkg) ships as + // files next to dist/, not bundled: the glue locates the .wasm + // relative to its own URL, which bundling would break. + external: (source) => + source.startsWith("@blocknote/") || source.startsWith("node:"), + output: { globals: {} }, + }, + }, + }) as UserConfig, +); diff --git a/packages/xl-typst-exporter/package.json b/packages/xl-typst-exporter/package.json index e3d6fd56df..717b3c456f 100644 --- a/packages/xl-typst-exporter/package.json +++ b/packages/xl-typst-exporter/package.json @@ -50,13 +50,13 @@ }, "devDependencies": { "@blocknote/shared": "workspace:^", - "@myriaddreamin/typst-ts-node-compiler": "^0.8.0-rc3", "@types/jsdom": "^21.1.7", "@types/node": "22.13.13", "rimraf": "^5.0.10", "rollup-plugin-webpack-stats": "^0.2.6", "typescript": "^7.0.2", - "vite-plus": "catalog:" + "vite-plus": "catalog:", + "@blocknote/xl-typst-compiler": "workspace:^" }, "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" } diff --git a/packages/xl-typst-exporter/src/typstExporter.test.ts b/packages/xl-typst-exporter/src/typstExporter.test.ts index e6322b2d8e..3073d18bf2 100644 --- a/packages/xl-typst-exporter/src/typstExporter.test.ts +++ b/packages/xl-typst-exporter/src/typstExporter.test.ts @@ -68,7 +68,7 @@ describe("TypstExporter", () => { await expect(typ).toMatchFileSnapshot("__snapshots__/testDocument.typ"); }); - it("applies default document options when none are given", async () => { + it("fabricates no document metadata when none is given", async () => { const exporter = new TypstExporter(schema, typstDefaultSchemaMappings); const typ = await exporter.toTypst( @@ -77,10 +77,12 @@ describe("TypstExporter", () => { ]), ); - expect(typ).toContain('#set document(title: "Document", author: "")'); - expect(typ).toContain( - '#set text(font: "Inter 18pt", size: 12pt, lang: "en")', - ); + // No invented title/author/language - PDF/UA metadata requirements are + // enforced (and reported) by the PDF exporter's conformance gating, not + // papered over with defaults. + expect(typ).not.toContain("#set document("); + expect(typ).not.toContain("lang:"); + expect(typ).toContain('#set text(font: "Inter 18pt", size: 12pt)'); expect(typ).toContain('#show raw: set text(font: "Geist Mono")'); }); diff --git a/packages/xl-typst-exporter/src/typstExporter.ts b/packages/xl-typst-exporter/src/typstExporter.ts index ca38de9c1d..f289055ef5 100644 --- a/packages/xl-typst-exporter/src/typstExporter.ts +++ b/packages/xl-typst-exporter/src/typstExporter.ts @@ -49,6 +49,16 @@ export type TypstExporterOptions = ExporterOptions & { emojiFontFamily?: string; }; +/** + * The default body font family — what the bundled default fonts declare and + * the editor uses. Exported so a custom `fontFamily` fallback list can + * reference it without hardcoding, e.g. `[DEFAULT_FONT_FAMILY, "Noto Sans SC"]`. + */ +export const DEFAULT_FONT_FAMILY = "Inter 18pt"; + +/** The default code font family. See {@link DEFAULT_FONT_FAMILY}. */ +export const DEFAULT_MONO_FONT_FAMILY = "Geist Mono"; + /** * Per-document options applied at export time, passed to `toTypst(blocks, ...)`. * Mirrors how the other exporters take title/author/header/footer at the export @@ -56,18 +66,22 @@ export type TypstExporterOptions = ExporterOptions & { */ export type TypstDocumentOptions = { /** - * Document title — required for PDF/UA (also sets `DisplayDocTitle`). - * @default "Document" + * Document title, written to the PDF metadata (and shown in the viewer's + * title bar). PDF/UA-1 requires one — without it the PDF exporter + * produces a tagged but unclaimed document. No default: absent means no + * title is written. */ title?: string; /** - * Document author, written to PDF metadata. - * @default "" + * Document author, written to PDF metadata. No default. */ author?: string; /** - * BCP-47 language tag, e.g. "en". Sets the document's natural language. - * @default "en" + * BCP-47 language tag of the document's natural language, e.g. "en". + * The PDF exporter requires it when declaring PDF/UA-1 (the default) — + * a wrong language declaration is an accessibility defect no validator + * can catch, so it must be stated rather than defaulted. No default + * here: absent means the markup declares no language. */ lang?: string; /** @@ -157,8 +171,13 @@ export class TypstExporter< // `options.colors[name]` whose types promise a value. const newOptions = { ...options, - fontFamily: options?.fontFamily ?? "Inter 18pt", - monoFontFamily: options?.monoFontFamily ?? "Geist Mono", + // These family names must match what the PDF exporter's bundled + // default font files declare in their name tables (defaultFonts.ts). + // The pairing is pinned by pdfExporter.test.ts's "keeps the bundled + // default fonts in sync" test - changing a default here without the + // matching font file fails it. + fontFamily: options?.fontFamily ?? DEFAULT_FONT_FAMILY, + monoFontFamily: options?.monoFontFamily ?? DEFAULT_MONO_FONT_FAMILY, fontSize: options?.fontSize ?? 12, colors: options?.colors ?? COLORS_DEFAULT, // Proxy cross-origin image fetches so any host works in the browser, not @@ -439,9 +458,21 @@ export class TypstExporter< families.length === 1 ? strLit(families[0]) : `(${families.map(strLit).join(", ")})`; - const title = doc.title ?? "Document"; - const author = doc.author ?? ""; - const lang = doc.lang ?? "en"; + // No fabricated metadata: title, author, and language appear only when + // supplied. (PDF/UA requires a title and language - the PDF exporter + // gates its conformance claim on Typst's validation resp. the language + // check, rather than inventing values here.) + const documentArgs: string[] = []; + if (doc.title !== undefined) { + documentArgs.push(`title: ${strLit(doc.title)}`); + } + if (doc.author !== undefined) { + documentArgs.push(`author: ${strLit(doc.author)}`); + } + const textArgs = [`font: ${fontArg}`, `size: ${fontSize}pt`]; + if (doc.lang !== undefined) { + textArgs.push(`lang: ${strLit(doc.lang)}`); + } const { header, footer } = doc; // Default margins ≈ the editor's 8% horizontal padding (54px / 670px) // applied to A4. @@ -456,8 +487,10 @@ export class TypstExporter< pageArgs.push(`footer: [${footer}]`); } return [ - `#set document(title: ${strLit(title)}, author: ${strLit(author)})`, - `#set text(font: ${fontArg}, size: ${fontSize}pt, lang: ${strLit(lang)})`, + ...(documentArgs.length + ? [`#set document(${documentArgs.join(", ")})`] + : []), + `#set text(${textArgs.join(", ")})`, `#set page(${pageArgs.join(", ")})`, // Line height ≈ the editor's 1.5 (≈18pt at 12pt). Block spacing is applied // as *padding* on each block (see applyBlockProps), not margin — so a diff --git a/playground/package.json b/playground/package.json index 90177d990f..eb8b7400ed 100644 --- a/playground/package.json +++ b/playground/package.json @@ -63,8 +63,8 @@ "react-router-dom": "^6.30.1", "y-partykit": "^0.0.25", "@blocknote/xl-typst-exporter": "workspace:^", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3", - "tex2typst": "^0.6.2" + "tex2typst": "^0.6.2", + "@blocknote/xl-typst-compiler": "workspace:^" }, "devDependencies": { "@tailwindcss/vite": "^4.1.14", diff --git a/playground/src/examples.gen.tsx b/playground/src/examples.gen.tsx index 952e642754..2da413c0f0 100644 --- a/playground/src/examples.gen.tsx +++ b/playground/src/examples.gen.tsx @@ -1283,8 +1283,7 @@ export const examples = { "@blocknote/xl-multi-column": "latest", "@blocknote/math-block": "latest", "@blocknote/diagram-block": "latest", - "@myriaddreamin/typst.ts": "^0.8.0-rc3", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3", + "@blocknote/xl-typst-compiler": "latest", } as any, pro: true, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 038f24d9b8..d4bb3206da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@blocknote/xl-pdf-exporter': specifier: workspace:* version: link:../packages/xl-pdf-exporter + '@blocknote/xl-typst-compiler': + specifier: workspace:* + version: link:../packages/xl-typst-compiler '@blocknote/xl-typst-exporter': specifier: workspace:* version: link:../packages/xl-typst-exporter @@ -165,12 +168,6 @@ importers: '@mui/material': specifier: ^5.16.1 version: 5.18.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@myriaddreamin/typst-ts-web-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 - '@myriaddreamin/typst.ts': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3(@myriaddreamin/typst-ts-web-compiler@0.8.0-rc3) '@orama/orama': specifier: ^3.1.18 version: 3.1.18 @@ -3144,18 +3141,15 @@ importers: '@blocknote/xl-pdf-exporter': specifier: latest version: link:../../../packages/xl-pdf-exporter + '@blocknote/xl-typst-compiler': + specifier: latest + version: link:../../../packages/xl-typst-compiler '@mantine/core': specifier: ^9.0.2 version: 9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@mantine/hooks': specifier: ^9.0.2 version: 9.1.1(react@19.2.5) - '@myriaddreamin/typst-ts-web-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 - '@myriaddreamin/typst.ts': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3(@myriaddreamin/typst-ts-web-compiler@0.8.0-rc3) react: specifier: ^19.2.3 version: 19.2.5 @@ -5430,12 +5424,12 @@ importers: '@blocknote/xl-pdf-exporter': specifier: workspace:^ version: link:../xl-pdf-exporter + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../xl-typst-compiler '@blocknote/xl-typst-exporter': specifier: workspace:^ version: link:../xl-typst-exporter - '@myriaddreamin/typst-ts-node-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 '@react-email/components': specifier: ^1.0.12 version: 1.0.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -5564,12 +5558,12 @@ importers: '@blocknote/xl-pdf-exporter': specifier: workspace:^ version: link:../xl-pdf-exporter + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../xl-typst-compiler '@blocknote/xl-typst-exporter': specifier: workspace:^ version: link:../xl-typst-exporter - '@myriaddreamin/typst-ts-node-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 '@react-email/components': specifier: ^1.0.12 version: 1.0.12(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -6240,25 +6234,19 @@ importers: '@blocknote/xl-multi-column': specifier: workspace:^ version: link:../xl-multi-column + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../xl-typst-compiler '@blocknote/xl-typst-exporter': specifier: workspace:^ version: link:../xl-typst-exporter - '@cantoo/pdf-lib': - specifier: ^2.9.1 - version: 2.9.1(html-entities@2.6.0) - '@myriaddreamin/typst-ts-web-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 - '@myriaddreamin/typst.ts': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3(@myriaddreamin/typst-ts-web-compiler@0.8.0-rc3) devDependencies: '@blocknote/shared': specifier: workspace:^ version: link:../../shared - '@myriaddreamin/typst-ts-node-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 + '@cantoo/pdf-lib': + specifier: ^2.9.1 + version: 2.9.1(html-entities@2.6.0) '@react-pdf/renderer': specifier: ^4.5.1 version: 4.5.1(react@19.2.5) @@ -6296,6 +6284,27 @@ importers: specifier: 'catalog:' version: 0.2.9(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.10)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.6.0)(typescript@7.0.2))(terser@5.46.2)(tsx@4.21.0)(typescript@7.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + packages/xl-typst-compiler: + devDependencies: + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + rimraf: + specifier: ^5.0.10 + version: 5.0.10 + rollup-plugin-webpack-stats: + specifier: ^0.2.6 + version: 0.2.6(rollup@4.60.1) + typescript: + specifier: ^7.0.2 + version: 7.0.2 + vite-plus: + specifier: 'catalog:' + version: 0.2.9(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.10)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.1.0))(msw@2.11.5(@types/node@25.6.0)(typescript@7.0.2))(terser@5.46.2)(tsx@4.21.0)(typescript@7.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0) + wasm-pack: + specifier: ^0.15.0 + version: 0.15.0 + packages/xl-typst-exporter: dependencies: '@blocknote/core': @@ -6308,9 +6317,9 @@ importers: '@blocknote/shared': specifier: workspace:^ version: link:../../shared - '@myriaddreamin/typst-ts-node-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../xl-typst-compiler '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 @@ -6386,6 +6395,9 @@ importers: '@blocknote/xl-pdf-exporter': specifier: workspace:^ version: link:../packages/xl-pdf-exporter + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../packages/xl-typst-compiler '@blocknote/xl-typst-exporter': specifier: workspace:^ version: link:../packages/xl-typst-exporter @@ -6422,9 +6434,6 @@ importers: '@mui/material': specifier: ^5.18.0 version: 5.18.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@myriaddreamin/typst-ts-web-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 '@react-pdf/math': specifier: ^2.0.1 version: 2.0.1(@react-pdf/renderer@4.5.1(react@19.2.5))(react@19.2.5) @@ -6538,9 +6547,9 @@ importers: specifier: ^0.2.1 version: 0.2.2 devDependencies: - '@myriaddreamin/typst-ts-node-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../packages/xl-typst-compiler '@types/node': specifier: ^25.6.0 version: 25.6.0 @@ -6559,6 +6568,9 @@ importers: tests: dependencies: + '@blocknote/xl-typst-compiler': + specifier: workspace:^ + version: link:../packages/xl-typst-compiler get-port-please: specifier: 3.2.0 version: 3.2.0 @@ -6596,9 +6608,6 @@ importers: '@blocknote/xl-typst-exporter': specifier: workspace:^ version: link:../packages/xl-typst-exporter - '@myriaddreamin/typst-ts-web-compiler': - specifier: ^0.8.0-rc3 - version: 0.8.0-rc3 '@playwright/test': specifier: 1.60.0 version: 1.60.0 @@ -7874,6 +7883,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -8088,94 +8101,6 @@ packages: '@types/react': optional: true - '@myriaddreamin/typst-ts-node-compiler-android-arm-eabi@0.8.0-rc3': - resolution: {integrity: sha512-HNqXX1Pyt4BI7WetnrcCX+Yfz+K0L9I/Eklhko22+XSVz+OchPD4To8E2dOkIV1H3lC302OrnAoNcXWZpQc9Pw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - - '@myriaddreamin/typst-ts-node-compiler-android-arm64@0.8.0-rc3': - resolution: {integrity: sha512-PS1jRRGWxZ6Fuv8BgppgENdpQX1AfZaBwQq+81onkE53CoDpMTuD41UFjqdKrOos/BnCjrie/jjBWRVCt4NNNg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@myriaddreamin/typst-ts-node-compiler-darwin-arm64@0.8.0-rc3': - resolution: {integrity: sha512-PmJ11x/pYvg2pF8y3wVV92Tk3fjiGdxWk2sXuUXcwsebH4wr3UDb2bsPyVUE6deNJ6SrUafglKm1nnXf4QMe8w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@myriaddreamin/typst-ts-node-compiler-darwin-x64@0.8.0-rc3': - resolution: {integrity: sha512-DoYzkayAnad/x5I5eXW8YTkZVTpthzMKn1HtbwSFP87QCEecFFQFgSKGYpSfWPedNAc8ZMnTzk1XsuuE7s6SUA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf@0.8.0-rc3': - resolution: {integrity: sha512-6mNiQ5S+pTAJA6xiHtMMYjJWBa+YnpnawK2To3Y9UpzNkhgIipi1Nd0m+E6lQsqFFwjlYR/0EPlgcpQQOzP+Fg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu@0.8.0-rc3': - resolution: {integrity: sha512-+c8a+W+mcLH6SGFh99HKiiNUEIQB7pZ3CyyFsl94IIuq+MqOAI9uEYPzauKsIsbDc9ZsWC9rie2wSYQHLvmNSg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl@0.8.0-rc3': - resolution: {integrity: sha512-bEYNf4eiyJqOo1AO/WRacRoXiKvJ60KHb3zItETR50GwV9bck+rhSwzLfWO9VsckKMPdWeovSVLcHkl9IHvMJA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu@0.8.0-rc3': - resolution: {integrity: sha512-bqqb2NCCytrrI4GD6LST7FIM3jsyBPRqZ9LtPFi7IGLEHPokWN1PkuZtbwJ4mxb3/eD7ovd5G6Qyv2WDqesW2Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@myriaddreamin/typst-ts-node-compiler-linux-x64-musl@0.8.0-rc3': - resolution: {integrity: sha512-uqjicMMq+ePQxsmjwQyGXYv7WK6DvGMfCSKzuv1jHLVFxD8BQFJ5UXWgMaOopCdRkFL1SdUzzipGaeo/8GQkpw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc@0.8.0-rc3': - resolution: {integrity: sha512-O2pAiIAKMMV+lRGSE1a+EqCJUWc+Gfi4zyo9OXTXeCnpzICp4P4vIUrjU6R784gJjuG+p4tQP+n3PRw81mkbIA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc@0.8.0-rc3': - resolution: {integrity: sha512-G/uP+tlOZLx0OXXO4CEqo2D/ewHc8uBqmGD5p6f+LX3SBbRSqB6CosDS7PCfh2tehfdC7HNXyLtey9s2g4Xg9w==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@myriaddreamin/typst-ts-node-compiler@0.8.0-rc3': - resolution: {integrity: sha512-UV/LynOjsoXvbPigusm7IGusu22cENI5UDGdMFWK6B6E0F74r9S+TnY/TAGN3S6PvJvpgMTo9Y/MvmqDlgzB8w==} - engines: {node: '>= 10'} - - '@myriaddreamin/typst-ts-web-compiler@0.8.0-rc3': - resolution: {integrity: sha512-akc8TlhBzldlU1DtbDvR/6ufzgJmQrZMqfYY2fCFbH7y/+h3YdmnyF+twtVqwv+g6YuFXm/Oc1wwa6cYQX4/aw==} - - '@myriaddreamin/typst.ts@0.8.0-rc3': - resolution: {integrity: sha512-Z5SzwBRNV3a8Zz6BCdUBnAKRQj2khNM90dhwFpGkefIYiFJ5AUj/F02uoyDZmY2TvziDKEsJn0800BeF7J8mAw==} - peerDependencies: - '@myriaddreamin/typst-ts-renderer': ^0.8.0-rc3 - '@myriaddreamin/typst-ts-web-compiler': ^0.8.0-rc3 - peerDependenciesMeta: - '@myriaddreamin/typst-ts-renderer': - optional: true - '@myriaddreamin/typst-ts-web-compiler': - optional: true - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -12309,6 +12234,10 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -13030,6 +12959,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -13578,9 +13508,6 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - idb@7.1.1: - resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -14545,6 +14472,10 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mj-context-menu@0.6.1: resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} @@ -15989,6 +15920,10 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + terser-webpack-plugin@5.5.0: resolution: {integrity: sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==} engines: {node: '>= 10.13.0'} @@ -16452,6 +16387,11 @@ packages: engines: {node: '>=20.0.0'} hasBin: true + wasm-pack@0.15.0: + resolution: {integrity: sha512-DdqtGWc3+iFx+7lL7QU5LBWs7qMnwQSWxF0htSfE15sNa3roVwHjAkTm2JXgueU2GGfSwNqbq2EzyC2b/biKDA==} + engines: {node: '>=16'} + hasBin: true + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -16663,6 +16603,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml@1.10.3: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} @@ -18092,6 +18036,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -18385,61 +18333,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@myriaddreamin/typst-ts-node-compiler-android-arm-eabi@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-android-arm64@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-darwin-arm64@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-darwin-x64@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-linux-x64-musl@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc@0.8.0-rc3': - optional: true - - '@myriaddreamin/typst-ts-node-compiler@0.8.0-rc3': - optionalDependencies: - '@myriaddreamin/typst-ts-node-compiler-android-arm-eabi': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-android-arm64': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-darwin-arm64': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-darwin-x64': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-linux-arm-gnueabihf': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-linux-arm64-gnu': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-linux-arm64-musl': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-linux-x64-gnu': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-linux-x64-musl': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-win32-arm64-msvc': 0.8.0-rc3 - '@myriaddreamin/typst-ts-node-compiler-win32-x64-msvc': 0.8.0-rc3 - - '@myriaddreamin/typst-ts-web-compiler@0.8.0-rc3': {} - - '@myriaddreamin/typst.ts@0.8.0-rc3(@myriaddreamin/typst-ts-web-compiler@0.8.0-rc3)': - dependencies: - idb: 7.1.1 - optionalDependencies: - '@myriaddreamin/typst-ts-web-compiler': 0.8.0-rc3 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -22559,6 +22452,8 @@ snapshots: chownr@1.1.4: {} + chownr@3.0.0: {} + chrome-trace-event@1.0.4: {} citty@0.1.6: @@ -24007,8 +23902,6 @@ snapshots: dependencies: safer-buffer: 2.1.2 - idb@7.1.1: {} - ieee754@1.2.1: {} ignore@5.3.2: {} @@ -25152,6 +25045,10 @@ snapshots: minipass@7.1.3: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mj-context-menu@0.6.1: {} mkdirp-classic@0.5.3: {} @@ -26878,6 +26775,14 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + terser-webpack-plugin@5.5.0(esbuild@0.27.5)(webpack@5.105.4(esbuild@0.27.5)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -27412,6 +27317,10 @@ snapshots: transitivePeerDependencies: - debug + wasm-pack@0.15.0: + dependencies: + tar: 7.5.22 + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -27641,6 +27550,8 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + yaml@1.10.3: {} yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ea1d7dca67..b88fc33bb0 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -67,6 +67,8 @@ allowBuilds: sharp: false workerd: false leveldown: false + # wasm-pack's binary shim self-installs on first run; no install script. + wasm-pack: false patchedDependencies: { "@y/prosemirror@2.0.0-6": patches/@y__prosemirror@2.0.0-6.patch, diff --git a/shared/package.json b/shared/package.json index d437720690..e85a8d23ca 100644 --- a/shared/package.json +++ b/shared/package.json @@ -20,13 +20,13 @@ "typescript": "^7.0.2", "vite-plus": "catalog:", "xml-formatter": "^3.6.7", - "@myriaddreamin/typst-ts-node-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-compiler": "workspace:^" }, "peerDependencies": { "@zip.js/zip.js": "^2.8.8", "image-meta": "^0.2.1", "xml-formatter": "^3.6.7", - "@myriaddreamin/typst-ts-node-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-compiler": "workspace:^" }, "peerDependenciesMeta": { "@zip.js/zip.js": { @@ -38,7 +38,7 @@ "xml-formatter": { "optional": true }, - "@myriaddreamin/typst-ts-node-compiler": { + "@blocknote/xl-typst-compiler": { "optional": true } } diff --git a/shared/util/binaryFileSnapshotUtil.ts b/shared/util/binaryFileSnapshotUtil.ts deleted file mode 100644 index 2e252a50d4..0000000000 --- a/shared/util/binaryFileSnapshotUtil.ts +++ /dev/null @@ -1,94 +0,0 @@ -import fs from "fs"; -import path from "path"; -const NAME_WORKER_STATE = "__vitest_worker__"; - -// from https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/runtime/utils.ts#L8 -export function getWorkerState(): any { - // @ts-expect-error untyped global - const workerState = globalThis[NAME_WORKER_STATE]; - if (!workerState) { - const errorMsg = - "Vitest failed to access its internal state." + - "\n\nOne of the following is possible:" + - '\n- "vitest" is imported directly without running "vitest" command' + - '\n- "vitest" is imported inside "globalSetup" (to fix this, use "setupFiles" instead, because "globalSetup" runs in a different context)' + - "\n- Otherwise, it might be a Vitest bug. Please report it to https://github.com/vitest-dev/vitest/issues\n"; - throw new Error(errorMsg); - } - return workerState; -} - -function getSnapshotOptions() { - const option: "new" | "all" | "none" = - getWorkerState().ctx.config.snapshotOptions.updateSnapshot; - return option; -} - -// async function createMD5FromBuffer(buffer: Buffer) { -// const hash = crypto.createHash("md5"); -// hash.update(buffer); -// return hash.digest("hex"); -// } - -// async function createMD5FromFile(filePath: string) { -// const hash = crypto.createHash("md5"); - -// if (!fs.existsSync(filePath)) { -// return undefined; -// } - -// await pipeline(fs.createReadStream(filePath), async function (source) { -// for await (const chunk of source) { -// hash.update(chunk); -// } -// }); - -// return hash.digest("hex"); -// } - -export async function toMatchBinaryFileSnapshot( - buffer: Buffer, - filepath: string, - options: { - /** - * On a mismatch that fails the test, also write the received bytes next - * to the baseline (as `.actual.`) so they can be inspected / - * diffed - useful for image snapshots. - */ - writeActualOnMismatch?: boolean; - } = {}, -) { - const fileBuffer = fs.existsSync(filepath) - ? fs.readFileSync(filepath) - : undefined; - - const same = fileBuffer && buffer.equals(fileBuffer); // && bufferMD5 === fileMD5; - - const option = getSnapshotOptions(); - - if (same) { - return; - } - - if (option === "none" || (option === "new" && fileBuffer !== undefined)) { - if (options.writeActualOnMismatch) { - // `name.ext` -> `name.actual.ext`; an extension-less path gets a plain - // `.actual` suffix (the no-match case must never fall through to the - // baseline path itself). - const actualPath = /\.[^./]+$/.test(filepath) - ? filepath.replace(/(\.[^./]+)$/, ".actual$1") - : `${filepath}.actual`; - // A missing baseline may mean its directory is missing too - the - // diagnostic write must not itself throw ENOENT and mask the snapshot - // mismatch below. - fs.mkdirSync(path.dirname(actualPath), { recursive: true }); - fs.writeFileSync(actualPath, buffer); - } - throw new Error(`${filepath} not matching `); - } - - // create dir if not exists - fs.mkdirSync(path.dirname(filepath), { recursive: true }); - - fs.writeFileSync(filepath, buffer); -} diff --git a/shared/util/typstTestUtil.ts b/shared/util/typstTestUtil.ts index a8acdb165b..78e04fc585 100644 --- a/shared/util/typstTestUtil.ts +++ b/shared/util/typstTestUtil.ts @@ -1,27 +1,59 @@ -import { resolve } from "node:path"; +import { readFileSync } from "node:fs"; +import { join, resolve } from "node:path"; -type NodeCompilerInstance = - import("@myriaddreamin/typst-ts-node-compiler").NodeCompiler; +type TypstCompilerInstance = + import("@blocknote/xl-typst-compiler").TypstCompiler; -// One shared compiler for the common (default-font) case - creating a -// NodeCompiler scans system fonts, which would dominate test time if every -// compile created its own. Its shadow files are reset per call; a call with -// custom `fontBlobs` gets a throwaway compiler instead, since fonts are -// fixed at creation. -let defaultCompiler: NodeCompilerInstance | undefined; +// The compiler wasm, loaded from the workspace package's build output. Tests +// need `pnpm --filter @blocknote/xl-typst-compiler run build:wasm` to have +// run; failing loudly beats a cryptic resolve error. +function wasmBytes(): Uint8Array { + const path = resolve( + __dirname, + "../../packages/xl-typst-compiler/pkg/blocknote_typst_wasm_bg.wasm", + ); + try { + return new Uint8Array(readFileSync(path)); + } catch { + throw new Error( + `Missing compiler wasm at ${path} - build it first: ` + + "pnpm --filter @blocknote/xl-typst-compiler run build:wasm", + ); + } +} + +// The default font set for tests: the shared assets the exporters reference. +// Unlike the previous node-compiler setup (which scanned *system* fonts), +// this is deterministic across machines - a test render can only use fonts +// that are checked into the repo. +function defaultFontBlobs(): Uint8Array[] { + const fontDir = resolve(__dirname, "../assets/fonts"); + return [ + "inter/Inter_18pt-Regular.ttf", + "inter/Inter_18pt-Italic.ttf", + "inter/Inter_18pt-Bold.ttf", + "inter/Inter_18pt-BoldItalic.ttf", + "GeistMono-Regular.ttf", + "newcm/NewCMMath-Regular.otf", + "newcm/NewCMMath-Book.otf", + "noto/Noto-COLRv1.ttf", + ].map((f) => new Uint8Array(readFileSync(join(fontDir, f)))); +} + +// One shared compiler for the common (default-font) case - font parsing +// would otherwise repeat per compile. A call with custom `fontBlobs` gets +// its own compiler, since an instance's font set only grows. +let defaultCompiler: TypstCompilerInstance | undefined; /** - * Compiles Typst source with the node compiler for tests, mapping an - * exporter's collected `assetFiles` into the compiler first. - * - * The node compiler resolves a project-absolute Typst path (`/assets/..`) - * against the cwd, so each shadow file is keyed by that resolved absolute - * path (the browser pipeline in `compileBrowser.ts` uses the virtual path - * directly). + * Compiles Typst source for tests, mapping an exporter's collected + * `assetFiles` into the compiler first. Fonts default to the shared test + * font set (Inter, Geist Mono, New CM Math, Noto Color Emoji). * - * Pass `pdfStandard: "ua-1"` to run Typst's own PDF/UA-1 validation - it - * errors on accessibility violations (e.g. images or equations without alt - * text), making the compile itself a conformance gate. + * Pass `pdfStandard: "ua-1"` to produce a *declared* PDF/UA-1 - Typst + * validates conformance at compile time and this helper throws on + * violations (e.g. images or equations without alt text), making the + * compile itself a conformance gate. */ export async function compileTypstForTesting( typst: string, @@ -29,32 +61,37 @@ export async function compileTypstForTesting( assets?: ReadonlyMap; pdfStandard?: string; creationTimestamp?: number; - fontBlobs?: Buffer[]; + fontBlobs?: Uint8Array[]; } = {}, -): Promise { - const { NodeCompiler } = - await import("@myriaddreamin/typst-ts-node-compiler"); - let compiler: NodeCompilerInstance; - if (options.fontBlobs?.length) { - compiler = NodeCompiler.create({ - fontArgs: [{ fontBlobs: options.fontBlobs }], +): Promise { + const { TypstCompiler } = await import("@blocknote/xl-typst-compiler"); + let compiler: TypstCompilerInstance; + // Truthiness on the array (not its length): an explicit `[]` means "a + // font-less compiler", not "the defaults". + if (options.fontBlobs) { + compiler = await TypstCompiler.create({ + wasm: wasmBytes(), + fonts: options.fontBlobs, }); } else { - defaultCompiler ??= NodeCompiler.create(); + defaultCompiler ??= await TypstCompiler.create({ + wasm: wasmBytes(), + fonts: defaultFontBlobs(), + }); compiler = defaultCompiler; - compiler.resetShadow(); } - for (const [path, bytes] of options.assets ?? []) { - compiler.mapShadow( - resolve(process.cwd(), path.replace(/^\/+/, "")), - Buffer.from(bytes), + const result = compiler.compilePdf(typst, { + assets: options.assets, + pdfStandard: options.pdfStandard, + creationTimestamp: options.creationTimestamp, + }); + if (result.error) { + // Tests want failures loud; production code uses the typed result. + throw new Error( + `Typst compile failed:\n${result.compileErrors + .map((d) => d.message) + .join("\n")}`, ); } - return compiler.pdf( - { mainFileContent: typst }, - { - pdfStandard: options.pdfStandard, - creationTimestamp: options.creationTimestamp, - }, - ); + return result.pdf; } diff --git a/tests/Dockerfile b/tests/Dockerfile index f19e9428c4..6805569494 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -41,7 +41,23 @@ COPY patches ./patches # enough — pnpm still runs the docs `postinstall` lifecycle script, which fails # in fumadocs-mdx. SKIP_DOCS_POSTINSTALL makes that script a no-op (see # docs/package.json) without disabling the native build scripts other deps need. -RUN SKIP_DOCS_POSTINSTALL=1 pnpm install --frozen-lockfile --filter '!docs' +# BuildKit cache mounts persist the pnpm store (package tarballs) and cache +# (registry metadata, which the supply-chain policy checks hit per package) +# across image rebuilds: after a dependency change, the reinstall only +# fetches what actually changed instead of the whole workspace store. The +# store lives on a separate mount, so pnpm copies instead of hardlinking - +# still far cheaper than downloading. The first build after adding this +# seeds the caches at full cost. +# minimumReleaseAge is disabled here: the policy re-verifies every package +# against live registry metadata (2000+ requests), which is both the slow +# part of this step and flaky under Docker Desktop's NAT - and the frozen +# lockfile being installed was already verified against the policy by the +# local install that produced it (and by CI). A throwaway test image +# re-checking it buys nothing. +RUN --mount=type=cache,id=pnpm-store,target=/pnpm-store \ + --mount=type=cache,id=pnpm-cache,target=/root/.cache/pnpm \ + SKIP_DOCS_POSTINSTALL=1 pnpm install --frozen-lockfile --filter '!docs' \ + --store-dir /pnpm-store --config.minimumReleaseAge=0 # Bake in the example apps: the tests import them and vite transpiles them from # source at run time. They sit after the install layer, so editing an example diff --git a/tests/docker-run.sh b/tests/docker-run.sh index 4cc2fd708b..8748043482 100755 --- a/tests/docker-run.sh +++ b/tests/docker-run.sh @@ -65,6 +65,19 @@ mounts=() for src in packages/*/src; do mounts+=(-v "$PWD/$src:/work/$src") done +# xl-typst-compiler is consumed through its build outputs (see +# vite.config.browser.ts): the wasm + glue in pkg/ and the built TS wrapper. +# Build them first (build:wasm + build) - they are gitignored, so the image +# cannot contain them. +for out in pkg dist types; do + if [ ! -d "packages/xl-typst-compiler/$out" ]; then + echo "packages/xl-typst-compiler/$out is missing - build it first:" >&2 + echo " pnpm exec vp run --filter @blocknote/xl-typst-compiler build" >&2 + echo "(compiles the Rust wasm too when needed - requires rustup)" >&2 + exit 1 + fi + mounts+=(-v "$PWD/packages/xl-typst-compiler/$out:/work/packages/xl-typst-compiler/$out") +done # The test files and browser config (callers iterate on these too). mounts+=( -v "$PWD/tests/src:/work/tests/src" diff --git a/tests/package.json b/tests/package.json index 31599a2dff..7a9ef2573b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -38,10 +38,10 @@ "rimraf": "^5.0.10", "vite-plus": "catalog:", "vitest-browser-react": "^2.2.0", - "@blocknote/xl-typst-exporter": "workspace:^", - "@myriaddreamin/typst-ts-web-compiler": "^0.8.0-rc3" + "@blocknote/xl-typst-exporter": "workspace:^" }, "dependencies": { - "get-port-please": "3.2.0" + "get-port-please": "3.2.0", + "@blocknote/xl-typst-compiler": "workspace:^" } } diff --git a/tests/scripts/install-pdf-tooling.sh b/tests/scripts/install-pdf-tooling.sh deleted file mode 100755 index 3b4582e60c..0000000000 --- a/tests/scripts/install-pdf-tooling.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# Installs the tooling the xl-pdf-exporter unit tests gate on: -# - pdftoppm (poppler) rasterizes the per-page visual snapshots; the test -# itself runs it through a digest-pinned container (see pdfua.test.ts) so -# the byte-exact PNG baselines can't drift with the host's poppler - this -# script only pre-pulls that image so the pull cost isn't paid mid-test. -# - veraPDF validates PDF/UA-1 conformance; it comes from its official -# container image behind a PATH shim, so tests invoke plain `verapdf` -# (locally, `brew install verapdf` provides the same tool natively). -# Used by CI (see .github/workflows). -# -# Both images are PINNED by digest: rasterizer upgrades change the rendered -# bytes and validator upgrades can change conformance verdicts, so either -# should land as a deliberate diff, not ambient CI drift. The veraPDF image -# currently publishes only dev-line tags (no stable releases) - the digest -# below is v1.31.156; when veraPDF starts tagging stable releases, repoint -# to one. -set -euo pipefail - -VERAPDF_IMAGE="ghcr.io/verapdf/cli@sha256:65583906f9abb4683242cd605c4317e821e60f3e1ee418d00db87b83f11f54e7" # v1.31.156 - -# The poppler image is defined in pdfua.test.ts (single source of truth, next -# to the baselines it renders); grep it out rather than duplicating the digest. -ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -POPPLER_IMAGE="$(grep -o 'minidocks/poppler@sha256:[0-9a-f]*' \ - "$ROOT/packages/xl-pdf-exporter/src/pdfua/pdfua.test.ts" | head -1)" -test -n "$POPPLER_IMAGE" - -docker pull -q "$VERAPDF_IMAGE" -docker pull -q --platform linux/amd64 "$POPPLER_IMAGE" - -# A PATH shim so tests can invoke plain `verapdf `. The image's -# entrypoint is the verapdf CLI (workdir /data, non-root user), so a trailing -# file argument is copied into a world-readable temp dir and mounted there - -# the container's uid can't read the caller's 0700 temp dirs directly. -sudo tee /usr/local/bin/verapdf >/dev/null < \`; only that shape gets the -# copy-and-mount treatment. Requiring the .pdf suffix (not just an existing -# file) keeps a trailing file-valued option (e.g. \`--policyfile x.sch\`) -# from being mounted as the input, and the \$# guard keeps a zero-argument -# call (e.g. bare \`verapdf\`) from tripping over an empty array subscript. -last="" -if [ "\$#" -gt 0 ]; then - last="\${args[\$# - 1]}" -fi -if [[ "\$last" == *.pdf && -f "\$last" ]]; then - tmp="\$(mktemp -d)" - trap 'rm -rf "\$tmp"' EXIT - chmod 755 "\$tmp" - cp "\$last" "\$tmp/input.pdf" - chmod 644 "\$tmp/input.pdf" - unset "args[\$# - 1]" - # No exec: the EXIT trap must still run to clean up the temp dir; set -e - # exits with docker's status code, so non-conformance still propagates. - docker run --rm -v "\$tmp":/data:ro "\$IMAGE" \${args[@]+"\${args[@]}"} /data/input.pdf - exit 0 -fi -# \${args[@]+...} keeps an empty array from tripping set -u on old bash. -exec docker run --rm "\$IMAGE" \${args[@]+"\${args[@]}"} -SHIM -sudo chmod +x /usr/local/bin/verapdf - -# Smoke-check both tools so a broken install fails here, not inside a test. -verapdf --version -docker run --rm --platform linux/amd64 "$POPPLER_IMAGE" pdftoppm -v diff --git a/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-1-chromium-linux.png b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-1-chromium-linux.png new file mode 100644 index 0000000000..c52ab39088 Binary files /dev/null and b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-1-chromium-linux.png differ diff --git a/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-2-chromium-linux.png b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-2-chromium-linux.png new file mode 100644 index 0000000000..e46e4ab9d0 Binary files /dev/null and b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-2-chromium-linux.png differ diff --git a/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-3-chromium-linux.png b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-3-chromium-linux.png new file mode 100644 index 0000000000..cb737d35b7 Binary files /dev/null and b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-3-chromium-linux.png differ diff --git a/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-4-chromium-linux.png b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-4-chromium-linux.png new file mode 100644 index 0000000000..10c511a3d3 Binary files /dev/null and b/tests/src/end-to-end/exporters/__screenshots__/exporterImages.test.tsx/typst-pdf-page-4-chromium-linux.png differ diff --git a/tests/src/end-to-end/exporters/exporterImages.test.tsx b/tests/src/end-to-end/exporters/exporterImages.test.tsx index 7534326311..4bf711b2f3 100644 --- a/tests/src/end-to-end/exporters/exporterImages.test.tsx +++ b/tests/src/end-to-end/exporters/exporterImages.test.tsx @@ -19,20 +19,21 @@ import { reactEmailDefaultSchemaMappings, } from "@blocknote/xl-email-exporter"; import { - compileTypstToTaggedPdf, - declarePdfUA, + compileTypstToPdf, TypstExporter, typstDefaultSchemaMappings, } from "@blocknote/xl-pdf-exporter"; -// Bundled wasm + a body font so the Typst compile below runs fully offline. +// Bundled wasm + fonts so the Typst compile below runs fully offline. // eslint-disable-next-line import/no-unresolved -import compilerWasmUrl from "@myriaddreamin/typst-ts-web-compiler/wasm?url"; +import compilerWasmUrl from "@blocknote/xl-typst-compiler/wasm?url"; // eslint-disable-next-line import/no-unresolved import interRegularUrl from "@shared/assets/fonts/inter/Inter_18pt-Regular.ttf?url"; -// Typst needs a math-capable font for equations; with `preloadDefaultFonts: -// false` (no CDN fonts) it must be bundled like the body font. +// Typst needs a math-capable font for equations and an emoji font for the +// document's emoji (the compiler ships no fonts; missing glyphs would fail +// PDF/UA-1 validation as `.notdef`). import newCMMathBookUrl from "@shared/assets/fonts/newcm/NewCMMath-Book.otf?url"; import newCMMathRegularUrl from "@shared/assets/fonts/newcm/NewCMMath-Regular.otf?url"; +import notoEmojiUrl from "@shared/assets/fonts/noto/Noto-COLRv1.ttf?url"; import { PDFExporter, pdfDefaultSchemaMappings, @@ -160,37 +161,70 @@ describe("email export through a complete exporter in the browser", () => { }); }); +// Builds the exporter, converts the shared document (plus invalid math / +// diagram placeholders), and compiles it natively as PDF/UA-1 through the +// real wasm pipeline. Shared by the assertion test and the visual test. +async function compileTypstUaPdf() { + const typstDocument = [ + ...testDocumentWithSourceBlocks, + invalidDiagramBlock, + invalidMathBlock, + ]; + + const exporter = new TypstExporter( + schema(), + { + ...typstDefaultSchemaMappings, + blockMapping: { + ...typstDefaultSchemaMappings.blockMapping, + mathBlock: typstMathBlockMapping, + diagram: typstDiagramBlockMapping, + }, + inlineContentMapping: { + ...typstDefaultSchemaMappings.inlineContentMapping, + math: typstInlineMathMapping, + }, + } as any, + { resolveFileUrl: testResolveFileUrl }, + ); + + const typ = await exporter.toTypst( + typstDocument as any, + { + title: "BlockNote Export", + lang: "en", + // Fixed timestamp: the visual baselines rely on deterministic bytes. + creationTimestamp: 1_700_000_000, + } as any, + ); + + const fonts = await Promise.all( + [interRegularUrl, newCMMathRegularUrl, newCMMathBookUrl, notoEmojiUrl].map( + async (url) => new Uint8Array(await (await fetch(url)).arrayBuffer()), + ), + ); + const result = await compileTypstToPdf(typ, { + wasm: new URL(compilerWasmUrl, document.baseURI), + fonts, + assets: exporter.assetFiles, + pdfStandard: "ua-1", + creationTimestamp: 1_700_000_000, + }); + if (result.error) { + throw new Error(result.compileErrors.map((d) => d.message).join(" | ")); + } + return { typ, exporter, pdf: result.pdf }; +} + describe("pdf/ua export through the complete typst pipeline in the browser", () => { test( "exports the shared document with math and diagrams", { timeout: 60000 }, async () => { - const typstDocument = [ - ...testDocumentWithSourceBlocks, - invalidDiagramBlock, - invalidMathBlock, - ]; - - const exporter = new TypstExporter( - schema(), - { - ...typstDefaultSchemaMappings, - blockMapping: { - ...typstDefaultSchemaMappings.blockMapping, - mathBlock: typstMathBlockMapping, - diagram: typstDiagramBlockMapping, - }, - inlineContentMapping: { - ...typstDefaultSchemaMappings.inlineContentMapping, - math: typstInlineMathMapping, - }, - } as any, - { resolveFileUrl: testResolveFileUrl }, - ); - - const typ = await exporter.toTypst(typstDocument as any, { - title: "BlockNote Export", - }); + // Compiling through the real wasm pipeline is what only the browser + // suite covers: bundler-served wasm loading (the node unit suites + // drive the same wasm from bytes). + const { typ, exporter, pdf } = await compileTypstUaPdf(); // Native equations with the LaTeX source as alt text, the diagram as a // figure with the Mermaid source as alt text - and the invalid sources @@ -211,23 +245,55 @@ describe("pdf/ua export through the complete typst pipeline in the browser", () ).toBe(true); expect(assetBytes.some((b) => b[0] === 0xff && b[1] === 0xd8)).toBe(true); - // Compile through the real wasm pipeline (the node unit suites - // substitute the node compiler; only this covers what browsers run), - // then declare PDF/UA - the same steps PDFExporter.toBytes composes. - const fonts = await Promise.all( - [interRegularUrl, newCMMathRegularUrl, newCMMathBookUrl].map( - async (url) => new Uint8Array(await (await fetch(url)).arrayBuffer()), - ), + expect(isPdf(pdf)).toBe(true); + expect(pdf.byteLength).toBeGreaterThan(10_000); + // Natively declared and validated by the compile itself. + expect(new TextDecoder("latin1").decode(pdf).includes("pdfuaid")).toBe( + true, ); - const tagged = await compileTypstToTaggedPdf(typ, { - getModule: () => compilerWasmUrl, - fonts, - preloadDefaultFonts: false, - assets: exporter.assetFiles, - }); - const ua = await declarePdfUA(tagged); - expect(isPdf(ua)).toBe(true); - expect(ua.byteLength).toBeGreaterThan(10_000); + }, + ); + + // Chromium only, like the react-pdf visual test below: the PDF is the + // same bytes everywhere (Typst lays it out from the supplied fonts, not + // browser rendering), so per-browser runs would only re-test pdf.js's + // rasterizer at 3x the suite cost. + test.skipIf(browserName !== "chromium")( + "matches the per-page visual snapshot", + // Cold wasm load + compile + rasterizing every page needs more headroom + // than the other tests on shared CI runners. + { timeout: 120000 }, + async () => { + const { pdf } = await compileTypstUaPdf(); + + // Render the produced PDF's pages with pdf.js and screenshot each + // page as a visual regression of the actual export (the same + // approach as the react-pdf test below). + const pdfjs = await import("pdfjs-dist"); + const workerUrl = ( + await import("pdfjs-dist/build/pdf.worker.min.mjs?url" as string) + ).default; + pdfjs.GlobalWorkerOptions.workerSrc = workerUrl; + + const parsed = await pdfjs.getDocument({ data: pdf.slice() }).promise; + // The test document contains a page break. + expect(parsed.numPages).toBeGreaterThanOrEqual(2); + + const frame = createExportFrame("fit-content"); + for (let n = 1; n <= parsed.numPages; n++) { + const pdfPage = await parsed.getPage(n); + const viewport = pdfPage.getViewport({ scale: 1 }); + const canvas = document.createElement("canvas"); + canvas.width = viewport.width; + canvas.height = viewport.height; + canvas.style.display = "block"; + await pdfPage.render({ + canvasContext: canvas.getContext("2d")!, + viewport, + } as any).promise; + frame.replaceChildren(canvas); + await screenshotFull(frame, `typst-pdf-page-${n}`); + } }, ); }); diff --git a/tests/src/end-to-end/static/__screenshots__/static.test.tsx/static-rendering-equality-chromium-linux.png b/tests/src/end-to-end/static/__screenshots__/static.test.tsx/static-rendering-equality-chromium-linux.png index bb98426953..544a0c0d37 100644 Binary files a/tests/src/end-to-end/static/__screenshots__/static.test.tsx/static-rendering-equality-chromium-linux.png and b/tests/src/end-to-end/static/__screenshots__/static.test.tsx/static-rendering-equality-chromium-linux.png differ diff --git a/tests/vite.config.browser.ts b/tests/vite.config.browser.ts index 21fb2a1e1b..b030d2f8fe 100644 --- a/tests/vite.config.browser.ts +++ b/tests/vite.config.browser.ts @@ -40,7 +40,12 @@ const blockNoteSrcAliases = Object.fromEntries( path.join(dir, "src"), ] as const, ) - .filter(([name]) => name?.startsWith("@blocknote/")), + .filter(([name]) => name?.startsWith("@blocknote/")) + // xl-typst-compiler resolves via its package exports instead: its `/pkg` + // and `/wasm` subpaths point at wasm build outputs that live next to + // (not under) src/, which the prefix-replace alias would misdirect. + // docker-run.sh mounts its dist/types/pkg alongside src for this. + .filter(([name]) => name !== "@blocknote/xl-typst-compiler"), ); // The e2e suite is designed to run in a fixed Linux environment (the Playwright