Skip to content

feat: render mermaid and svg code blocks as diagrams - #10

Merged
leonardosalasd merged 14 commits into
mainfrom
feat/v2.0.0
Aug 20, 2026
Merged

leonardosalasd merged 14 commits into
mainfrom
feat/v2.0.0

Conversation

@leonardosalasd

Copy link
Copy Markdown
Owner

A mermaid block is now rendered to SVG and embedded as a picture, and a svg block is passed through, since Typst renders SVG natively. Everything
else keeps rendering as syntax-highlighted code.

Mermaid goes through mermaidx, which runs Mermaid on an embedded JavaScript
engine — no Node install and no headless browser, so the tool stays a plain
pip install. Wheels cover Linux, macOS, and Windows on Python 3.10 through
3.14. Invalid diagram sources are reported with Mermaid's own parse message
instead of aborting with a traceback.

Conversion now carries generated content alongside copied assets, since a
rendered diagram has no file of its own.

Pictures also stop being forced to full width. A new fit-image helper keeps
an image at its natural size and shrinks it only when it is too wide for the
text block, which fixes small figures being blown up to fill the page.

Closes #8.

A ```mermaid block is now rendered to SVG and embedded as a picture, and a
```svg block is passed through, since Typst renders SVG natively. Everything
else keeps rendering as syntax-highlighted code.

Mermaid goes through mermaidx, which runs Mermaid on an embedded JavaScript
engine — no Node install and no headless browser, so the tool stays a plain
pip install. Wheels cover Linux, macOS, and Windows on Python 3.10 through
3.14. Invalid diagram sources are reported with Mermaid's own parse message
instead of aborting with a traceback.

Conversion now carries generated content alongside copied assets, since a
rendered diagram has no file of its own.

Pictures also stop being forced to full width. A new fit-image helper keeps
an image at its natural size and shrinks it only when it is too wide for the
text block, which fixes small figures being blown up to fill the page.

Closes #8.
Every template hardcoded us-letter, which is the standard in the US and
Canada and nowhere else. Page size is now a real option: --paper on the
command line, or a paper key in front matter, covering A3 through A6, both
ISO B5 and JIS B5, and the US letter, legal, and tabloid sizes.

BREAKING CHANGE: the default page size is now a4 rather than us-letter.
Pass --paper us-letter, or set paper: us-letter in front matter, to keep the
previous output.

Templates also gain an ..options sink, so adding an option in a later
release no longer breaks a user-supplied .typ that has not been updated.
Markdown written for other toolchains carries LaTeX math, and Typst does not
speak it: a backslash escapes the next character, so $\frac{a}{b}$ reaches the
compiler as an unknown variable named rac. A new latex module rewrites it —
Greek letters, relations and operators, fractions and roots, scripts, font
commands, matrices, and cases. Unrecognized commands drop their backslash,
which lands on the right Typst symbol most of the time.

Two differences between the languages needed real handling rather than a
lookup table. Typst reads a run of letters as one identifier and rejects the
ones it does not know, while LaTeX reads xy as x times y, so runs are split
into single letters unless they name an operator such as sin or lim. And
matrix columns are separated by & in LaTeX but by a comma in Typst, with rows
moving from \\ to a semicolon, which made environments context-sensitive.

Inline math also gets a stricter rule than the one mistune ships, whose
closing guard is a lookahead where it needs a lookbehind. Without that fix
"it costs $10 and $20" parsed the middle as math and mangled it — the kind of
line that appears in nearly every README this tool is pointed at.

Every symbol in the table was checked against the compiler, and the tests
compile their translations rather than only comparing strings.
`doc-engine --help` listed two commands and two global flags, so finding the
options that matter meant knowing to ask for `build --help` first. Since
building is what nearly every run does, the top-level screen now shows those
flags directly, grouped by what they affect — output, document details,
appearance, and the flags used while writing — instead of one flat column.
Both screens open with worked examples.

Descriptions are read from the Click parameters, so the help text still has a
single source of truth and cannot drift from the flags themselves.

`info` moves to a grid so wrapped values line up under their label, and it now
reports what the build can actually do: page sizes, accents, diagrams, math.
A project that outgrew a single file can now list its parts in doc-engine.md
as ordinary Markdown links, so the manifest still reads as a table of contents
on GitHub. Entries are handled by what they are: Markdown files become
sections, .mmd sources are rendered where they appear, images are placed as
captioned figures, and a .bib registers as the bibliography for the document.
Front matter at the top carries the metadata for the whole thing.

Each entry converts against its own folder, so a picture referenced from
inside an included file resolves exactly as it does when that file is built
alone. Asset names are namespaced per entry so two folders can both hold a
logo.png without one overwriting the other.

Watch mode follows every file the manifest names, and re-reads the list after
each rebuild in case the manifest itself changed.

Also fixes tables, which have been wrong since 1.0.0: the header row was read
as a table_row when a table_head holds its cells directly, so the column count
came from the first cell's text and every table collapsed into one column.
Two layouts join the set. `article` is the classic LaTeX paper: New Computer
Modern, numbered sections, a title block at the top of the first page instead
of a cover sheet. `report` trades density for legibility — 12pt on generous
leading, wide margins that hold the measure near 65 characters, and a lot of
air around headings.

Section numbering in `article` drops the levels that never appear, because the
first `#` becomes the document title and real sections usually start at `##`,
which otherwise numbered them 0.1, 0.2, 0.3.

The existing templates asked for Inter, Cascadia Code, and Linux Libertine and
fell back to other system fonts. Typst only bundles New Computer Modern,
Libertinus Serif, and DejaVu Sans Mono, so none of those existed inside the
published Docker image and documents built there silently used whatever was
left. Each stack now ends in a bundled font.
A team that always builds the same way can put it in .doc-engine.toml, or in a
[tool.doc-engine] table in pyproject.toml, instead of repeating flags or
copying front matter between documents. Only known keys are read, so a typo
cannot quietly change how a document is built. Precedence runs flag, then
front matter, then the project file.

--pdf-standard writes PDF/A-2b or PDF/A-3b for archives that require it.

--fetch-images downloads images linked by URL, which until now rendered as
alt text. It stays off by default, because building a document should not
reach the network on its own. Each download is bounded: http and https only, a
ten second timeout, a size ceiling, and a check that the response really is an
image. A download that fails warns and leaves the alt text rather than failing
the build.

Adopts ruff for linting and formatting, wired into CI and documented in
CONTRIBUTING. This replaces the code-style badge that claimed a formatter the
project never ran.
A tall picture had no good outcome. fit-image only constrained the width, so a
top-down flowchart kept its natural height, ran past the bottom of the page,
and Typst clipped whatever did not fit — the first steps of the diagram simply
disappeared from the PDF. It now scales by whichever dimension is tighter, so
the whole picture always survives.

That still leaves such a diagram legible only in principle, so --tall-images
split cuts it into page-tall pieces and places them one after another, which
keeps the boxes at a readable size across several pages. The default stays
fit, where the picture is scaled to sit on one page.

Adds pillow, used only when a picture actually has to be cut.
Covers diagrams, math, the manifest, project configuration, page sizes, tall
images, and archival output, and marks the roadmap items this release closes.
A tall picture used more than once left a dangling reference. It is registered
under a single name, so the second occurrence tried to cut a file the first
pass had already replaced, and the document pointed at an asset that no longer
existed — the compile would fail on a missing file. The pieces are remembered
per picture now.

A Markdown file pulled in by a manifest had its own front matter rendered as
body text, so "title: Internal" showed up as a heading in the PDF. The manifest
owns the metadata, so only the body of an included file is used.

Figure captions went into Typst content unescaped, which made a caption such as
"Cost $5 and #1" open math and code expressions. Captions are Markdown, so they
now go through the converter.

Also closes forty gaps in the math translation, found by feeding it the kind of
notation a real paper uses: logic, floors and ceilings, big operators, long and
hooked arrows, order relations, and vector accents.

CI gains a job that builds the Docker image and runs the CLI inside it, since
this release adds dependencies with compiled parts, and the smoke test now
exercises diagrams, math, tables, page size, and PDF/A rather than plain text.
Three sources can set the same option — a flag, the document's front matter,
and the project file — and deciding between them had grown into the middle of
a 200-line command callback, where the only way to exercise it was to run the
whole program against a temporary directory.

It moves to a settings module: a function over plain dictionaries returning
what the build should use. Colour names and the template search stay in the
CLI and are passed in, so the rules can be read without the vocabulary.

No behaviour changes, and the precedence now has direct tests instead of being
covered only through end-to-end invocations.
The header GIF still showed 0.1.x output from April, four versions back, and
it is the first thing anyone arriving from a link sees. The new one runs
v2.0.0 against a document with math, a diagram, and a table.

Adds a note at the top saying plainly what this branch is: v2.0.0 complete but
not published, with the commands to run it from a checkout, and pointers to
issues and discussions for anyone willing to test it.

@leonardosalasd leonardosalasd left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

OK

@leonardosalasd
leonardosalasd merged commit bfe01a8 into main Aug 20, 2026
21 checks passed
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.

Not support Mermaid/SVG code block

1 participant