Skip to content

fix(typst): emit compilable mermaid embeds and add body-only mermaid API#70

Merged
sc2ben merged 5 commits into
mainfrom
fix/typst-mermaid-image-emit
Jul 3, 2026
Merged

fix(typst): emit compilable mermaid embeds and add body-only mermaid API#70
sc2ben merged 5 commits into
mainfrom
fix/typst-mermaid-image-emit

Conversation

@sc2ben

@sc2ben sc2ben commented Jul 3, 2026

Copy link
Copy Markdown
Member

Problem

The Typst renderer emits #image.decode(bytes.fromBase64("…")) for mermaid blocks. bytes.fromBase64 does not exist in Typst (and image.decode is deprecated), so every document containing a rendered mermaid diagram fails typst compile with a hard error:

error: type bytes does not contain field `fromBase64`

The mermaid tests only assert the emitted string — nothing ever ran the output through a real typst, which is how this slipped through.

Fix

  • Embed the SVG source directly in a Typst string literal: image(bytes("<svg…>"), format: "svg"), using the existing writeStringLiteral escaping (" and \; raw newlines are legal in Typst strings). Verified against typst 0.14.2, including multi-line pozeiden output.
  • Size diagrams naturally instead of width: 100%: a layout/measure wrapper renders at intrinsic size and scales down only when wider than the line width. Previously a 4-node flowchart blew up to full text width and sprawled across 3 pages; it now sits on one page at natural size (matches LaTeX \includegraphics max-width conventions).
  • New public API: renderTypstWithMermaid (root) / renderToWriterWithMermaid (typst module) — body-only rendering with the mermaid hook, for callers that supply their own preamble. The internals already supported this (see okMermaid test helper); it just wasn't exposed. Needed by PolicyPress (try typst as a latex replacement PolicyPress#58), which owns its own eisvogel-style preamble.
  • Version → 0.7.2 (0.7.1 tag already exists at e590c0a; note its zon still said 0.7.0 — this bump fixes that too).

Testing

  • zig build test — all pass, incl. new tests: string-literal escaping with quotes/backslashes/newlines, and the new public API.
  • zig build spec 652/652, zig build gfm 24/24.
  • End-to-end smoke: markdown w/ mermaid → zigmark -f typst (pozeiden) → typst compile (0.14.2) → PDF renders the flowchart as a vector diagram at natural size.

Downstream: PolicyPress issue sc2in/PolicyPress#58 (LaTeX→Typst migration) depends on this; please tag v0.7.2 after merge so it can pin the release.

The Typst renderer emitted #image.decode(bytes.fromBase64("…")), but
bytes.fromBase64 does not exist in Typst and image.decode is deprecated —
every document with a rendered mermaid block failed typst compile. The
tests only asserted the emitted string, so this never met a real typst.

- Embed diagrams as image(bytes("<svg…>"), format: "svg") with the SVG
  source in a string literal (quotes/backslashes escaped, raw newlines
  legal; verified against typst 0.14.2).
- Render diagrams at natural size, scaling down only when wider than the
  line width (layout/measure wrapper) — matches how LaTeX pipelines size
  images instead of blowing small flowcharts up to full text width.
- Add renderTypstWithMermaid / renderToWriterWithMermaid: body-only Typst
  rendering with the mermaid hook, for callers that supply their own
  preamble (e.g. PolicyPress).
- Bump version to 0.7.2.

Signed-off-by: TsunamiNoAi <spam@falseblue.com>
- nix run .#bump -- <version|patch|minor|major> bumps build.zig.zon and
  rolls the CHANGELOG [Unreleased] section into a dated release section
  (ported from PolicyPress).
- .githooks/pre-push rejects release-tag pushes without a CHANGELOG change
  since the previous tag; installed by the devshell shellHook.
- The release workflow enforces the same rule as an un-bypassable backstop
  before building artifacts.

Signed-off-by: TsunamiNoAi <spam@falseblue.com>
@tsunaminoai

Copy link
Copy Markdown
Contributor

Also added here per request: the nix run .#bump release helper (build.zig.zon + CHANGELOG roll) and the release-tag CHANGELOG guard (.githooks/pre-push installed by the devshell, plus an un-bypassable check in the release workflow) — ported from PolicyPress.

@tsunaminoai tsunaminoai requested a review from Copilot July 3, 2026 18:56
@tsunaminoai tsunaminoai added the bug Something isn't working label Jul 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Typst mermaid rendering so the emitted Typst compiles (by embedding SVG source directly instead of using nonexistent/deprecated Typst APIs), adds a new body-only Typst rendering API with the mermaid hook for downstream consumers that provide their own preamble, and tightens the release process around changelog/versioning.

Changes:

  • Replace Typst mermaid output from #image.decode(bytes.fromBase64(...)) to a compilable bytes("...") + image(..., format: "svg") embed with intrinsic sizing capped to line width.
  • Add public body-only Typst rendering with mermaid hook (renderTypstWithMermaid / typst.renderToWriterWithMermaid) and expand tests to cover string-literal escaping and the new API.
  • Bump version to 0.7.2 and add release tooling/guards (bump helper, pre-push hook, CI changelog enforcement).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/root.zig Exposes a new body-only Typst API with mermaid hook and updates public mermaid docs.
src/markdown/renderers/typst.zig Implements the new Typst mermaid embedding strategy, adds body-only mermaid writer API, and expands tests.
README.md Updates architecture/API documentation for Typst mermaid support and new entry point.
flake.nix Installs a version-controlled pre-push hook and adds a nix run .#bump helper app.
CHANGELOG.md Adds 0.7.2 release notes documenting the Typst mermaid fix and new API.
build.zig.zon Bumps package version to 0.7.2.
.github/workflows/release.yml Ensures full git history for tags and enforces changelog updates on release tags.
.githooks/pre-push Adds a local guard preventing release tags without changelog updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/root.zig Outdated
Comment thread src/markdown/renderers/typst.zig Outdated
Comment thread README.md Outdated
Comment thread src/root.zig
…ownership

Copilot review on #70 flagged that the public docs describe the Typst mermaid
output as "inline #image calls" while the renderer actually emits a #{ … } block
that binds the SVG via bytes("…") and calls image(…, format: "svg") inside a
layout/measure wrapper. Align README + all four Typst mermaid docstrings with the
real output.

Also document MermaidRendererFn's ownership contract: the returned slice must be
allocated with the allocator passed to the callback, since both the HTML and Typst
renderers free it with that allocator. Returning static memory would trigger an
invalid free.

Docs-only; no behavior change. zig build test passes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread src/markdown/renderers/typst.zig Outdated
Comment thread CHANGELOG.md
Comment thread flake.nix Outdated
…llback ownership

Second round of Copilot review on #70:

- CHANGELOG.md: add the `## [Unreleased]` header the `nix run .#bump` helper
  rolls into a dated release. Without it, bump would update build.zig.zon but
  leave the changelog untouched, then trip the pre-push/CI release guard.
- flake.nix bump app: fail fast (with a clear message, before touching any
  files) when `## [Unreleased]` is missing, instead of silently succeeding and
  committing only build.zig.zon. Rolled entries now use the em-dash separator
  to match existing changelog headers.
- html/typst renderers: document that the mermaid callback must return memory
  allocated with the passed allocator (it is freed after emit) on the raw
  fn-pointer signatures, matching the note already on `MermaidRendererFn`.

Docs + release-tooling only; no library behavior change. zig build test passes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread .githooks/pre-push Outdated
…shed sha

Copilot review on #70: for annotated tags the pushed local_sha is the tag
object rather than a commit. Resolve `prev` and the CHANGELOG diff through the
tag ref (`$tag`) instead so the guard peels to the tagged commit for both
lightweight and annotated tags. Verified against both tag types; shellcheck clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/release.yml
@sc2ben sc2ben merged commit b9bee4e into main Jul 3, 2026
5 checks passed
@sc2ben sc2ben deleted the fix/typst-mermaid-image-emit branch July 3, 2026 20:04
sc2ben added a commit to sc2in/PolicyPress that referenced this pull request Jul 3, 2026
#113)

* build: add pozeiden dependency and bump zigmark to 0.7.2 pre-release pin

zigmark is pinned to the fix/typst-mermaid-image-emit commit (sc2in/zigmark#70)
until v0.7.2 is tagged; swap to ?ref=v0.7.2 before merge. pozeiden is pinned to
the 23-update-to-zig-0160 branch head (zig 0.16 support is not on its main yet);
the lock resolves the same content via the v0.1.3 tag tarball.

* feat(utils): add underscoresToBlocks and executableInPath helpers

underscoresToBlocks converts redaction underscores to solid █ bars for the
typst engine (CommonMark parses 3+ underscores as thematic breaks, which
render as thin gray rules instead of redaction marks). executableInPath
moves from pandoc.zig so it survives the pandoc teardown.

* feat!: replace pandoc/xelatex PDF engine with typst

New src/typst.zig renders policies in-process: markdown → utils
preprocessing (unchanged) → zigmark AST → Typst markup with an
eisvogel-matching preamble (title page + colored rule + 6cm logo,
3-column header/footer, zebra tables, TOC depth 3, 5-column version
history incl. Revised By) → 'typst compile'. Mermaid diagrams render
via pozeiden to inline SVG — no mermaid-filter/node/Chromium, and they
now work inside the nix sandbox and on macOS.

Parity preserved: CLI flags, output filename patterns (issue #97),
draft.png watermark resolution (site root → theme fallback),
redaction bars (underscores → █ via utils.underscoresToBlocks).

pozeiden is not thread-safe (module-global arena and lazily initialised
grammar caches) while policies compile concurrently, so the mermaid
wrapper serialises calls behind a spinlock (zigmark's hook signature
has no std.Io for an Io.Mutex).

Removed: src/pandoc.zig, templates/eisvogel.latex (embed machinery in
build.zig), .puppeteer.json, Config.data_dir. The 'pdf' build step now
runs the typst engine standalone; e2e no longer copies the LaTeX
template into starter/. Tests: pdf rendering gates on typst instead of
xelatex and now exercises the mermaid fixture; new typst-source tests
for inline SVG, redaction bars, and the draft background; draft.png
resolution tests target Typst.resolveDraftPng.

* refactor: drop mermaid spinlock; pozeiden is thread-safe upstream

pozeiden now guarantees concurrent render safety itself (threadlocal theme
state, locked grammar-cache init — sc2in/pozeiden#35), so the consumer-side
serialisation is unnecessary. Repin to the fix commit; swap to a tagged
release once #35 merges and a tag is cut.

* build(nix)+ci+docs: drop the LaTeX toolchain from infra

flake.nix: remove eisvogel-tex input, pandoc, mermaid-filter, imagemagick
(unused), the mmdc/Chromium sandbox wrapper, and all fontconfig plumbing.
typst joins runtimeDeps; fonts (Source Sans 3, Source Code Pro) reach typst
via TYPST_FONT_PATHS in the devshells, apps, and the test check (with
TYPST_IGNORE_SYSTEM_FONTS for determinism). zig build no longer depends on
templates/, so Zola template edits stop busting the build cache.

action.yml: drop the eisvogel.latex copy step; pin the build shell to the
action's own checkout (path: flake ref) so consumers on released binaries
can't be broken by flake changes on main.

CI: macOS upgrades from 'zig build check' to the full test suite (the
mermaid-filter/Chromium blocker is gone); remove the apparmor userns
sysctls that existed only for Chromium.

Docs: README, building-pdfs, writing-policies, installation (ADO sample →
v1.4.0, closure-size note), live-editing; CHANGELOG [Unreleased] entry.

* fix: redact_web no longer drives PDF redaction

redact_web is documented as web-only (the Zola templates read it for the
site's redaction bars), but Config.load seeded the PDF pipeline's redact
default from it. An org hiding content (e.g. phone numbers) on the public
website could not keep it in the PDFs without knowing to pass --no-redact.

PDF redaction is now controlled solely by --redact/--no-redact and the
action's redact_mode input, which always passes an explicit flag, so action
consumers are unaffected.

Closes #115

* fix(ci): restore userns sysctl - bubblewrap needs it, not just Chromium

zig2nix's zig-hook runs the compiler inside bwrap, which requires
unprivileged user namespaces on Ubuntu 24.04 runners. The step's old
comment attributed it solely to Chromium/mermaid-filter, but removing it
broke any from-source build of zig2nix tooling with
'bwrap: setting up uid map: Permission denied'. Restored with an
accurate comment.

* fix(nix): TYPST_IGNORE_SYSTEM_FONTS accepts only true/false

typst's clap rejects '1' ('invalid value for --ignore-system-fonts'),
which failed the pdf-rendering test inside checks.test. Verified with an
unpiped 'nix flake check' this time (the earlier pass was tail masking
the exit code).

* fix(action): build the binary from the flake for in-repo runs

The PR preview workflow (uses: ./) installed the latest RELEASE binary —
v1.3.x still speaks pandoc, which no longer exists in this branch's ci
shell, so previews failed with 'pandoc not found'. When action_ref is
empty (local action), build policypress from the action's own flake so
previews exercise the code under review; remote consumers keep the
release download. The theme checkout likewise falls back to the
workflow's commit instead of the default branch.

* build: pin zigmark v0.7.3 and pozeiden v0.2.0 release tags

Replaces the interim PR-commit pins now that both upstream releases are
cut. zigmark v0.7.3 carries the typst mermaid emit fix and the body-only
renderTypstWithMermaid API; pozeiden v0.2.0 carries zig 0.16 support and
thread-safe rendering.

* build: bump zigmark pin to v0.7.4

Immutable releases on the zigmark repo prevented re-pointing v0.7.3, so
the release was re-cut as v0.7.4 (same fixes, plus the pozeiden v0.2.0
lazy-dep bump from zigmark#71). Pinned to the exact tag commit since
zig fetch resolved the bare ?ref one commit behind the tag.

* fix: address Copilot review findings

- Never delete a pre-existing .typ path on name collision; retry with a
  fresh random name instead (error.TypstWorkFileConflict after 16 tries).
- Build the output PDF path with std.fs.path.join instead of string
  concatenation.
- underscoresToBlocks: the closing frontmatter delimiter only counts as a
  whole line (line start + newline/EOF). Previously a '---' inside a
  frontmatter value ended the protected region early and let redaction
  corrupt underscores in later keys like last_reviewed. Adds a regression
  test.

---------

Co-authored-by: TsunamiNoAi <spam@falseblue.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants