Building a Markup Engine for Three Targets

One Source, Three Outputs
Three targets pull on the same document, the web, a printer, and a search index, yet only one parse should ever feed them all.
The core idea keeps parsing and rendering apart as different jobs. Text is read into a tree exactly once, and that tree feeds a web renderer, a print renderer, and a data serializer, so the document is never re-parsed to switch formats or targets.
Frontmatter comes built into the same surface, so metadata and body split without a second library, and the call that reads the tree also reads the document title, tags, and dates in one pass. The surface stays small on purpose, a handful of static calls that each answer one clear need, which keeps the engine easy to reach for and hard to misuse across every project.
Three Tools for One Job
Shipping content to the web, to print, and to a search index usually means stitching a Markdown renderer to a metadata reader and a separate print converter, each one carrying its own quirks and its own tangled dependency tree along for the ride.
The stitched setup drifts over time as each part reads the source its own way. A renderer built for the web reads a table one way, a print converter reads it another, and the metadata reader keeps its own rules for the same file, so a heading that looks right on a page can quietly break in a PDF or vanish from an index. Each library drags dependencies that slow installs.
The gap was a single parser that reads a document once and speaks three formats, correct by the same rules everywhere it lands. No adapter layer, no dependency tree, and no divergence between what the reader sees and what the crawler stores away.

Cracks Between Formats
- Separate renderers read the same table and heading by different rules, so output drifts between targets.
- A bolted-on metadata reader parses frontmatter with its own quirks, splitting the source from the render.
- Print conversion through an external tool adds a dependency chain and a second syntax to maintain.
- Loose spec coverage lets edge cases render one way on the web and another way in print or data.
Correct Everywhere It Renders
- Parse each document exactly once into a tree that every output shares.
- Emit web markup, a print body, and a serializable tree from that single tree.
- Split frontmatter natively so metadata and body come from one call.
- Hold identical behaviour across targets by validating against every published spec case.
- Ship zero third-party dependencies so the engine installs and runs anywhere.
Parse Once, Emit Thrice
The strategy separates reading from writing as two clean stages. A scanner walks the source into tokens, a builder folds those tokens into a tree, and three writers each translate that tree into its own target format without touching the text again.
Reading the flow shows why the design holds together under pressure. The tree is the single source of truth, so a table, a footnote, or a math span is understood one time and then described three separate ways for its target. Correctness lives in the parse step alone, and each writer only decides how to phrase what the tree already means, never what the content is.
Frontmatter is a first-class citizen here, not a bolt-on. A dedicated reader peels the metadata off the top, returns a clean body.

Tokens, Tree, Renderers
Every document becomes a tree first, then travels to whichever writer the caller asks for. A scanner splits text into structural tokens, a builder assembles them into the tree, and a web writer, a print writer, and a serializer each read that tree to produce output. Heading identifiers and relative links resolve during the walk, so the same options shape every target.
// Parse once, then emit each target from the same source
const html = Markdown.toHtml('# Title\n\nSome **bold** text.', { headingId: true })
const latex = Markdown.toLatex('# Title\n\nSome **bold** text.')
const { meta, body } = Markdown.frontmatter('---\ntitle: Guide\n---\n# Body')
Correctness here is not a claim, it is measured against published cases. The full CommonMark and GitHub surface is covered, alongside math, print mapping, extensions, and the frontmatter subset, each pinned to its own spec suite. Tables, task lists, alerts, footnotes, definition lists, and inline math all resolve the same way before any writer ever sees them at all.

Validated Against Every Spec
Note
A zero-dependency parser that quietly powers many internal pipelines, one of them the built-in Markdown engine inside Staverse, where a single parse turns raw content into pages, feeds, and search indexes, all kept aligned to one shared meaning.
| Coverage | Cases Passing |
|---|---|
| CommonMark | 652 |
| GitHub GFM | 683 |
| LaTeX | 70 |
| LaTeX Math | 52 |
| Extensions | 45 |
| Frontmatter | 46 |
Throughput confirms the design holds under sustained load. A 1 KB document parses and renders in around 150 microseconds across all three targets, a 5 KB mixed document stays under 900 microseconds, and a dense 10 KB document with repeated headings, tables, lists, code blocks, math, footnotes, and alerts finishes near 1.7 milliseconds on a single Apple M3 Pro core.
| Input Size | Parse | HTML | JSON | LaTeX |
|---|---|---|---|---|
| 1 KB | 152.4 µs | 147.1 µs | 152.5 µs | 136.2 µs |
| 5 KB | 888.8 µs | 829.0 µs | 828.6 µs | 756.6 µs |
| 10 KB | 1.7 ms | 1.6 ms | 1.7 ms | 1.5 ms |
Frontmatter splits in 4.3 microseconds for simple metadata and 12.3 microseconds for deeply nested YAML, invisible in any profile.
Stress runs push each feature hard, and none of them buckle under pressure. Inline formatting at thirty repetitions of every type clears in 531 microseconds, a thirty-row six-column table in 278 microseconds, twenty-five levels of nested lists in 155 microseconds, ten levels of nested blockquotes in 76 microseconds, and math blocks in only 56 microseconds each run.
| Stress Case | Time |
|---|---|
| Inline formatting, 30x all types | 531 µs |
| Table, 30 rows by 6 columns | 278 µs |
| Autolinks and link references | 117 µs |
| Alerts and HTML blocks | 85 µs |
| Nested blockquote, 10 levels | 76 µs |
| Footnotes and definitions | 64 µs |
| Math blocks, 10x | 56 µs |
Footnotes with definitions and abbreviations resolve in 64 microseconds, autolinks with link references land in 117 microseconds, and alerts with raw HTML blocks in 85 microseconds, so the tree walker scales linearly with real document complexity.
Most Markdown parsers on npm and jsr trade spec compliance for speed, or speed for features in turn. The few that pass the full CommonMark and GFM suites pull in heavy dependency trees and still lose time to the allocator on any larger document.
Full compliance at 652 CommonMark cases and 683 GFM cases lands alongside throughput that stays ahead of alternatives skipping half the surface. One lexer pass, one tree build, and a tight writer loop that never backtracks let the engine scale predictably with input size rather than collapsing under nested structures or the long inline runs that trip up heavier parsers.
The Split Paid Off
- Separating the parse step from the writers kept every output aligned to one meaning.
- Making the tree the single source of truth removed drift between web, print, and data.
- Treating frontmatter as native erased a whole dependency and a second set of rules.
- Pinning behaviour to published spec cases turned correctness into something measurable, not assumed.
- Shipping zero dependencies kept installs fast and the runtime free of conflicts.
Text Into Any Target
A tangle of three glued tools became one engine that reads a document once and speaks web, print, and data fluently. Parsing, print mapping, and metadata now share a single surface, keeping the path from raw text to any target short and predictable.
The editor lands on a parser that clearly earned its role here. One tree, three writers, and zero dependencies in the bundle.
Pages, feeds, print bodies, and search indexes all draw from the same parsed tree, so a change in the source shows up identically everywhere it renders. Native frontmatter keeps metadata beside the body, spec coverage keeps edge cases from diverging, and the small surface keeps the engine easy to reach for. Adding a new output just means adding a writer, not a parse.