Markdown’s sidekick.
Mausmark
I use Markdown a lot. Most of the time I don’t need HTML; I need the document’s structure. So I built Mausmark: a parser and tools around it. I use it to power this site and, in development, to format Markdown files.
I’m still working out what exactly Mausmark will become. But I’m excited about it and want to share its most useful part yet — the parser. It’s available as a WebAssembly library: v0.3.0 on npm.
Try it
This demo uses a renderer that writes to the DOM directly. Turn on CommonMark to parse the same text as CommonMark.
Markdown
Rendered document
The parser
I want a Markdown parser to give me the tree quickly and leave the rest to me. I don’t want to parse an HTML string to get the structure back. I also want the tree to be compact, so building another structure from it is cheap. And I don’t want to fit my ideas into a plugin system, maintain dependencies, toolchains, or specific versions of CMake. I want a tree; the rest is my business.
The package has no runtime dependencies and nothing to compile. The parser core needs no C standard library or OS calls and uses caller-provided memory; the JavaScript bindings manage that memory for you. It hands back a flat document tree, 16 bytes per node. Text is stored as slices of the original document. Links and reference definitions are separate nodes, and it’s up to you how to resolve them. The same tree can also drive a heading index, a link checker, or a formatter that writes Markdown back out.
The default dialect follows original Markdown, with Mausmark’s brace spans. You can also switch to CommonMark 0.31.2: rendered output matches 649 of its 652 spec examples byte for byte. Three definition-dependent reference-link cases remain unsupported. There’s no GFM support yet: no built-in tables, strikethrough, task lists, or bare-URL autolinks.
In the default dialect, curly braces mark a span: {text **bold** text} comes back as a node with parsed Markdown inside; the original interior text is also available on the node.
Spans nest and stay on one line.
You can use those to add custom functionality to the renderer, like {tag} or {func param}.
What a span means is up to the renderer; CommonMark reads braces as plain text.
Your own renderer
All three demos use the same WebAssembly parser. Only the renderers differ.
Sometimes I need Markdown to carry more than prose: tasks, tags, or project-specific structure. I’m experimenting with giving its existing elements new meanings in the renderer, using brace spans where I need an extra container. Curly braces were already on original Markdown’s list of escapable characters; Mausmark gives them a role as inline containers.
For example, this renderer reads [*], [**], and so on as footnote markers.
Definitions after a paragraph belong to that paragraph, so the next paragraph can start with [*] again.
The renderer collects the notes into a numbered list, with links in both directions.
These are this renderer’s rules, not built-in footnote support in the parser.
Markdown
Footnotes
Here’s another example: a simple task list.
In Mausmark’s default dialect, [ ] and [x] after a list item’s bullet are reference-style links.
They stay in the tree even without a matching definition.
The renderer in the demo below reads [ ] and [x] as task list indicators.
It adds the completed and total task counts to the heading immediately before the list, like Things to do (2/4).
No plugins and no change to the parser: a different walk over the same tree.
The task rules belong to this renderer; another reader can use the same parsed document without inheriting them.
Notice how {…} is turned into a tag.
Markdown
Task list
Fast to the tree
I want parsing to be the smallest part of the work, so there’s plenty of room for whatever comes after it. These are native benchmarks, not timings of the WebAssembly package; the native parser is still in development. In source-to-tree measurements on an M3 Pro (September 29, 2026), it parses a 1.2 MB copy of Moby-Dick in about 220 µs, and the 910 Markdown files in curl’s documentation in 4.13 µs each on average.
The comparison below includes parsing as CommonMark and conversion into a common in-memory format, with text decoded and references resolved. It does not include HTML output. Measured on an M3 Pro, October 9–12, 2026:
| Parser | curl docs | tldr pages | Pro Git |
|---|---|---|---|
| Mausmark | 484 MiB/s | 298 MiB/s | 1057 MiB/s |
| pulldown-cmark | 364 MiB/s | 242 MiB/s | 780 MiB/s |
| md4c | 331 MiB/s | 231 MiB/s | 629 MiB/s |
| cmark | 138 MiB/s | 119 MiB/s | 366 MiB/s |
| comrak | 120 MiB/s | 98 MiB/s | 327 MiB/s |
Pro Git is the book’s first edition in all its languages, joined into one document of about 11 MB, the way cmark’s own benchmark builds it. All five produce identical output on tldr. Outputs differ for md4c and pulldown-cmark on curl, and for md4c on Pro Git. Those speed comparisons aren’t strictly like-for-like.
Get it
If you use npm:npm install mausmark
The package has the WebAssembly parser, ESM bindings, TypeScript declarations, and an example DOM renderer to copy and adapt.
You write the output code or adapt that example; reference lookup is the renderer’s responsibility too.
It runs on Node.js 22.14+, Bun, and browsers with WebAssembly SIMD. Browser bundlers must emit the package’s .wasm asset.
Here’s a minimal example:
import { createParser, Dialect, NodeType } from "mausmark";
const parser = await createParser();
const document = parser.parse("# Hello, Mausmark");
// Or: parser.parse(source, Dialect.COMMONMARK)
for (let i = 0; i < document.nodeCount; i += 1) {
if (document.type(i) === NodeType.HEADING) {
console.log(document.joinedText(i)); // "Hello, Mausmark"
}
}
The module is 75.8 kB, 29.8 kB gzipped. It’s pre-1.0, so minor releases may change the API. The npm bindings cap input at 64 MiB and parser arena growth at 512 MiB. The package is MIT-licensed; its Unicode data comes with the Unicode License notice.
What’s next
The native parser is in development, and I plan to open its source at some point. There will also be an executable that formats Markdown and outputs it in various forms.