Skip to content

zn v0.12.0: LaTeX display math and TikZ in the reading view with math = "latex" - #18

Merged
adibhanna merged 11 commits into
mainfrom
zn-0.12.0
Oct 27, 2026
Merged

adibhanna merged 11 commits into
mainfrom
zn-0.12.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

Ships #17 by @spirit-of-light: [terminal] math = "latex" renders display math through the user's own pdftex, so the LaTeX the desktop's KaTeX reads (\frac, align, mhchem, TikZ) shows as pictures in the terminal preview instead of falling back to source. typst stays the default.

The contributor's five commits land unchanged in a merge commit. Review findings follow it as separate commits:

  • Wide blocks were clipped. standalone's page is about 12 cm wide, so a long display lost its tail and a 20 cm TikZ figure stopped at 12 cm. A block whose TeX log reports an overfull line now renders once more on a page widened by exactly that much; blocks that fit keep the usual width.
  • zn mcp served different instructions under latex. The variant asked agents for bare tikzpicture blocks, which the desktop, the phones and the share viewer show as plain text. The instructions are the desktop's text again under either engine, and the preview meets them instead: ```tikz fences render under latex, normalised the way the desktop's TikZ renderer does it, and the default preamble.tex loads the desktop's TikZ libraries.
  • $$ math turned red in the editor under the default engine. chroma's typst lexer marks every backslash in LaTeX as an error. $$ bodies are lexed as tex under both engines.
  • The TeX cache never shrank. Each preamble edit left a 3.5 MB format and each formula edit a PNG. Formats and pictures unused for 30 days are now deleted; a session touches what it uses.
  • Comments cited a spec that is not in the repo (AC7, "Step 5"), and one claimed reads were sandboxed when TeX Live reads any path anyway. They now state the constraint.

Verified: go vet ./..., go test ./..., cross-builds for darwin, linux and windows, the Homebrew formula tests, and the real-TeX suite (CI skips it) against a basic TeX Live 2026 with exactly the README's tlmgr list. Each fix's real-TeX test fails before it (a long formula and a 20 cm figure both came out 1256 px wide). Driven in tmux on a scratch vault: $$, \[ \], align, mhchem, a bare tikzpicture, a desktop-style ```tikz fence with right=of, a long formula and a broken fence (error row above the code), plus the editor's colours under typst before and after. Write escapes (absolute, ../, ~, $HOME, quoted) are all refused; reads by absolute path are not, as the README warns.

spirit-of-light and others added 11 commits October 26, 2026 18:37
The reading view renders display math only through typst, and passes the
block to typst verbatim, so real LaTeX such as \frac{a}{b} falls back to
source. This adds the settings the LaTeX engine builds on: math picks
typst (the default) or latex, render_timeout bounds one external render
in seconds (default 10), and render_concurrency caps how many run at
once (default 2).

A bad value never stops zn from starting: an unknown engine reads as
typst and a non-positive number as the default. zn doctor reports it,
and under math = "latex" it also names a missing pdftex or pdftocairo.
Saving a setting stays strict, the way images already is. The starter
config.toml carries the keys with a comment naming the tools, and the
settings screen offers typst or latex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With [terminal] math = "latex", display math renders through the user's
own pdftex instead of typst, so full LaTeX works: amsmath environments,
mhchem and TikZ. $$ blocks, \[ \] (one or many lines) and top-level
equation, align, gather, multline, alignat, flalign, eqnarray (starred
too) and tikzpicture blocks are detected; inside code fences, $$, lists,
quotes and callouts they stay text, and under typst nothing changes.

Each block is a standalone document over preamble.tex in the config
dir. zn writes the default (amsmath, amssymb, mhchem, tikz with
arrows.meta and calc) on the first render and never rewrites it. The
preamble is dumped once into a pdftex format named by its hash, so a
block only pays for itself; a changed preamble builds a new format.
pdftex runs with -no-shell-escape and openin_any=p/openout_any=p, so a
note cannot run commands or write outside the work dir. pdftocairo
turns the PDF into a transparent PNG, cropped to the formula so its
size does not shrink with the pane, coloured with the theme's text
colour, and cached on disk under zn/tex.

Every external render (mmdc, typst, pdftex, pdftocairo) now runs through
one runner that kills it after render_timeout, and through one pool of
render_concurrency slots. A TeX error shows its first line above the
source and is remembered; a timeout is not, so the block renders again
when its note is opened, without looping on every redraw. The editor
highlights math bodies with chroma's tex or typst lexer to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor already closes $$ and code fences on Enter. Under latex,
\[ now gets \] and \begin{name} gets \end{name} for any environment
name, with the cursor on the empty line between, and no closer is added
when one already follows. Under typst these lines get a plain Enter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note-writing instructions tell an AI to emit ```tikz fences, which
the latex engine does not render; it renders a top-level tikzpicture
and loads libraries from preamble.tex. Under math = "latex" the server
now serves a copy of the instructions with the TikZ guidance rewritten
for that: a bare \begin{tikzpicture} block, no \usetikzlibrary line.
The copy comes from six exact replacements, and a test fails if any of
them stops matching, so the typst text stays the single source. The
typst text and a user's override file are unchanged, and the setting is
read once when the server starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's reading-view section and the in-app help describe the math
engine choice, render_timeout and render_concurrency, where preamble.tex
lives and what it loads. They also say what latex needs: pdftex from a
basic TeX Live plus the packages zn uses, and pdftocairo from poppler.
They warn that TeX can read any file by absolute path, so a note can
show a readable file in the preview, and advise leaving latex off for
untrusted vaults. The Homebrew formula gains caveats with the same
install steps and no new dependency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…latex" (by @spirit-of-light)

The reading view could only send $$ blocks to typst, verbatim, so the
LaTeX that the desktop's KaTeX reads (\frac, align, mhchem) fell back to
source in the terminal. This adds [terminal] math = "latex": $$, \[ \] and
the top-level amsmath and tikzpicture environments compile through the
user's own pdftex over a preamble.tex next to config.toml, precompiled
into a format named by its hash, and pdftocairo turns each block into a
transparent PNG in the theme's text colour, cropped to the ink and cached
under the user cache dir. typst stays the default and nothing changes
under it in the preview.

Every external render (mmdc, typst, pdftex, pdftocairo) now goes through
one runner bounded by render_timeout and one pool of render_concurrency
slots, so a runaway TeX loop or a hung mermaid-cli is killed instead of
pinning a goroutine. A TeX error shows its first line above the source;
a timeout is not remembered, so the block tries again when its note is
opened. pdftex runs with -no-shell-escape and openout_any=p, and the
README says plainly that TeX can still read files by absolute path.

The contributor's suite passes, including the real-TeX tests (CI skips
them) against a basic TeX Live with exactly the README's tlmgr list.
Review findings land in the commits that follow this merge.
…clipped

standalone's varwidth page is \linewidth wide, about 12 cm, and pdftex
lets a longer display or a wider picture run past its right edge, where
pdftocairo cuts it off. A 26-term sum lost its last half and a 20 cm
TikZ figure stopped at 12 cm, with nothing in the preview to say so.

TeX already reports the overflow ("Overfull \hbox (395pt too wide)"), so
a block whose log says so renders once more with the page widened by
that much through \standaloneconfig. A block that fits renders once, as
before, at the usual width: widening every page instead would stretch
multline and push equation numbers out to the far edge, and the crop
would then keep a wide, mostly empty picture.

The cache key's version token moves on so the clipped PNGs already on
disk are not read again.
…ktop's instructions

Under math = "latex" the MCP server served a rewritten copy of the
note-writing instructions that asked agents for a bare tikzpicture block
instead of a ```tikz fence, because only the bare block rendered in the
zn preview. But the vault is the desktop's too: the desktop, the phones
and the share viewer render ```tikz fences and show a bare tikzpicture
as plain text, so every figure an agent wrote through zn mcp would draw
in one terminal and nowhere else. The instructions are the desktop's
text again, byte for byte, under either engine.

The preview meets them instead: under latex a ```tikz fence now renders
through pdftex, normalised the way the desktop's TikZ renderer does it
(wrapSource in apps/desktop/src/main/tikz.ts): a \documentclass line
goes, and \usepackage or \usetikzlibrary lines above the picture stay in
the preamble, ahead of \begin{document}. The default preamble.tex loads
the TikZ libraries the desktop loads for every fence, so a fence written
there (right=of a, -Stealth) draws here without lines of its own. Until
the picture is ready, and when TeX fails, the fence shows as code, with
the error above it. Under typst a fence stays code, as before.

Bare tikzpicture blocks still render under latex. The real-TeX test now
renders the instructions' own ```tikz example as a fence.
… too

The merge taught the editor to colour display math, picking chroma's
lexer by engine: tex under latex, typst under typst. typst is the
default, and the $$ blocks in a ZenNotes vault hold LaTeX, because the
desktop renders them with KaTeX. chroma's typst lexer reads every
backslash in \frac or \sqrt as an error token, which the theme paints
red, so every existing note's math turned red in the editor for anyone
who never touched the setting.

$$ bodies are now lexed as tex under both engines; \[ \] and the display
environments are still lexed only under latex, where they are math. A
```tikz fence, which chroma has no lexer for and so left plain, is
lexed as tex as well.
Everything latex renders stays under <UserCacheDir>/zn/tex: one format
per preamble.tex (3.5 MB with the default packages, so every edit of the
preamble leaves another behind) and one PNG per block per theme colour,
including every intermediate version of a formula typed with the
preview open. Nothing was ever deleted, and the README said so.

A session now touches the format it renders with and every PNG it reads
back, and the first render of a session deletes the formats and PNGs
untouched for 30 days, plus the temp files of renders that died midway.
Age rather than "not the current preamble" decides, because a second zn
with its own config dir, and so its own preamble, shares the cache and
keeps its format by using it. A file that cannot be deleted, open in
another zn on Windows, waits for the next session.
…ec item it met

The latex work arrived with comments and test messages keyed to an
acceptance-criteria list (AC7, AC29, "Step 5", "rev 7") that never
entered the repository, so a reader met references they could not
follow. They now state the constraint itself, and "the AC7
environments" reads "the display environments".

One comment was also wrong rather than only unreadable: texEnv claimed
pdftex could not read outside its working dir. openin_any=p does not
stop it; TeX Live 2026 reads any file by absolute or ../ path anyway,
which is what the README warns about and what the real-TeX sandbox test
asserts. The comment now says so, and that writes are what stay inside.
@adibhanna
adibhanna merged commit 354723e into main Oct 27, 2026
5 checks passed
@adibhanna adibhanna mentioned this pull request Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants