A fast CommonMark markdown parser for Zig. Builds a full AST for programmatic access, with optional extensions for CriticMarkup, GFM, and Obsidian-flavored markdown.
  • Zig 98.7%
  • Shell 1.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Mikael Säker 0b30047988 docs: repoint links to git.urverk.org
The referenced repos have migrated; their codeberg copies are deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 11:41:28 +02:00
bench Migrate to Zig 0.16 2026-05-01 17:11:52 +02:00
src Migrate to Zig 0.16 2026-05-01 17:11:52 +02:00
test Achieve 100% CommonMark spec conformance (652/652) 2026-02-01 23:16:12 +01:00
.gitignore Initial scaffold: AST, block parser (headings, paragraphs, thematic breaks) 2026-02-01 10:49:31 +01:00
BENCHMARKS.md Optimize renderer and allocator, narrow md4c gap from 1.79x to 1.32x 2026-02-02 10:22:01 +01:00
build.zig Add cross-parser benchmark comparison (markz vs cmark vs md4c) 2026-02-02 09:28:49 +01:00
build.zig.zon Migrate to Zig 0.16 2026-05-01 17:11:52 +02:00
CLAUDE.md docs: repoint links to git.urverk.org 2026-07-25 11:41:28 +02:00
context.md Migrate to Zig 0.16 2026-05-01 17:11:52 +02:00
DESIGN.md docs: repoint links to git.urverk.org 2026-07-25 11:41:28 +02:00
LICENSE Added license. 2026-02-04 22:39:48 +01:00
README.md docs: repoint links to git.urverk.org 2026-07-25 11:41:28 +02:00

markz

A fast CommonMark markdown parser for Zig. Builds a full AST for programmatic access, with optional extensions for GFM tables/task lists, CriticMarkup, and Obsidian-flavored markdown.

  • 100% CommonMark spec conformance (652/652 examples)
  • 1.5x faster than cmark (the C reference implementation)
  • Zero-copy text nodes reference original source bytes
  • Extensions for GFM (tables, task lists), CriticMarkup, and Obsidian, toggled at parse time
  • Zig 0.16+, no dependencies

Performance

Measured on Apple Silicon (M-series), parse + render to HTML:

Corpus markz cmark md4c
Pro Git (10.8 MB) 249 MB/s 162 MB/s 330 MB/s
Synthetic (976 KB) 76 MB/s 42 MB/s 91 MB/s
Spec suite (15 KB) 5.8 MB/s 4.8 MB/s 5.3 MB/s

markz builds a full AST then renders in a separate pass. md4c uses streaming callbacks with no intermediate tree, which accounts for most of the gap. The AST is the point — it's what consumers like editors and analysis tools need.

Getting started

Add markz to your build.zig.zon:

.markz = .{ .url = "git+https://git.urverk.org/urverk/markz#<commit>" },

Then in build.zig:

const markz_dep = b.dependency("markz", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("markz", markz_dep.module("markz"));

Usage

Parse and render to HTML

const markz = @import("markz");

const html = blk: {
    var doc = try markz.parse(allocator, markdown_source);
    defer doc.deinit();
    break :blk try markz.renderHtml(allocator, &doc);
};
defer allocator.free(html);

Walk the AST

var doc = try markz.parse(allocator, source);
defer doc.deinit();

var iter = doc.iterator();
while (iter.next()) |event| {
    switch (event) {
        .enter => |node| {
            // Container opened (block_quote, list, emphasis, link, ...)
            switch (node.tag) {
                .heading => std.debug.print("<h{}>", .{node.data.heading.level}),
                .emphasis => std.debug.print("<em>", .{}),
                .link => {
                    const ld = doc.getLinkData(node.data.link);
                    std.debug.print("<a href=\"{s}\">", .{ld.destination});
                },
                else => {},
            }
        },
        .exit => |node| {
            // Container closed
            switch (node.tag) {
                .heading => std.debug.print("</h{}>", .{node.data.heading.level}),
                .emphasis => std.debug.print("</em>", .{}),
                .link => std.debug.print("</a>", .{}),
                else => {},
            }
        },
        .leaf => |node| {
            // Leaf node (text, code_span, soft_break, ...)
            switch (node.tag) {
                .text => std.debug.print("{s}", .{doc.nodeText(node)}),
                .code_span => std.debug.print("<code>{s}</code>", .{doc.nodeText(node)}),
                .soft_break => std.debug.print("\n", .{}),
                else => {},
            }
        },
    }
}

Parse with extensions

var doc = try markz.parseWith(allocator, source, .{
    .gfm = true,
    .critic_markup = true,
    .obsidian = true,
});
defer doc.deinit();

Extensions are off by default. Standard markdown produces identical output regardless of extension flags.

Extensions

GFM (GitHub Flavored Markdown)

Tables with optional column alignment:

| Left | Center | Right |
|:-----|:------:|------:|
| a    | b      | c     |

Renders to <table> with <thead>/<tbody>, and align attributes on cells.

Task lists in list items:

- [ ] todo
- [x] done

Renders checkboxes as <input type="checkbox" disabled="" /> (checked when [x]).

CriticMarkup

Editorial markup for prose review:

Syntax Renders as
{++inserted text++} <ins>inserted text</ins>
{--deleted text--} <del>deleted text</del>
{~~old~>new~~} <del>old</del><ins>new</ins>
{>>comment<<} <span class="critic comment">comment</span>
{==highlighted==} <mark>highlighted</mark>

Obsidian

Wiki-style markup for note-taking apps:

Syntax Renders as
[[page]] <a href="page" class="wikilink">page</a>
[[page|display text]] <a href="page" class="wikilink">display text</a>
#tag <span class="obsidian-tag">#tag</span>

YAML frontmatter (--- fenced at document start) is parsed as a frontmatter node but not included in HTML output.

AST node types

Block-level

document, block_quote, list, list_item, heading, code_block, html_block, paragraph, thematic_break, frontmatter

Inline

text, emphasis, strong, code_span, link, image, autolink, html_inline, soft_break, hard_break

GFM

table, table_head, table_body, table_row, table_cell

list_item gains a task_state (none, unchecked, checked) when GFM is enabled.

CriticMarkup

critic_insertion, critic_deletion, critic_substitution, critic_comment, critic_highlight

Obsidian

wiki_link, obsidian_tag

Building

zig build test          # unit tests
zig build test-spec     # CommonMark spec conformance (652 examples)
zig build bench         # performance benchmarks