{"id":49130391,"url":"https://github.com/sc2in/zigmark","last_synced_at":"2026-06-06T06:04:55.951Z","repository":{"id":348584303,"uuid":"1173706707","full_name":"sc2in/zigmark","owner":"sc2in","description":"Simple markdown library for zig","archived":false,"fork":false,"pushed_at":"2026-06-02T23:09:15.000Z","size":575,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T00:17:27.588Z","etag":null,"topics":["cli","library","markdown","parser","renderer","zig"],"latest_commit_sha":null,"homepage":"https://zigmark.sc2.in","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sc2in.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-05T16:58:26.000Z","updated_at":"2026-04-21T16:01:28.000Z","dependencies_parsed_at":"2026-04-21T18:00:35.409Z","dependency_job_id":null,"html_url":"https://github.com/sc2in/zigmark","commit_stats":null,"previous_names":["sc2in/zigmark"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/sc2in/zigmark","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sc2in%2Fzigmark","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sc2in%2Fzigmark/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sc2in%2Fzigmark/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sc2in%2Fzigmark/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sc2in","download_url":"https://codeload.github.com/sc2in/zigmark/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sc2in%2Fzigmark/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33971112,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","library","markdown","parser","renderer","zig"],"created_at":"2026-04-21T18:00:28.430Z","updated_at":"2026-06-06T06:04:55.913Z","avatar_url":"https://github.com/sc2in.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# zigmark\n\n[![CI](https://github.com/sc2in/zigmark/actions/workflows/ci.yml/badge.svg)](https://github.com/sc2in/zigmark/actions/workflows/ci.yml)\n\nA CommonMark-compliant Markdown parser and renderer for Zig. Passes **all 652 CommonMark spec tests** and **all 24 GFM extension tests** (100%).\n\nRenders to **HTML**, **Typst** (PDF-ready), **AST**, and more.\n\nBuilds as both a **CLI tool** and a **C-callable shared library** (`libzigmark.so`).\n\n## Installation\n\nAdd `zigmark` as a dependency in your `build.zig.zon`:\n\n```zig\n.dependencies = .{\n    .zigmark = .{\n        .url = \"https://github.com/sc2in/zigmark/archive/\u003ccommit\u003e.tar.gz\",\n        .hash = \"...\",\n    },\n}\n```\n\nThen in your `build.zig`:\n\n```zig\nconst zigmark = b.dependency(\"zigmark\", .{ .target = target, .optimize = optimize });\nexe.root_module.addImport(\"zigmark\", zigmark.module(\"zigmark\"));\n```\n\n## CLI Usage\n\n```bash\nzig build\n```\n\n### Convert Markdown to HTML\n\n```bash\n# From a file\nzigmark README.md\n\n# From stdin\necho '# Hello' | zigmark\n\n# Write to a file\nzigmark -o output.html README.md\n```\n\n### Inspect the AST\n\n```bash\necho '**bold** and *italic*' | zigmark -f ast\n```\n\n```\nDocument\n└── Paragraph\n    ├── Strong ('*')\n    │   └── Text \"bold\"\n    ├── Text \" and \"\n    └── Emphasis ('*')\n        └── Text \"italic\"\n```\n\n### Convert Markdown to Typst (PDF)\n\n```bash\n# Body-only Typst markup (embed in your own document)\necho '# Hello' | zigmark -f typst\n\n# Full document with eisvogel-inspired preamble — pipe straight to typst\nzigmark -f typst report.md | typst compile - report.pdf\n```\n\nYAML frontmatter fields are automatically mapped to document options:\n\n```markdown\n---\ntitle: \"My Report\"\nauthor: Alice\ndate: 2026-03-19\ntitlepage: true\ntoc: true\nnumbersections: true\ncolorlinks: true\nheader-right: \"Confidential\"\nfooter-left: \"Alice\"\n---\n\n# Introduction\n\nHello **world**.\n```\n\nSupported frontmatter fields:\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `title` | string | — | Document title |\n| `subtitle` | string | — | Subtitle shown on title page |\n| `author` | string or list | — | Author name(s); list uses the first entry |\n| `date` | string | — | Date shown on title page |\n| `lang` | string | `en` | Document language |\n| `paper` | string | `a4` | Paper size (e.g. `a4`, `us-letter`) |\n| `fontsize` | string | `11pt` | Base font size |\n| `titlepage` | bool | `false` | Generate a full-bleed title page |\n| `titlepage-color` | string | `1E3A5F` | Title page background (hex, no `#`) |\n| `titlepage-text-color` | string | `FFFFFF` | Title page text colour |\n| `titlepage-rule-color` | string | `AAAAAA` | Title page rule colour |\n| `titlepage-rule-height` | number | `4` | Title page rule thickness (pt) |\n| `toc` | bool | `false` | Insert a table of contents |\n| `toc-title` | string | `Contents` | TOC heading |\n| `toc-own-page` | bool | `false` | _(reserved, not yet implemented)_ |\n| `toc-depth` | number | `3` | TOC depth |\n| `numbersections` | bool | `false` | Number headings |\n| `disable-header-and-footer` | bool | `false` | Suppress page header and footer |\n| `header-left` / `header-center` / `header-right` | string | title / — / date | Header slots |\n| `footer-left` / `footer-center` / `footer-right` | string | author / — / page\\# | Footer slots |\n| `colorlinks` | bool | `true` | Colour hyperlinks |\n| `linkcolor` | string | `A50000` | Internal link colour (hex) |\n| `urlcolor` | string | `4077C0` | URL link colour (hex) |\n\n### AI-Friendly Output\n\n```bash\nzigmark -f ai README.md\n```\n\nProduces a token-efficient AST representation suitable for LLM consumption.\n\n### Extract Frontmatter as JSON\n\n```bash\nzigmark -f frontmatter post.md\n```\n\nParses the frontmatter block (YAML `---`, TOML `+++`, JSON `{`, or ZON `.{`) and\nemits it as pretty-printed JSON. Outputs `{}` when no frontmatter is present,\nso the output is always valid JSON and safe to pipe.\n\n```bash\n# Pipe into jq\nzigmark -f frontmatter post.md | jq '.title'\n\n# Extract a nested key\nzigmark -f frontmatter post.md | jq '.extra.author'\n```\n\n### Edit Frontmatter\n\n`--format markdown` re-serialises the frontmatter (in its original format) and\npasses the body through verbatim. Use `--set` and `--delete` to mutate fields\nbefore writing:\n\n```bash\n# Update a field and delete another, keep body unchanged\nzigmark -f markdown --set title=\"New Title\" --delete draft post.md\n\n# Set a nested key (intermediate objects are created automatically)\nzigmark -f markdown --set extra.owner=SC2 post.md\n\n# Pipe the result back over the original file\nzigmark -f markdown --set date=2025-06-01 post.md -o post.md\n```\n\n`--format normalize` does the same frontmatter handling but also reconstructs\nthe Markdown body from the AST, normalising headings to ATX style, links to\ninline, and code blocks to fenced:\n\n```bash\nzigmark -f normalize --set title=\"Clean\" post.md\n```\n\n### Edit Body Blocks\n\n`--set-block` replaces a single block in the document body using a\n`type[N]` selector — the same bracket syntax used by the AST query API.\n`type` is any block tag (`block`, `heading`, `paragraph`, `table`, …);\n`N` is a zero-based index. The right-hand side is parsed as Markdown and\nits first block becomes the replacement. Applies to `normalize` format.\n\n```bash\n# Replace the first heading\nzigmark -f normalize --set-block 'heading[0]=# New Title' post.md\n\n# Replace a block at an absolute index (any type)\nzigmark -f normalize --set-block 'block[3]=Updated paragraph text.' post.md\n\n# Replace the second table\nzigmark -f normalize --set-block 'table[1]=| A | B |\\n|---|---|\\n| 1 | 2 |' post.md\n\n# Combine with frontmatter edits\nzigmark -f normalize --set title=\"Clean\" --set-block 'heading[0]=# Clean' post.md -o post.md\n```\n\n`--section-start` and `--section-end` replace every block _between_ two\nHTML comment markers (the markers themselves are preserved). Replacement\nMarkdown is read from stdin; the document file must be given as a\npositional argument. Applies to `normalize` format.\n\n```bash\n# Replace the content between \u003c!-- bench-start --\u003e and \u003c!-- bench-end --\u003e\ncat new-perf-tables.md | zigmark -f normalize \\\n  --section-start bench-start \\\n  --section-end   bench-end   \\\n  README.md -o README.md\n```\n\nThis is how `nix run .#bench` updates the performance section of this\nREADME — it generates the new table Markdown, then uses zigmark's own AST\nmutation API to splice it in, replacing the Python regex that did the same\njob before.\n\n### Options\n\n```\nUsage: zigmark [OPTIONS] [FILE]\n\n  -h, --help                  Display this help and exit.\n  -v, --version               Print version and exit.\n  -f, --format \u003cstr\u003e          Output format: \"html\" (default), \"typst\", \"ast\",\n                              \"ai\", \"terminal\", \"frontmatter\", \"markdown\", or\n                              \"normalize\".\n  -o, --output \u003cstr\u003e          Write output to FILE instead of stdout.\n  -s, --set \u003cstr\u003e...          Set a frontmatter field (KEY=VALUE). Repeatable.\n                              Applies to: markdown, normalize, frontmatter.\n  -d, --delete \u003cstr\u003e...       Delete a frontmatter field (dot-path). Repeatable.\n                              Applies to: markdown, normalize, frontmatter.\n  -e, --set-block \u003cstr\u003e...    Edit a body block (SELECTOR=CONTENT). Selectors:\n                              block[N], heading[N], paragraph[N], table[N].\n                              First block of CONTENT replaces the target.\n                              Repeatable. Applies to: normalize.\n      --section-start \u003cstr\u003e   ) Replace document body between two HTML comment\n      --section-end   \u003cstr\u003e   ) markers with Markdown from stdin. FILE required.\n                                Applies to: normalize.\n```\n\n## Zig Library Usage\n\n### Basic Parsing and Rendering\n\n```zig\nconst std = @import(\"std\");\nconst zigmark = @import(\"zigmark\");\n\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    defer _ = gpa.deinit();\n    const allocator = gpa.allocator();\n\n    const markdown =\n        \\\\# Hello World\n        \\\\\n        \\\\This is a **bold** paragraph with a [link](https://sc2.in).\n        \\\\\n        \\\\- List item 1\n        \\\\- List item 2\n    ;\n\n    var parser = zigmark.Parser.init();\n    var doc = try parser.parseMarkdown(allocator, markdown);\n    defer doc.deinit(allocator);\n\n    const html = try zigmark.HTMLRenderer.render(allocator, doc);\n    defer allocator.free(html);\n\n    std.debug.print(\"{s}\\n\", .{html});\n}\n```\n\n### Typst Rendering\n\nGenerate Typst markup from a parsed document:\n\n```zig\nconst zigmark = @import(\"zigmark\");\n\n// Body-only (embed in your own Typst document)\nconst markup = try zigmark.TypstRenderer.render(allocator, doc);\ndefer allocator.free(markup);\n\n// Full document with eisvogel-inspired preamble\nconst opts = zigmark.typst.DocumentOptions{\n    .title          = \"My Report\",\n    .author         = \"Alice\",\n    .date           = \"2026-03-19\",\n    .titlepage      = true,\n    .toc            = true,\n    .numbersections = true,\n    .colorlinks     = true,\n};\nconst full = try zigmark.typst.renderDocument(allocator, doc, opts);\ndefer allocator.free(full);\n```\n\n### AST Query System\n\nNavigate the parsed document with jQuery-like selectors:\n\n```zig\nvar query = doc.get();\n\n// Get all headings (optionally filter by level)\nvar headings = try query.headings(allocator, null);\nvar h2s = try query.headings(allocator, 2);\n\n// Get all links\nvar links = try query.links(allocator);\n\n// Count elements\nconst para_count = query.count(.paragraph);\n```\n\n### Frontmatter\n\nExtract and query structured metadata from the top of a Markdown file. All four formats are normalised to `std.json.Value` for uniform access.\n\n| Format | Opening marker | Example |\n|---|---|---|\n| YAML | `---` | `--- \\ntitle: Hello\\n---` |\n| TOML | `+++` | `+++\\ntitle = \"Hello\"\\n+++` |\n| JSON | `{` | `{\"title\": \"Hello\"}` |\n| ZON | `.{` | `.{ .title = \"Hello\" }` |\n\n```zig\nconst FrontMatter = zigmark.FrontMatter;\n\n// Parse from a full Markdown document (auto-detects format)\nvar fm = try FrontMatter.initFromMarkdown(allocator, markdown_source);\ndefer fm.deinit();\n\n// Dot-separated key lookup — returns ?std.json.Value\nconst title  = fm.get(\"title\");            // top-level key\nconst host   = fm.get(\"server.host\");      // nested key\nconst first  = fm.get(\"tags\");             // array → .array variant\n\nif (title) |t| std.debug.print(\"title: {s}\\n\", .{t.string});\n\n// Or parse a bare frontmatter string directly\nvar fm2 = try FrontMatter.init(allocator, source, .toml);\ndefer fm2.deinit();\n\n// Mutate: set a value at a dot-separated path (creates intermediates as needed)\ntry fm.set(\"title\", .{ .string = \"New Title\" });\ntry fm.set(\"extra.owner\", .{ .string = \"SC2\" });\ntry fm.set(\"draft\", .{ .bool = false });\n\n// Delete a key\n_ = fm.delete(\"draft\");\n\n// Deep-merge another frontmatter document (overlay keys win on conflict)\nvar overlay = try FrontMatter.initFromMarkdown(allocator, other_source);\ndefer overlay.deinit();\ntry fm.merge(overlay);\n\n// Re-serialise in the original format (YAML/TOML/JSON/ZON)\nconst serialized = try fm.serialize(allocator);\ndefer allocator.free(serialized);\n```\n\nZON 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).\n\n```zig\n// ZON example\nconst source =\n    \\\\.{\n    \\\\    .title   = \"My Post\",\n    \\\\    .tags    = .{ \"zig\", \"wasm\" },\n    \\\\    .draft   = false,\n    \\\\    .weight  = 10,\n    \\\\    .status  = .published,   // enum literal → \"published\"\n    \\\\}\n;\nvar fm = try FrontMatter.init(allocator, source, .zon);\ndefer fm.deinit();\n```\n\n### Library\n\nA `Library` holds a collection of parsed Markdown documents with their frontmatter and lets you query across all of them using an extended dot-syntax.\n\n```zig\nconst Library = zigmark.Library;\n\nvar lib = Library.init(allocator);\ndefer lib.deinit();\n\n// In-memory: path is an optional identifier\ntry lib.add(source_a, \"policies/access-control.md\");\ntry lib.add(source_b, null); // anonymous document\n\n// From disk: path is stored as the entry identifier\ntry lib.addFromFile(\"policies/hr.md\");\n\n// Recursively load all *.md files under a directory\ntry lib.addFromDir(\"policies/\");\n\n// Query returns ?[]Library.Result — null when nothing matches.\n// Caller frees the slice; entry/block pointers are valid while the Library lives.\nconst results = try lib.query(allocator, \"extra.owner=SC2 @heading\") orelse return;\ndefer allocator.free(results);\n\nfor (results) |r| {\n    std.debug.print(\"path: {?s}  confidence: {d:.1}\\n\", .{ r.entry.path, r.confidence });\n    if (r.block) |b| {\n        std.debug.print(\"  heading level {d}\\n\", .{b.heading.level});\n    }\n}\n\n// Sort results in-place by a frontmatter field (ascending or descending)\nLibrary.sortBy(results, \"title\", true);\n```\n\n#### Query syntax\n\nTokens are whitespace-separated and may appear in any order.\n\n| Token | Meaning |\n|---|---|\n| `path` | frontmatter field at `path` must exist |\n| `path=value` | frontmatter field at `path` must equal `value` |\n| `@block_type` | select blocks of this type from matching documents |\n\nMultiple `path` / `path=value` tokens are **AND-combined**: a document must satisfy every filter to appear in results.\n\nThe 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`, …).\n\nWithout a `@block_type` token, one result per matching document is returned with `result.block == null`.\n\n#### Examples\n\n```zig\n// All documents that have a title field\ntry lib.query(allocator, \"title\")\n\n// Documents owned by SC2\ntry lib.query(allocator, \"extra.owner=SC2\")\n\n// Documents owned by SC2 in the security category (AND filter)\ntry lib.query(allocator, \"extra.owner=SC2 extra.category=security\")\n\n// Every heading across every document in the library\ntry lib.query(allocator, \"@heading\")\n\n// Headings only from SC2-owned documents\ntry lib.query(allocator, \"extra.owner=SC2 @heading\")\n\n// Fenced code blocks from documents tagged with a specific taxonomy entry\ntry lib.query(allocator, \"taxonomies.SCF @fenced_code_block\")\n```\n\n#### Result fields\n\n| Field | Type | Description |\n|---|---|---|\n| `entry` | `*const Library.Entry` | The matching document (`.document`, `.frontmatter`, `.path`) |\n| `block` | `?*const AST.Block` | The specific block that matched, or `null` for doc-level results |\n| `confidence` | `f32` | Match confidence in `[0.0, 1.0]`; results sorted descending |\n\nDocuments without frontmatter are supported — they simply never match frontmatter filters.\n\n#### Sorting\n\n`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.\n\n```zig\n// Sort by title A→Z\nLibrary.sortBy(results, \"title\", true);\n\n// Sort by date newest-first\nLibrary.sortBy(results, \"date\", false);\n```\n\n### Streaming / Large Documents\n\nFor large documents, avoid building a full output string with `renderToWriter`, which writes directly to any `*std.Io.Writer`:\n\n```zig\nvar out_buf: [8192]u8 = undefined;\nvar writer = file.writer(\u0026out_buf);\n\n// Render directly to a file — no intermediate allocation\ntry zigmark.HTMLRenderer.renderToWriter(allocator, \u0026writer.interface, doc);\ntry writer.interface.flush();\n```\n\nAll six built-in renderers (`HTMLRenderer`, `ASTRenderer`, `AIRenderer`, `TerminalRenderer`, `MarkdownRenderer`, `TypstRenderer`) expose `renderToWriter`. The Typst full-document variant is `zigmark.typst.renderDocumentToWriter`.\n\nTo parse from a stream (file, stdin, pipe, socket) without a `readToEndAlloc` call:\n\n```zig\nvar read_buf: [4096]u8 = undefined;\nvar reader = file.reader(\u0026read_buf);\n\nvar parser = zigmark.Parser.init();\nvar doc = try parser.parseFromReader(allocator, \u0026reader.interface);\ndefer doc.deinit(allocator);\n```\n\nThe returned `AST.Document` is fully self-contained — no external buffer needs to outlive it.\n\n### Custom Renderers\n\nImplement both `render` and `renderToWriter` to satisfy the `Renderer` interface:\n\n```zig\npub fn render(allocator: Allocator, doc: AST.Document) ![]u8 { ... }\npub fn renderToWriter(allocator: Allocator, writer: *std.Io.Writer, doc: AST.Document) !void { ... }\n\nconst MyRenderer = zigmark.Renderer.create(my_backend);\nconst output = try MyRenderer.render(allocator, doc);\ntry MyRenderer.renderToWriter(allocator, \u0026writer.interface, doc);\n```\n\n## C Shared Library\n\nThe build produces `libzigmark.so` and `include/zigmark.h` — a self-contained shared library with no libc dependency.\n\n### C API\n\n```c\n#include \"zigmark.h\"\n\nZigmarkDocument  *zigmark_parse(const char *input, size_t len);\nvoid              zigmark_free_document(ZigmarkDocument *doc);\n\nchar             *zigmark_render_html(ZigmarkDocument *doc);\nchar             *zigmark_render_ast(ZigmarkDocument *doc);\nchar             *zigmark_render_ai(ZigmarkDocument *doc);\nvoid              zigmark_free_string(char *str);\n\nconst char       *zigmark_version(void);\n\n/* Frontmatter */\nZigmarkFrontmatter *zigmark_frontmatter_parse(const char *input, size_t len);\nvoid                zigmark_frontmatter_free(ZigmarkFrontmatter *fm);\nchar               *zigmark_frontmatter_to_json(ZigmarkFrontmatter *fm);\nchar               *zigmark_frontmatter_get(ZigmarkFrontmatter *fm, const char *key);\nchar               *zigmark_frontmatter_serialize(ZigmarkFrontmatter *fm);\nint                 zigmark_frontmatter_merge(ZigmarkFrontmatter *base, ZigmarkFrontmatter *overlay);\nint                 zigmark_frontmatter_set(ZigmarkFrontmatter *fm, const char *path, const char *json_value);\nint                 zigmark_frontmatter_set_raw(ZigmarkFrontmatter *fm, const char *path, const char *raw);\n```\n\n### Example\n\n```c\n#include \u003cstdio.h\u003e\n#include \"zigmark.h\"\n\nint main(void) {\n    const char *md = \"# Hello\\n\\nWorld.\";\n    ZigmarkDocument *doc = zigmark_parse(md, 15);\n    if (!doc) return 1;\n\n    char *html = zigmark_render_html(doc);\n    if (html) { printf(\"%s\", html); zigmark_free_string(html); }\n\n    zigmark_free_document(doc);\n    return 0;\n}\n```\n\n### Compile and Link\n\n```bash\nzig build -Doptimize=ReleaseSafe\nzig cc -o example example.c -Izig-out/include -Lzig-out/lib -lzigmark\nLD_LIBRARY_PATH=zig-out/lib ./example\n```\n\n## Features\n\n### CommonMark Compliance — 652/652 ✅\n\nEvery section of the [CommonMark 0.31.2](https://spec.commonmark.org/0.31.2/) spec passes:\n\n| Section | Tests |\n|---|---|\n| Tabs | 11 |\n| Backslash escapes | 13 |\n| Entity and numeric character references | 17 |\n| Precedence | 1 |\n| Thematic breaks | 19 |\n| ATX headings | 18 |\n| Setext headings | 27 |\n| Indented code blocks | 12 |\n| Fenced code blocks | 29 |\n| HTML blocks | 44 |\n| Link reference definitions | 27 |\n| Paragraphs | 8 |\n| Blank lines | 1 |\n| Block quotes | 25 |\n| List items | 48 |\n| Lists | 27 |\n| Code spans | 22 |\n| Emphasis and strong emphasis | 132 |\n| Links | 90 |\n| Images | 22 |\n| Autolinks | 19 |\n| Raw HTML | 20 |\n| Hard line breaks | 15 |\n| Soft line breaks | 2 |\n| Textual content | 3 |\n\n### GFM Extensions — 24/24 ✅\n\nAll [GitHub Flavored Markdown](https://github.github.com/gfm/) extensions pass.\n\n| GFM Extension | Tests |\n|---|---|\n| Tables | 8/8 ✅ |\n| Task list items | 2/2 ✅ |\n| Strikethrough | 2/2 ✅ |\n| Autolinks (extended) | 11/11 ✅ |\n| Disallowed raw HTML | 1/1 ✅ |\n\n**Tables** — pipe-delimited with column alignment (`---`, `:---`, `---:`, `:---:`):\n\n```markdown\n| Name  | Role     | Score |\n| ----- | -------- | ----: |\n| Alice | Engineer |    42 |\n| Bob   | Designer |    37 |\n```\n\n**Task lists** — checked and unchecked items render as disabled checkboxes:\n\n```markdown\n- [x] Done\n- [ ] Not done\n```\n\n**Strikethrough** — `~~text~~` renders as `\u003cdel\u003etext\u003c/del\u003e`:\n\n```markdown\n~~deleted text~~\n```\n\n**Extended autolinks** — bare `www.` links, `http://`/`https://`/`ftp://` URLs, and bare email addresses are auto-linked without angle brackets:\n\n```markdown\nVisit www.example.com or https://example.com or email user@example.com\n```\n\n**Disallowed raw HTML** — the tags `\u003ctitle\u003e`, `\u003ctextarea\u003e`, `\u003cstyle\u003e`, `\u003cxmp\u003e`, `\u003ciframe\u003e`, `\u003cnoembed\u003e`, `\u003cnoframes\u003e`, `\u003cscript\u003e`, and `\u003cplaintext\u003e` have their opening `\u003c` escaped to `\u0026lt;`.\n\nRun the GFM suite with `zig build gfm`.\n\n### Extensions\n\n- **Frontmatter** — YAML (`---`), TOML (`+++`), JSON (`{`), and ZON (`.{`) extraction, all normalised to `std.json.Value`\n- **Footnotes** — `[^label]` references and definitions\n- **GFM Tables** — pipe-delimited tables with optional column alignment\n- **GFM Task lists** — `- [x]` / `- [ ]` items rendered as disabled checkboxes\n- **GFM Strikethrough** — `~~text~~` rendered as `\u003cdel\u003etext\u003c/del\u003e`\n- **GFM Extended autolinks** — bare `www.`, `http(s)://`, `ftp://`, and email autolinks\n- **GFM Disallowed raw HTML** — dangerous tags escaped at render time\n\n## Building \\\u0026 Testing\n\n```bash\n# Build CLI + shared library + docs\nzig build\n\n# Release build\nzig build -Doptimize=ReleaseSafe\n\n# Run unit tests\nzig build test\n\n# Run full CommonMark spec suite (summary)\nzig build spec\n\n# Run a single section with verbose failure output\nzig build spec-emphasis\n\n# Run GFM extension spec suite (summary)\nzig build gfm\n\n# Run a specific GFM extension section\nzig build gfm-tables\n\n# Generate docs\nzig build docs\n```\n\n### Build Outputs\n\n```\nzig-out/\n├── bin/zigmark           # CLI executable\n├── lib/libzigmark.so     # C-callable shared library\n├── include/zigmark.h     # C header\n├── docs/                 # Generated documentation\n├── wasm/                 # WebAssembly module (zig build wasm)\n│   ├── zigmark.wasm\n│   └── index.html        # Live preview demo\n└── site/                 # Combined site (zig build site)\n    ├── zigmark.wasm\n    ├── index.html        # Playground\n    └── docs/             # API docs\n```\n\n### WASM\n\nBuild the WebAssembly module (\\~81 KiB):\n\n```bash\nzig build wasm\n```\n\nServe the live preview demo locally:\n\n```bash\n# With Python\npython3 -m http.server 8080 -d zig-out/wasm\n\n# With Nix\nnix run .#wasm-demo\n```\n\nOpen `http://localhost:8080` — the demo renders Markdown in real-time using the\nWASM module and includes a side-by-side benchmark against [marked.js](https://marked.js.org/).\n\nSee `examples/wasm/` for the WASM entry point and demo source.\n\n### Nix\n\n```bash\n# Build\nnix build\n\n# Run\nnix run . -- README.md\n\n# Dev shell (includes zls, benchmark tool, auto-updates zon2json-lock)\nnix develop\n\n# WASM live preview demo\nnix run .#wasm-demo\n\n# Build combined site (playground + docs) for zigmark.sc2.in\nnix build .#site\n\n# Run CLI performance benchmark (compares zigmark vs cmark, updates README)\nnix run .#bench\n```\n\nRequires **Zig 0.15.2** or later.\n\n## Architecture\n\n- **`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`\n- **`AST`** — Typed union-based Abstract Syntax Tree (`Document` → `Block` → `Inline`)\n- **`HTMLRenderer`** — CommonMark-compliant HTML serialiser; `renderHtmlWithMermaid` accepts an optional `pozeiden.render`-compatible function to convert `mermaid`/`mermaidjs` fenced blocks to inline SVG `\u003cfigure\u003e` elements (falls back to a plain code block on error or when `null`)\n- **`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\n- **`ASTRenderer`** — Human-readable tree diagram with box-drawing characters\n- **`AIRenderer`** — Token-efficient AST representation for LLM consumption\n- **`MarkdownRenderer`** — AST→Markdown normaliser; converts headings to ATX, links to inline, code blocks to fenced\n- **`Renderer`** — Type-erased vtable interface for pluggable output backends; exposes both `render → []u8` and `renderToWriter → void` paths\n- **`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\n- **`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\n- **C ABI** — Opaque-pointer API in `root.zig` exported as `libzigmark.so`\n\n## Performance\n\n\u003c!-- bench-start --\u003e\n\n_Last updated: 2026-04-20 · input: `README.md` (31 KB) · run `nix run .#bench` to reproduce_\n\n### Speed\n\n| Command | Mean \\[ms\\] | Min \\[ms\\] | Max \\[ms\\] | Relative |\n|:---|---:|---:|---:|---:|\n| `lowdown` | 2.6 ± 1.4 | 1.3 | 13.2 | 1.00 |\n| **`zigmark (ReleaseFast)`** | 3.5 ± 2.1 | 1.9 | 24.3 | 1.38 ± 1.12 |\n| `discount` | 3.5 ± 1.8 | 1.7 | 18.5 | 1.37 ± 1.04 |\n| **`zigmark (ReleaseSafe)`** | 4.0 ± 2.3 | 2.0 | 20.3 | 1.55 ± 1.24 |\n| **`zigmark (ReleaseSmall)`** | 4.0 ± 1.6 | 2.3 | 18.6 | 1.57 ± 1.08 |\n| `cmark-gfm` | 5.6 ± 1.9 | 3.2 | 17.4 | 2.20 ± 1.45 |\n| `cmark` | 5.9 ± 2.3 | 3.1 | 23.7 | 2.31 ± 1.59 |\n| `pandoc` | 211.4 ± 34.5 | 167.7 | 278.4 | 1.00 |\n\n### Memory (peak RSS)\n\n| Command | Peak RSS (KB) |\n|:---|---:|\n| **`zigmark (ReleaseSmall)`** | 1856 |\n| **`zigmark (ReleaseFast)`** | 2116 |\n| `discount` | 2168 |\n| **`zigmark (ReleaseSafe)`** | 2372 |\n| `lowdown` | 3040 |\n| `cmark` | 4104 |\n| `cmark-gfm` | 4168 |\n| `pandoc` | 128628 |\n\n\u003c!-- bench-end --\u003e\n\n## Future Plans\n\n- Additional renderers (plain text)\n- AST modification API\n\n## License\n\n[PolyForm Noncommercial 1.0.0](LICENSE) © 2025 Star City Security Consulting, LLC (SC2)\n\nFree to use for any **noncommercial** purpose — personal projects, research,\neducation, nonprofits, and government institutions are all welcome.\n\n**Commercial use requires a separate licence.** If you or your organisation\nintend to profit from zigmark (products, SaaS, consulting work billed to a\nclient, etc.) contact **\u003cinquiries@sc2.in\u003e**. Commercial licensees also get\npriority support and the option to sponsor features.\n\n**Solo practitioners and independent consultants** using zigmark as a tool in\ntheir own practice — not reselling it or embedding it in a product — are\nwelcome to use it without a commercial licence.\n\n## Contributing\n\nContributions are welcome. By submitting a pull request you agree that your\ncontribution is licensed under the same terms as the rest of\nthis project.\n\n### Security\n\n**Do not open a public issue for security vulnerabilities.**\n\nIf you discover a security issue, please report it responsibly by emailing\n**\u003csecurity@sc2.in\u003e** with a description of the vulnerability, steps to\nreproduce, and any relevant details. You will receive acknowledgement within 72\nhours and we will work with you on a fix before any public disclosure.\n\n### Guidelines\n\n- **Tests must pass.** Run `zig build test` (unit), `zig build spec` (all\n  652 CommonMark spec tests), and `zig build gfm` (all 24 GFM extension\n  tests) before opening a PR.\n- **One concern per PR.** Keep pull requests focused — a bug fix, a new\n  feature, or a refactor, not all three at once.\n- **No spec regressions.** The 652/652 CommonMark 0.31.2 and 24/24 GFM\n  pass rates are the baseline. PRs that cause spec failures will not be merged.\n- **Follow existing style.** The codebase uses `zig fmt`-standard formatting\n  and descriptive naming. When in doubt, match what's already there.\n- **Document public API changes.** If you add or change an exported function,\n  update the README and/or `include/zigmark.h` accordingly.\n- **Sign your commits.** Use `git commit -s` to add a `Signed-off-by` line\n  ([DCO](https://developercertificate.org/)).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsc2in%2Fzigmark","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsc2in%2Fzigmark","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsc2in%2Fzigmark/lists"}