Skip to content
Taliesin User Guide

5 Rich writing

The prose surface: typography, offline math, diagrams, figures, callouts, citations, sidenotes and raw HTML passthrough.

Everything you’d reach for in a technical post works out of the box, rendered server-side and shipped offline: no CDN, no client re-run when the reader’s device flips to dark.

5.1 Prose and typography

Standard Markdown works as expected: bold, italic, strikethrough, inline code, links, nested lists, blockquotes, and pipe tables with per-column alignment:

Cell languageRunsOutput
{python}in a warm Jupyter kernelspliced back as blocks
{js}in the reader’s browsermounted live
{mermaid}in the browser, lazilya rendered diagram

Smart typography turns straight quotes into curly ones and handles ellipses…, matching the look of a hand-written post.

Code blocks are syntax-highlighted server-side and get a hover Copy button. A fence whose language token isn’t recognised still renders, just unhighlighted and with no warning, so check the label if a block you expected to be highlighted looks flat (a common cause is a typo like ```pyton). The language is read from the first token and the rest of the info string is ignored, so a pasted ```rust,ignore still highlights. When a block is meant to be plain (a terminal transcript, sample output), label it text or console.

struct Block {
    id: String,
    sourcepos: String,
    html: String,
}

5.2 Math

Math renders server-side with KaTeX and ships fully offline (the fonts are inlined at build time). Inline math like eiπ+1=0e^{i\pi} + 1 = 0 sits in running text; display math stands alone; and a labelled equation is numbered and cross-referencable. Multi-line aligned environments work too. The discrete Fourier transform [1] is given by Equation 5.1:

X[k]=n=0N1x[n]e2πikn/NX[k] = \sum_{n=0}^{N-1} x[n]\, e^{-2\pi i k n / N}(5.1)

5.3 Diagrams

A {mermaid} code block renders client-side with mermaid.js; the colours are CSS-driven, so they follow the theme. Adding %%| label: fig-… and %%| fig-cap: turns a diagram into a numbered, captioned figure you can cross-reference, just like an image. As Figure 5.1 shows, one block model feeds three render targets:

flowchart LR
  Q[".tmd"] --> BM["block model"]
  BM --> H["HTML page"]
  BM --> S["website + nav"]
  BM --> K["book + TOC"]
Figure 5.1: One block model, three render targets.

Every diagram type mermaid supports works the same way (sequence, state, class, ER, gantt, and the rest); the syntax for each is in mermaid’s own documentation.

5.4 Figures and captions

A standalone image with a {#fig-…} attribute, or a code cell tagged with #| label: fig-… and #| fig-cap:, becomes a numbered <figure>, and @fig- cross-references resolve to its number. Figure 5.2 is drawn by a matplotlib cell that sets no theme colours: inline figures default to a transparent background with neutral-grey axes, so they track the light/dark theme on their own.

Figure 5.2: Variance explained per principal component.

5.4.1 What an image gets without asking

Point at a .png, .jpg/.jpeg, .gif or .webp and the rest is automatic. There is nothing to configure and nothing to pre-process.

A training loss curve falling from 1.0 to near zero over forty iterations.
Figure 5.3: A training loss curve falling from 1.0 to near zero over forty iterations.

Its box is reserved before it loads. Figure 5.3 above is emitted with the intrinsic width and height read from the file itself, so the prose below it never jumps as the image arrives. This happens in the preview too, which is the point: a preview that does not shift the way the built page shifts is not previewing it.

Images are copied across unchanged, so shrink or re-encode the source yourself if the byte count matters.

.svg and .avif are the exceptions to the box-reservation rule above: an SVG has no intrinsic pixel size to state, and AVIF decoding is deliberately not compiled in, so nothing is emitted for either.

5.5 Callouts

Fenced divs become callouts, layout grids, or generic containers.

Tip

Use a title= attribute or a leading heading to label a callout; otherwise it falls back to the callout kind.

A callout with collapse="true" becomes a native, JavaScript-free disclosure:

The full block model (click to expand)

Every top-level element carries data-block-id (a content hash), data-sourcepos (its source range), and, for included files, data-source-file.

5.6 Citations and cross-references

With a bibliography: in the front matter, [@key] citations become numbered links to an auto-generated References section, formatted from the BibTeX file. This page cites the TeXbook [2] and the FFT paper [1], and groups with locators work too: [2, 1, p. 297]. Cross-references resolve to labelled links across @fig-, @sec-, @tbl-, @eq- and @lst-, carrying the resolved number when known (as @eq-dft and @fig-scree above do): across chapters, so a @fig- in one chapter resolves to its figure in another.

In a site or book, bibliography: can instead live once in _site.yml and be shared by every page; a page’s own bibliography: is then merged over it. See Configuration.

5.7 Footnotes and sidenotes

Footnotes use the standard [^label] syntax. A reference renders as a superscript number, and the note itself renders in the right margin, beside the line that cites it.11Like this one. Write [^label] where you want the marker, and [^label]: … anywhere for the body; Taliesin does the numbering and the placement.Back There is no list of endnotes at the bottom of the page: a note sits where you need it, so reading one costs a glance instead of a jump to the end and back.

Narrow the window past the point where there is a margin to put a note in and the notes fold away; tapping a number reveals that one in place. Print does the same, since paper has no reader to tap.

Notes are for asides short enough to sit in a narrow column, and a definition carrying block content (a list, a quote, a code block) is flattened to its text with a warning, because a margin note can only hold inline content.

For an aside with no reference to hang it off, a ::: {.column-margin} block floats into the same margin column on a wide screen and folds into the flow on a narrow one: web-native marginalia a PDF can’t do. .column-margin is the only spelling. Footnotes and margin blocks share one column, so the two never collide.

This is a margin note. On a wide screen it sits in the margin, aligned to where it was written; narrow the window and it collapses to an indented note. Math works here too: σ2=Var(X)\sigma^2 = \operatorname{Var}(X).

The opposite move on the same axis is to make a block wider than the reading column: ::: {.column-page} widens up to the page width, for content the ~70-character measure is simply wrong for.

ClassWhat it doesReach for it when
.column-marginFloats the block into the right margin on a wide screen, folds it into the flow on a narrow oneAn aside with no footnote marker to hang it off
.column-pageWidens the block up to the full page widthA wide table, a multi-panel plot, a diagram whose labels only fit at size

Most of what wants the room needs no escape. A code block, a folded cell, a numbered listing, a cell’s output, a data table and a Mermaid diagram all take the wider band by themselves, because the reading measure fits 55 columns of code and PEP 8 asks for 79. The band is a ceiling rather than a size: a box is as wide as its content needs, never narrower than the prose and never wider than 100 columns. It centres on the prose unless a margin note has claimed the right side, in which case it right-aligns to the prose instead.

Neither escape is a new block type: .column-margin and .column-page are plain classes, so they work on a table, a figure, a listing, or a bare div, and the block keeps its identity (a @fig- cross-reference still resolves through one).

5.8 Reusing a partial

{{< include path/to/partial.tmd >}}

Splices another file’s contents into this one before parsing, as if you had pasted it in place. The directive must be alone on its line, and it is left literal inside a fence or an inline code span (which is why the examples on this page do not fire). The path is resolved relative to the file that holds the directive.

Includes are how you keep one author bio, one license footer, or one shared figure in a single file and pull it into many pages. Because the splice happens before the block model is built, the included blocks are first-class: they carry their own block ids and source positions, and click-to-source still resolves into the partial’s own file and line, not the page that pulled it in. The dev server also watches every transitively included file, so editing a partial re-renders every page that uses it.

The _includes/ convention

A partial is just a .tmd file, but you usually do not want it discovered as its own page in a site build. A file (or folder) whose name starts with an underscore is skipped by the page walker, and the idiomatic home for shared partials is an _includes/ directory near the project root. A nested post then reaches its shared partial with a relative climb, exactly as the corpus does:

{{< include ../../_includes/three-scene.tmd >}}

An include that cannot be expanded is left visible and reported, never dropped silently: an unsafe path (absolute, or one that climbs above the project root), an include cycle, and a missing or unreadable file each draw their own located warning instead of failing the build outright.

5.9 Raw HTML passthrough

A {=html} block is emitted verbatim, so you can drop in markup the Markdown grammar doesn’t cover (custom widgets, embeds, audio players):

This badge is raw HTML, not escaped. Its inline styles read the page's theme variables, so it tracks light and dark with everything else.

The palette is exposed as CSS custom properties you can use the same way: --tali-fg, --tali-bg, --tali-muted, --tali-border, --tali-code-bg and --tali-accent.

References

[1] J. W. Cooley and J. W. Tukey, “An Algorithm for the Machine Calculation of Complex Fourier Series,” Mathematics of Computation, vol. 19, no. 90, pp. 297–301, 1965.
[2] D. E. Knuth, The TeXbook. Reading, Massachusetts: Addison-Wesley, 1984.