An open API service indexing awesome lists of open source software.

https://github.com/sc2in/zigmark

Simple markdown library for zig
https://github.com/sc2in/zigmark

cli library markdown parser renderer zig

Last synced: 17 days ago
JSON representation

Simple markdown library for zig

Awesome Lists containing this project

README

          

# zigmark

[![CI](https://github.com/sc2in/zigmark/actions/workflows/ci.yml/badge.svg)](https://github.com/sc2in/zigmark/actions/workflows/ci.yml)

A CommonMark-compliant Markdown parser and renderer for Zig. Passes **all 652 CommonMark spec tests** and **all 24 GFM extension tests** (100%).

Renders to **HTML**, **Typst** (PDF-ready), **AST**, and more.

Builds as both a **CLI tool** and a **C-callable shared library** (`libzigmark.so`).

## Installation

Add `zigmark` as a dependency in your `build.zig.zon`:

```zig
.dependencies = .{
.zigmark = .{
.url = "https://github.com/sc2in/zigmark/archive/.tar.gz",
.hash = "...",
},
}
```

Then in your `build.zig`:

```zig
const zigmark = b.dependency("zigmark", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zigmark", zigmark.module("zigmark"));
```

## CLI Usage

```bash
zig build
```

### Convert Markdown to HTML

```bash
# From a file
zigmark README.md

# From stdin
echo '# Hello' | zigmark

# Write to a file
zigmark -o output.html README.md
```

### Inspect the AST

```bash
echo '**bold** and *italic*' | zigmark -f ast
```

```
Document
└── Paragraph
├── Strong ('*')
│ └── Text "bold"
├── Text " and "
└── Emphasis ('*')
└── Text "italic"
```

### Convert Markdown to Typst (PDF)

```bash
# Body-only Typst markup (embed in your own document)
echo '# Hello' | zigmark -f typst

# Full document with eisvogel-inspired preamble — pipe straight to typst
zigmark -f typst report.md | typst compile - report.pdf
```

YAML frontmatter fields are automatically mapped to document options:

```markdown
---
title: "My Report"
author: Alice
date: 2026-03-19
titlepage: true
toc: true
numbersections: true
colorlinks: true
header-right: "Confidential"
footer-left: "Alice"
---

# Introduction

Hello **world**.
```

Supported frontmatter fields:

| Field | Type | Default | Description |
|---|---|---|---|
| `title` | string | — | Document title |
| `subtitle` | string | — | Subtitle shown on title page |
| `author` | string or list | — | Author name(s); list uses the first entry |
| `date` | string | — | Date shown on title page |
| `lang` | string | `en` | Document language |
| `paper` | string | `a4` | Paper size (e.g. `a4`, `us-letter`) |
| `fontsize` | string | `11pt` | Base font size |
| `titlepage` | bool | `false` | Generate a full-bleed title page |
| `titlepage-color` | string | `1E3A5F` | Title page background (hex, no `#`) |
| `titlepage-text-color` | string | `FFFFFF` | Title page text colour |
| `titlepage-rule-color` | string | `AAAAAA` | Title page rule colour |
| `titlepage-rule-height` | number | `4` | Title page rule thickness (pt) |
| `toc` | bool | `false` | Insert a table of contents |
| `toc-title` | string | `Contents` | TOC heading |
| `toc-own-page` | bool | `false` | _(reserved, not yet implemented)_ |
| `toc-depth` | number | `3` | TOC depth |
| `numbersections` | bool | `false` | Number headings |
| `disable-header-and-footer` | bool | `false` | Suppress page header and footer |
| `header-left` / `header-center` / `header-right` | string | title / — / date | Header slots |
| `footer-left` / `footer-center` / `footer-right` | string | author / — / page\# | Footer slots |
| `colorlinks` | bool | `true` | Colour hyperlinks |
| `linkcolor` | string | `A50000` | Internal link colour (hex) |
| `urlcolor` | string | `4077C0` | URL link colour (hex) |

### AI-Friendly Output

```bash
zigmark -f ai README.md
```

Produces a token-efficient AST representation suitable for LLM consumption.

### Extract Frontmatter as JSON

```bash
zigmark -f frontmatter post.md
```

Parses the frontmatter block (YAML `---`, TOML `+++`, JSON `{`, or ZON `.{`) and
emits it as pretty-printed JSON. Outputs `{}` when no frontmatter is present,
so the output is always valid JSON and safe to pipe.

```bash
# Pipe into jq
zigmark -f frontmatter post.md | jq '.title'

# Extract a nested key
zigmark -f frontmatter post.md | jq '.extra.author'
```

### Edit Frontmatter

`--format markdown` re-serialises the frontmatter (in its original format) and
passes the body through verbatim. Use `--set` and `--delete` to mutate fields
before writing:

```bash
# Update a field and delete another, keep body unchanged
zigmark -f markdown --set title="New Title" --delete draft post.md

# Set a nested key (intermediate objects are created automatically)
zigmark -f markdown --set extra.owner=SC2 post.md

# Pipe the result back over the original file
zigmark -f markdown --set date=2025-06-01 post.md -o post.md
```

`--format normalize` does the same frontmatter handling but also reconstructs
the Markdown body from the AST, normalising headings to ATX style, links to
inline, and code blocks to fenced:

```bash
zigmark -f normalize --set title="Clean" post.md
```

### Edit Body Blocks

`--set-block` replaces a single block in the document body using a
`type[N]` selector — the same bracket syntax used by the AST query API.
`type` is any block tag (`block`, `heading`, `paragraph`, `table`, …);
`N` is a zero-based index. The right-hand side is parsed as Markdown and
its first block becomes the replacement. Applies to `normalize` format.

```bash
# Replace the first heading
zigmark -f normalize --set-block 'heading[0]=# New Title' post.md

# Replace a block at an absolute index (any type)
zigmark -f normalize --set-block 'block[3]=Updated paragraph text.' post.md

# Replace the second table
zigmark -f normalize --set-block 'table[1]=| A | B |\n|---|---|\n| 1 | 2 |' post.md

# Combine with frontmatter edits
zigmark -f normalize --set title="Clean" --set-block 'heading[0]=# Clean' post.md -o post.md
```

`--section-start` and `--section-end` replace every block _between_ two
HTML comment markers (the markers themselves are preserved). Replacement
Markdown is read from stdin; the document file must be given as a
positional argument. Applies to `normalize` format.

```bash
# Replace the content between and
cat new-perf-tables.md | zigmark -f normalize \
--section-start bench-start \
--section-end bench-end \
README.md -o README.md
```

This is how `nix run .#bench` updates the performance section of this
README — it generates the new table Markdown, then uses zigmark's own AST
mutation API to splice it in, replacing the Python regex that did the same
job before.

### Options

```
Usage: zigmark [OPTIONS] [FILE]

-h, --help Display this help and exit.
-v, --version Print version and exit.
-f, --format Output format: "html" (default), "typst", "ast",
"ai", "terminal", "frontmatter", "markdown", or
"normalize".
-o, --output Write output to FILE instead of stdout.
-s, --set ... Set a frontmatter field (KEY=VALUE). Repeatable.
Applies to: markdown, normalize, frontmatter.
-d, --delete ... Delete a frontmatter field (dot-path). Repeatable.
Applies to: markdown, normalize, frontmatter.
-e, --set-block ... Edit a body block (SELECTOR=CONTENT). Selectors:
block[N], heading[N], paragraph[N], table[N].
First block of CONTENT replaces the target.
Repeatable. Applies to: normalize.
--section-start ) Replace document body between two HTML comment
--section-end ) markers with Markdown from stdin. FILE required.
Applies to: normalize.
```

## Zig Library Usage

### Basic Parsing and Rendering

```zig
const std = @import("std");
const zigmark = @import("zigmark");

pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

const markdown =
\\# Hello World
\\
\\This is a **bold** paragraph with a [link](https://sc2.in).
\\
\\- List item 1
\\- List item 2
;

var parser = zigmark.Parser.init();
var doc = try parser.parseMarkdown(allocator, markdown);
defer doc.deinit(allocator);

const html = try zigmark.HTMLRenderer.render(allocator, doc);
defer allocator.free(html);

std.debug.print("{s}\n", .{html});
}
```

### Typst Rendering

Generate Typst markup from a parsed document:

```zig
const zigmark = @import("zigmark");

// Body-only (embed in your own Typst document)
const markup = try zigmark.TypstRenderer.render(allocator, doc);
defer allocator.free(markup);

// Full document with eisvogel-inspired preamble
const opts = zigmark.typst.DocumentOptions{
.title = "My Report",
.author = "Alice",
.date = "2026-03-19",
.titlepage = true,
.toc = true,
.numbersections = true,
.colorlinks = true,
};
const full = try zigmark.typst.renderDocument(allocator, doc, opts);
defer allocator.free(full);
```

### AST Query System

Navigate the parsed document with jQuery-like selectors:

```zig
var query = doc.get();

// Get all headings (optionally filter by level)
var headings = try query.headings(allocator, null);
var h2s = try query.headings(allocator, 2);

// Get all links
var links = try query.links(allocator);

// Count elements
const para_count = query.count(.paragraph);
```

### Frontmatter

Extract and query structured metadata from the top of a Markdown file. All four formats are normalised to `std.json.Value` for uniform access.

| Format | Opening marker | Example |
|---|---|---|
| YAML | `---` | `--- \ntitle: Hello\n---` |
| TOML | `+++` | `+++\ntitle = "Hello"\n+++` |
| JSON | `{` | `{"title": "Hello"}` |
| ZON | `.{` | `.{ .title = "Hello" }` |

```zig
const FrontMatter = zigmark.FrontMatter;

// Parse from a full Markdown document (auto-detects format)
var fm = try FrontMatter.initFromMarkdown(allocator, markdown_source);
defer fm.deinit();

// Dot-separated key lookup — returns ?std.json.Value
const title = fm.get("title"); // top-level key
const host = fm.get("server.host"); // nested key
const first = fm.get("tags"); // array → .array variant

if (title) |t| std.debug.print("title: {s}\n", .{t.string});

// Or parse a bare frontmatter string directly
var fm2 = try FrontMatter.init(allocator, source, .toml);
defer fm2.deinit();

// Mutate: set a value at a dot-separated path (creates intermediates as needed)
try fm.set("title", .{ .string = "New Title" });
try fm.set("extra.owner", .{ .string = "SC2" });
try fm.set("draft", .{ .bool = false });

// Delete a key
_ = fm.delete("draft");

// Deep-merge another frontmatter document (overlay keys win on conflict)
var overlay = try FrontMatter.initFromMarkdown(allocator, other_source);
defer overlay.deinit();
try fm.merge(overlay);

// Re-serialise in the original format (YAML/TOML/JSON/ZON)
const serialized = try fm.serialize(allocator);
defer allocator.free(serialized);
```

ZON frontmatter supports the full frontmatter subset: anonymous structs, array tuples, strings (with escape sequences), integers (decimal / hex / octal / binary), floats, booleans, `null`, and enum literals (returned as strings).

```zig
// ZON example
const source =
\\.{
\\ .title = "My Post",
\\ .tags = .{ "zig", "wasm" },
\\ .draft = false,
\\ .weight = 10,
\\ .status = .published, // enum literal → "published"
\\}
;
var fm = try FrontMatter.init(allocator, source, .zon);
defer fm.deinit();
```

### Library

A `Library` holds a collection of parsed Markdown documents with their frontmatter and lets you query across all of them using an extended dot-syntax.

```zig
const Library = zigmark.Library;

var lib = Library.init(allocator);
defer lib.deinit();

// In-memory: path is an optional identifier
try lib.add(source_a, "policies/access-control.md");
try lib.add(source_b, null); // anonymous document

// From disk: path is stored as the entry identifier
try lib.addFromFile("policies/hr.md");

// Recursively load all *.md files under a directory
try lib.addFromDir("policies/");

// Query returns ?[]Library.Result — null when nothing matches.
// Caller frees the slice; entry/block pointers are valid while the Library lives.
const results = try lib.query(allocator, "extra.owner=SC2 @heading") orelse return;
defer allocator.free(results);

for (results) |r| {
std.debug.print("path: {?s} confidence: {d:.1}\n", .{ r.entry.path, r.confidence });
if (r.block) |b| {
std.debug.print(" heading level {d}\n", .{b.heading.level});
}
}

// Sort results in-place by a frontmatter field (ascending or descending)
Library.sortBy(results, "title", true);
```

#### Query syntax

Tokens are whitespace-separated and may appear in any order.

| Token | Meaning |
|---|---|
| `path` | frontmatter field at `path` must exist |
| `path=value` | frontmatter field at `path` must equal `value` |
| `@block_type` | select blocks of this type from matching documents |

Multiple `path` / `path=value` tokens are **AND-combined**: a document must satisfy every filter to appear in results.

The dot-path syntax is identical to `Frontmatter` (`"title"`, `"extra.owner"`, `"taxonomies.SCF"`). Block type names match the `AST.Block` union tags (`heading`, `paragraph`, `code_block`, `fenced_code_block`, `blockquote`, `list`, `table`, …).

Without a `@block_type` token, one result per matching document is returned with `result.block == null`.

#### Examples

```zig
// All documents that have a title field
try lib.query(allocator, "title")

// Documents owned by SC2
try lib.query(allocator, "extra.owner=SC2")

// Documents owned by SC2 in the security category (AND filter)
try lib.query(allocator, "extra.owner=SC2 extra.category=security")

// Every heading across every document in the library
try lib.query(allocator, "@heading")

// Headings only from SC2-owned documents
try lib.query(allocator, "extra.owner=SC2 @heading")

// Fenced code blocks from documents tagged with a specific taxonomy entry
try lib.query(allocator, "taxonomies.SCF @fenced_code_block")
```

#### Result fields

| Field | Type | Description |
|---|---|---|
| `entry` | `*const Library.Entry` | The matching document (`.document`, `.frontmatter`, `.path`) |
| `block` | `?*const AST.Block` | The specific block that matched, or `null` for doc-level results |
| `confidence` | `f32` | Match confidence in `[0.0, 1.0]`; results sorted descending |

Documents without frontmatter are supported — they simply never match frontmatter filters.

#### Sorting

`Library.sortBy` sorts a result slice in-place by a frontmatter field value. String fields are compared lexicographically; integer and float fields are compared numerically. Results missing the field sort last.

```zig
// Sort by title A→Z
Library.sortBy(results, "title", true);

// Sort by date newest-first
Library.sortBy(results, "date", false);
```

### Streaming / Large Documents

For large documents, avoid building a full output string with `renderToWriter`, which writes directly to any `*std.Io.Writer`:

```zig
var out_buf: [8192]u8 = undefined;
var writer = file.writer(&out_buf);

// Render directly to a file — no intermediate allocation
try zigmark.HTMLRenderer.renderToWriter(allocator, &writer.interface, doc);
try writer.interface.flush();
```

All six built-in renderers (`HTMLRenderer`, `ASTRenderer`, `AIRenderer`, `TerminalRenderer`, `MarkdownRenderer`, `TypstRenderer`) expose `renderToWriter`. The Typst full-document variant is `zigmark.typst.renderDocumentToWriter`.

To parse from a stream (file, stdin, pipe, socket) without a `readToEndAlloc` call:

```zig
var read_buf: [4096]u8 = undefined;
var reader = file.reader(&read_buf);

var parser = zigmark.Parser.init();
var doc = try parser.parseFromReader(allocator, &reader.interface);
defer doc.deinit(allocator);
```

The returned `AST.Document` is fully self-contained — no external buffer needs to outlive it.

### Custom Renderers

Implement both `render` and `renderToWriter` to satisfy the `Renderer` interface:

```zig
pub fn render(allocator: Allocator, doc: AST.Document) ![]u8 { ... }
pub fn renderToWriter(allocator: Allocator, writer: *std.Io.Writer, doc: AST.Document) !void { ... }

const MyRenderer = zigmark.Renderer.create(my_backend);
const output = try MyRenderer.render(allocator, doc);
try MyRenderer.renderToWriter(allocator, &writer.interface, doc);
```

## C Shared Library

The build produces `libzigmark.so` and `include/zigmark.h` — a self-contained shared library with no libc dependency.

### C API

```c
#include "zigmark.h"

ZigmarkDocument *zigmark_parse(const char *input, size_t len);
void zigmark_free_document(ZigmarkDocument *doc);

char *zigmark_render_html(ZigmarkDocument *doc);
char *zigmark_render_ast(ZigmarkDocument *doc);
char *zigmark_render_ai(ZigmarkDocument *doc);
void zigmark_free_string(char *str);

const char *zigmark_version(void);

/* Frontmatter */
ZigmarkFrontmatter *zigmark_frontmatter_parse(const char *input, size_t len);
void zigmark_frontmatter_free(ZigmarkFrontmatter *fm);
char *zigmark_frontmatter_to_json(ZigmarkFrontmatter *fm);
char *zigmark_frontmatter_get(ZigmarkFrontmatter *fm, const char *key);
char *zigmark_frontmatter_serialize(ZigmarkFrontmatter *fm);
int zigmark_frontmatter_merge(ZigmarkFrontmatter *base, ZigmarkFrontmatter *overlay);
int zigmark_frontmatter_set(ZigmarkFrontmatter *fm, const char *path, const char *json_value);
int zigmark_frontmatter_set_raw(ZigmarkFrontmatter *fm, const char *path, const char *raw);
```

### Example

```c
#include
#include "zigmark.h"

int main(void) {
const char *md = "# Hello\n\nWorld.";
ZigmarkDocument *doc = zigmark_parse(md, 15);
if (!doc) return 1;

char *html = zigmark_render_html(doc);
if (html) { printf("%s", html); zigmark_free_string(html); }

zigmark_free_document(doc);
return 0;
}
```

### Compile and Link

```bash
zig build -Doptimize=ReleaseSafe
zig cc -o example example.c -Izig-out/include -Lzig-out/lib -lzigmark
LD_LIBRARY_PATH=zig-out/lib ./example
```

## Features

### CommonMark Compliance — 652/652 ✅

Every section of the [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/) spec passes:

| Section | Tests |
|---|---|
| Tabs | 11 |
| Backslash escapes | 13 |
| Entity and numeric character references | 17 |
| Precedence | 1 |
| Thematic breaks | 19 |
| ATX headings | 18 |
| Setext headings | 27 |
| Indented code blocks | 12 |
| Fenced code blocks | 29 |
| HTML blocks | 44 |
| Link reference definitions | 27 |
| Paragraphs | 8 |
| Blank lines | 1 |
| Block quotes | 25 |
| List items | 48 |
| Lists | 27 |
| Code spans | 22 |
| Emphasis and strong emphasis | 132 |
| Links | 90 |
| Images | 22 |
| Autolinks | 19 |
| Raw HTML | 20 |
| Hard line breaks | 15 |
| Soft line breaks | 2 |
| Textual content | 3 |

### GFM Extensions — 24/24 ✅

All [GitHub Flavored Markdown](https://github.github.com/gfm/) extensions pass.

| GFM Extension | Tests |
|---|---|
| Tables | 8/8 ✅ |
| Task list items | 2/2 ✅ |
| Strikethrough | 2/2 ✅ |
| Autolinks (extended) | 11/11 ✅ |
| Disallowed raw HTML | 1/1 ✅ |

**Tables** — pipe-delimited with column alignment (`---`, `:---`, `---:`, `:---:`):

```markdown
| Name | Role | Score |
| ----- | -------- | ----: |
| Alice | Engineer | 42 |
| Bob | Designer | 37 |
```

**Task lists** — checked and unchecked items render as disabled checkboxes:

```markdown
- [x] Done
- [ ] Not done
```

**Strikethrough** — `~~text~~` renders as `text`:

```markdown
~~deleted text~~
```

**Extended autolinks** — bare `www.` links, `http://`/`https://`/`ftp://` URLs, and bare email addresses are auto-linked without angle brackets:

```markdown
Visit www.example.com or https://example.com or email user@example.com
```

**Disallowed raw HTML** — the tags ``, ``, ``, `<xmp>`, `<iframe>`, `<noembed>`, `<noframes>`, `<script>`, and `<plaintext>` have their opening `<` escaped to `&lt;`.

Run the GFM suite with `zig build gfm`.

### Extensions

- **Frontmatter** — YAML (`---`), TOML (`+++`), JSON (`{`), and ZON (`.{`) extraction, all normalised to `std.json.Value`
- **Footnotes** — `[^label]` references and definitions
- **GFM Tables** — pipe-delimited tables with optional column alignment
- **GFM Task lists** — `- [x]` / `- [ ]` items rendered as disabled checkboxes
- **GFM Strikethrough** — `~~text~~` rendered as `<del>text</del>`
- **GFM Extended autolinks** — bare `www.`, `http(s)://`, `ftp://`, and email autolinks
- **GFM Disallowed raw HTML** — dangerous tags escaped at render time

## Building \& Testing

```bash
# Build CLI + shared library + docs
zig build

# Release build
zig build -Doptimize=ReleaseSafe

# Run unit tests
zig build test

# Run full CommonMark spec suite (summary)
zig build spec

# Run a single section with verbose failure output
zig build spec-emphasis

# Run GFM extension spec suite (summary)
zig build gfm

# Run a specific GFM extension section
zig build gfm-tables

# Generate docs
zig build docs
```

### Build Outputs

```
zig-out/
├── bin/zigmark # CLI executable
├── lib/libzigmark.so # C-callable shared library
├── include/zigmark.h # C header
├── docs/ # Generated documentation
├── wasm/ # WebAssembly module (zig build wasm)
│ ├── zigmark.wasm
│ └── index.html # Live preview demo
└── site/ # Combined site (zig build site)
├── zigmark.wasm
├── index.html # Playground
└── docs/ # API docs
```

### WASM

Build the WebAssembly module (\~81 KiB):

```bash
zig build wasm
```

Serve the live preview demo locally:

```bash
# With Python
python3 -m http.server 8080 -d zig-out/wasm

# With Nix
nix run .#wasm-demo
```

Open `http://localhost:8080` — the demo renders Markdown in real-time using the
WASM module and includes a side-by-side benchmark against [marked.js](https://marked.js.org/).

See `examples/wasm/` for the WASM entry point and demo source.

### Nix

```bash
# Build
nix build

# Run
nix run . -- README.md

# Dev shell (includes zls, benchmark tool, auto-updates zon2json-lock)
nix develop

# WASM live preview demo
nix run .#wasm-demo

# Build combined site (playground + docs) for zigmark.sc2.in
nix build .#site

# Run CLI performance benchmark (compares zigmark vs cmark, updates README)
nix run .#bench
```

Requires **Zig 0.15.2** or later.

## Architecture

- **`Parser`** — Block-level + inline two-pass parser built on the [mecha](https://github.com/Hejsil/mecha) parser combinator library; accepts a `[]const u8` via `parseMarkdown` or any `*std.Io.Reader` via `parseFromReader`
- **`AST`** — Typed union-based Abstract Syntax Tree (`Document` → `Block` → `Inline`)
- **`HTMLRenderer`** — CommonMark-compliant HTML serialiser; `renderHtmlWithMermaid` accepts an optional `pozeiden.render`-compatible function to convert `mermaid`/`mermaidjs` fenced blocks to inline SVG `<figure>` elements (falls back to a plain code block on error or when `null`)
- **`TypstRenderer`** — Typst markup renderer; `typst.renderDocument` adds an eisvogel-inspired preamble (title page, TOC, headers/footers, styled code blocks and blockquotes) driven by `DocumentOptions`; `renderTypstDocWithMermaid` converts `mermaid`/`mermaidjs` blocks to `#image.decode` calls
- **`ASTRenderer`** — Human-readable tree diagram with box-drawing characters
- **`AIRenderer`** — Token-efficient AST representation for LLM consumption
- **`MarkdownRenderer`** — AST→Markdown normaliser; converts headings to ATX, links to inline, code blocks to fenced
- **`Renderer`** — Type-erased vtable interface for pluggable output backends; exposes both `render → []u8` and `renderToWriter → void` paths
- **`Frontmatter`** — YAML/TOML/JSON/ZON metadata extraction, mutation (`set`, `delete`, `merge`), and re-serialisation; YAML via [zig-yaml](https://github.com/kubkon/zig-yaml), TOML via [tomlz](https://github.com/tsunaminoai/tomlz), JSON via `std.json`, ZON via a built-in recursive-descent parser
- **`Library`** — Queryable collection of documents; AND-combined frontmatter filters, block-type selectors (`@heading`, `@code_block`, …), confidence-ranked results, `addFromFile`/`addFromDir` bulk loading, and `sortBy` for in-place result ordering
- **C ABI** — Opaque-pointer API in `root.zig` exported as `libzigmark.so`

## Performance

<!-- bench-start -->

_Last updated: 2026-04-20 · input: `README.md` (31 KB) · run `nix run .#bench` to reproduce_

### Speed

| Command | Mean \[ms\] | Min \[ms\] | Max \[ms\] | Relative |
|:---|---:|---:|---:|---:|
| `lowdown` | 2.6 ± 1.4 | 1.3 | 13.2 | 1.00 |
| **`zigmark (ReleaseFast)`** | 3.5 ± 2.1 | 1.9 | 24.3 | 1.38 ± 1.12 |
| `discount` | 3.5 ± 1.8 | 1.7 | 18.5 | 1.37 ± 1.04 |
| **`zigmark (ReleaseSafe)`** | 4.0 ± 2.3 | 2.0 | 20.3 | 1.55 ± 1.24 |
| **`zigmark (ReleaseSmall)`** | 4.0 ± 1.6 | 2.3 | 18.6 | 1.57 ± 1.08 |
| `cmark-gfm` | 5.6 ± 1.9 | 3.2 | 17.4 | 2.20 ± 1.45 |
| `cmark` | 5.9 ± 2.3 | 3.1 | 23.7 | 2.31 ± 1.59 |
| `pandoc` | 211.4 ± 34.5 | 167.7 | 278.4 | 1.00 |

### Memory (peak RSS)

| Command | Peak RSS (KB) |
|:---|---:|
| **`zigmark (ReleaseSmall)`** | 1856 |
| **`zigmark (ReleaseFast)`** | 2116 |
| `discount` | 2168 |
| **`zigmark (ReleaseSafe)`** | 2372 |
| `lowdown` | 3040 |
| `cmark` | 4104 |
| `cmark-gfm` | 4168 |
| `pandoc` | 128628 |

<!-- bench-end -->

## Future Plans

- Additional renderers (plain text)
- AST modification API

## License

[PolyForm Noncommercial 1.0.0](LICENSE) © 2025 Star City Security Consulting, LLC (SC2)

Free to use for any **noncommercial** purpose — personal projects, research,
education, nonprofits, and government institutions are all welcome.

**Commercial use requires a separate licence.** If you or your organisation
intend to profit from zigmark (products, SaaS, consulting work billed to a
client, etc.) contact **<inquiries@sc2.in>**. Commercial licensees also get
priority support and the option to sponsor features.

**Solo practitioners and independent consultants** using zigmark as a tool in
their own practice — not reselling it or embedding it in a product — are
welcome to use it without a commercial licence.

## Contributing

Contributions are welcome. By submitting a pull request you agree that your
contribution is licensed under the same terms as the rest of
this project.

### Security

**Do not open a public issue for security vulnerabilities.**

If you discover a security issue, please report it responsibly by emailing
**<security@sc2.in>** with a description of the vulnerability, steps to
reproduce, and any relevant details. You will receive acknowledgement within 72
hours and we will work with you on a fix before any public disclosure.

### Guidelines

- **Tests must pass.** Run `zig build test` (unit), `zig build spec` (all
652 CommonMark spec tests), and `zig build gfm` (all 24 GFM extension
tests) before opening a PR.
- **One concern per PR.** Keep pull requests focused — a bug fix, a new
feature, or a refactor, not all three at once.
- **No spec regressions.** The 652/652 CommonMark 0.31.2 and 24/24 GFM
pass rates are the baseline. PRs that cause spec failures will not be merged.
- **Follow existing style.** The codebase uses `zig fmt`-standard formatting
and descriptive naming. When in doubt, match what's already there.
- **Document public API changes.** If you add or change an exported function,
update the README and/or `include/zigmark.h` accordingly.
- **Sign your commits.** Use `git commit -s` to add a `Signed-off-by` line
([DCO](https://developercertificate.org/)).