Writing
This page documents everything ostat knows how to render, and is written using all of it. If the renderer breaks, this page shows it.
Front matter
Front matter is a --- fence around literal JSON, which is what
gingerBill’s generator does.
Odin has no YAML or TOML parser in its core library, and core:encoding/json
unmarshals straight into a struct, so the format costs almost no parser code.
---
{
"title": "Writing",
"date": "2026-01-30",
"description": "Optional. Falls back to the opening paragraph.",
"draft": false,
"slug": "optional-override"
}
---
title is required. A slug may not contain a path separator, .., or
whitespace: it becomes a directory name, and a content file is the least
trusted input the generator has.
date is YYYY-MM-DD, optionally followed by a time as THH:MM or
THH:MM:SS:
"date": "2026-01-30"
"date": "2026-01-30T14:30"
"date": "2026-01-30T14:30:05"
The time is what separates two posts published on the same day. Without one a date can only say they are equal, and the sort has to fall back to the slug, which orders them by a rule about their file names rather than about them. A date on its own is midnight, so it sorts at the start of its day and reaches the feed the way it always has.
The whole stamp goes in a <time datetime> attribute and the feed’s
<pubDate>. A listing shows the day: the time is there to order two posts,
not to be read in an index.
Margin notes
An aside is written where the claim isThis is a margin note. On a wide screen it sits out in the margin; below the breakpoint the marker becomes a toggle and the note folds inline., not collected at the foot of the page.
An aside is written where the claim is[^why-here], not collected.
[^why-here]: This is a margin note, and it may carry *markdown*.
Definitions may sit anywhere in the file and continue onto indented lines. The generator writes no numbers: the count comes from a CSS counter, so a note’s number is the stylesheet’s business and the two cannot drift apart.
An unresolved marker is left as prose rather than failing the build, because
[^ is ordinary text far more often than it is a noteA POSIX character class like [^a-z] is the obvious case, and it used to abort the build with an error naming a note nobody had written.. A mistyped
label is still caught from the other side: the definition it was meant to reach
goes unreferenced, and that does fail the build.
Prose
Inline: bold, italic, inline_code(), struck through, a
link, and a link back home.
A quote sits on a rule and goes muted. It is not a callout and does not want to be one.
Lists nest:
- A first item
- A second item, long enough to wrap onto a second line so the hanging indent
has a chance to be wrong
- A nested item
- Another nested item
Ordered lists may hold code, which stays inside its item:
-
Build the generator.
./build.sh -
Build a site with it.
./build/debug/ostat build site -
There is no step three.
Code
CommonMark has no syntax highlighting and libcmark does none, so ostat highlights fenced blocks itself, emitting the class names Chroma uses so the stylesheet needs no special casing.
package main
import "core:fmt"
main :: proc() {
// Comments are muted and italic.
fmt.println("Hellope, world!")
}
odin, zig and bash have lexers. Anything else renders escaped and
uncoloured:
this line is deliberately far too long to fit inside the measure and should produce a horizontal scrollbar on the block itself, never on the page
A longer fence holds a shorter one, which is how you document Markdown:
````text
```
An inner fence, kept as content.
```
````
Tables
Tables are not in CommonMark either, so they are rewritten before cmark sees them. Alignment comes from the delimiter row.
| Extension | Where it comes from | Aligned |
|---|---|---|
| Tables | gfm.odin, before cmark |
centre |
~~strikethrough~~ |
the same pass | centre |
| Highlighting | highlight.odin |
centre |
Raw HTML
Raw HTML passes through, so a figure renders as one:
That rule is an <hr>, and this is the end of the page.