marvdown

This is Marvdown ⚡️ A stupid simple Markdown parser

Active Pure Nim score 67/100 · last commit 2026-09-06 · 13 stars · tests present · no docs generated

Summary

Latest Version Unknown
License MIT
CI Status Failing
Stars 13
Forks 0
Open Issues 5
Last Commit 2026-09-06
Downloads 0
Last Indexed 2026-09-07 06:07

Installation

nimble install marvdown
choosenim install marvdown
git clone https://github.com/openpeeps/marvdown

OS Compatibility

Platform Linux macOS Windows FreeBSD OpenBSD NetBSD Android iOS WASM Embedded
marvdown - - - - - - -

Source

Repository https://github.com/openpeeps/marvdown
Homepage https://openpeeps.github.io/marvdown/
Registry Source github

README


This is Marvdown ⚡️ A stupid simple Markdown parser

nimble install marvdown

API reference | Download

Github Actions Github Actions

[!NOTE]
Marv is still in early development. Some features are not fully implemented yet. Contributions are welcome!

😍 Key Features

  • [x] Extremely Fast & Lightweight! Check benchmarks
  • [x] CommonMark compliant
  • [x] Compiled cross-platform CLI app
  • [x] Nim library for easy integration in your 👑 Nim projects
  • [x] Markdown to HTML
  • [x] Auto-generate heading IDs for anchor links
  • [x] Table of contents data via getSelectorItems
  • [x] Markdown to JSON (AST)
  • [x] GitHub Flavored Markdown (partial): strikethrough, tables, task lists, autolinks, alerts
  • [x] GitHub-style alerts (NOTE, TIP, IMPORTANT, WARNING, CAUTION)
  • [x] Footnotes
  • [x] Reference-style links
  • [x] Bare URL & email autolinks
  • [x] YAML front matter
  • [x] Components: @include files, @attr props & $variable interpolation
  • [x] Lazy-loading for iframes, videos & images
  • [x] Custom per-line transform hook (customTransform)
  • [ ] Markdown to PDF

About

Marv is a stupid simple markdown parser written in Nim. It can be used as a library in your Nim projects or as a CLI tool to convert markdown files to HTML. It supports headings, paragraphs, bold, italic, strikethrough, links, images, lists (incl. task lists), blockquotes (incl. GitHub alerts), code blocks, inline code, tables (with optional footer), footnotes, reference links, raw HTML, autolinks, YAML front matter and more.

Installing

Install Marvdown via Nimble

nimble install marvdown

Quick Start

From the command line

marvdown html sample.md --optAnchors
marvdown html sample.md --optAnchors --output out.html
marvdown json sample.md
  • --optAnchors — generate heading anchors (with a 🔗 link icon)
  • --bench — print timing stats
  • --components — enable @include / @attr / $variable components

As a Nim library

import marvdown

# one-liner
echo marvdown.toHtml(readFile("sample.md"))

# with custom options
let opts = MarkdownOptions(
  allowed: @[tagP, tagStrong, tagEm, tagA, tagCode, tagPre],
  enableAnchors: true,
  anchorIcon: "🔗"
)
var md = newMarkdown(readFile("sample.md"), opts)
echo md.toHtml()

Settings (MarkdownOptions)

All knobs live on the MarkdownOptions object passed to newMarkdown:

let opts = MarkdownOptions(
  # Which raw HTML tags are allowed. Empty `@[]` means NO raw HTML.
  allowed: @[tagA, tagDiv, tagSpan, tagImg, tagP, tagPre, tagCode],
  # …or allow tags by category instead:
  allowTagsByType: none(TagType),   # tagNone | tagInline | tagBlock | tagAll

  allowInlineStyle: false,   # allow `style` attributes/tags
  allowHtmlAttributes: false, # allow attributes like `width`, `title`

  enableAnchors: true,       # add id="…" + anchor link to headings
  anchorIcon: "🔗",          # icon used inside the anchor link

  showFootnotes: true,       # render footnotes at the end of the doc

  htmlTableClasses: none(seq[string]),  # e.g. some(@["table", "table-striped"])

  enableEmailAutolinks: false,  # `<user@example.com>` → mailto link

  enableComponents: false,   # enable @include / @attr / $variable
  componentBaseDir: "",      # base dir for @include paths

  customTransform: nil,      # proc(line: string): string per-line hook

  lazyloadIframes: false,    # <iframe src> → data-src
  lazyloadVideos: false,     # <video>/<audio>/<source> src → data-src
  lazyloadImages: false,     # <img> & ![alt](url) src → data-src

  parseYaml: true            # parse YAML front matter (--- blocks); set false to skip for speed
)

Features

Headings & anchors

Build a table of contents from the generated anchors:

for item in md.getSelectorItems():   # seq[(level, anchor, title)]
  echo item.level, ". ", item.title, "  #", item.anchor

Inline formatting

Supports bold, italic, code, ~~strikethrough~~ and combinations.

Links & autolinks

Inline links with optional titles, bare https:// auto-links and mailto: email autolinks via enableEmailAutolinks.

Images

Images with alt text and optional titles.

Lists & task lists

Unordered (-, *, +), ordered (1.) and task lists (- [x], - [ ]) with nesting.

Blockquotes & alerts

Blockquotes and GitHub-style alerts. Supported markers: NOTE, TIP, IMPORTANT, WARNING, CAUTION.

Code blocks

Fenced code blocks with language info and indented code blocks (4 spaces).

Tables (with optional footer)

| Name | Role |
| ---- | ---- |
| Ana  | CEO  |
| Bob  | CTO  |
|------|------|
| Total | 2    |
<table>
  <thead><tr><th>Name</th><th>Role</th></tr></thead>
  <tbody><tr><td>Ana</td><td>CEO</td></tr><tr><td>Bob</td><td>CTO</td></tr></tbody>
  <tfoot><tr><td>Total</td><td>2</td></tr></tfoot>
</table>

Add CSS classes with htmlTableClasses: some(@["table", "table-striped"]).

Footnotes

Footnote references and definitions rendered at the end of the document.

Reference links

Supports explicit [text][ref], collapsed [text][] and shortcut [text] references.

Raw HTML

Raw HTML is gated by the allowed / allowTagsByType options.

Components (@include, @attr, $variable)

Include other files (markdown or HTML) and use props & variables in the HTML:

card.html:

<div @title="Marvdown" @badge="v0.1.4">
  <h2>$title</h2>
  <p>Release <code>$badge</code></p>
</div>

page.md:

@include("card.html")
<div>
  <h2>Marvdown</h2>
  <p>Release <code>v0.1.4</code></p>
</div>
  • @attr="value" is captured into a global scope and stripped from the output
  • $variable is resolved from the scope; unknown variables stay literal
  • \$ escapes to a literal $
  • Enable with enableComponents: true and set componentBaseDir to the folder containing the includes

customTransform

Hook every body line before it is parsed — great for custom syntax:

let opts = MarkdownOptions(
  customTransform: proc(line: string): string =
    if line == "@card": "<div class=\"card\">Custom card</div>"
    else: line
)

Lazy-loading media

MarkdownOptions(lazyloadIframes: true)
MarkdownOptions(lazyloadVideos: true)
MarkdownOptions(lazyloadImages: true)

<iframe src="…">, <video src>, <audio src>, <source src> and <img src> (both raw HTML and ![alt](url)) are rewritten from src to data-src, ready for an IntersectionObserver.

YAML front matter

let header = md.getHeader()          # YAMLObject (OrderedTable)
echo yamlmod.getStr(header["title"]) # "Marvdown"

AST / JSON

echo marvdown.getAst(readFile("sample.md"))
[{"kind":"mdkHeading","level":1,"textAnchor":null,
  "children":{"items":[{"kind":"mdkText","text":"Hello","children":null,"line":1}]},
  "line":1}]

Examples

Check out the examples/ folder for runnable code:

# run the comprehensive feature example
nim c -r examples/example.nim

# or use the CLI on a feature-rich sample document
nim c src/marvdown.nim
./marvdown html examples/sample.md --optAnchors
./marvdown json examples/sample.md

Benchmarks

Marvdown is super fast! Run clue test -d:release (options allowTagsByType: tagAll, parseYaml: false):

Marvdown Benchmark – toHtml (wall time, release)
==================================================

| Document                 | Size       |   Iters |   Total ms |     Avg ms |       Throughput |        Out |     CPU ms |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
| sample.md (CommonMark)   | 27.1 KB    |     500 |     269.20 |      0.538 |       49.22 MB/s |    27.9 KB |     269.04 |
| tests/data/big.md        | 4.84 MB    |       5 |     841.29 |    168.257 |       28.76 MB/s |    6.15 MB |     840.78 |
| synthetic 100 lines      | 4.2 KB     |     200 |      36.75 |      0.184 |       22.54 MB/s |     6.8 KB |      36.75 |
| synthetic 1k lines       | 43.8 KB    |      50 |      92.55 |      1.851 |       23.08 MB/s |    68.9 KB |      92.52 |
| synthetic 10k lines      | 451.2 KB   |       5 |     116.21 |     23.242 |       18.96 MB/s |   703.1 KB |     116.20 |
| tiny 1 line              | 14 B       |    1000 |       1.99 |      0.002 |        6.72 MB/s |       26 B |       1.99 |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
  Iterations: 1760  |  Total wall: 1357.98 ms
Marvdown Benchmark – toHtml + anchors
=======================================

| Document                 | Size       |   Iters |   Total ms |     Avg ms |       Throughput |        Out |     CPU ms |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
| sample.md (CommonMark) + | 27.1 KB    |     250 |     133.27 |      0.533 |       49.71 MB/s |    28.0 KB |     133.27 |
| synthetic 1k lines +anch | 43.8 KB    |      25 |      48.78 |      1.951 |       21.90 MB/s |    75.6 KB |      48.79 |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
  Iterations: 275  |  Total wall: 182.05 ms
Marvdown Benchmark – toHtml vs toJson (sample.md)
===================================================

| Document                 | Size       |   Iters |   Total ms |     Avg ms |       Throughput |        Out |     CPU ms |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
| sample toHtml            | 27.1 KB    |     100 |      52.58 |      0.526 |       50.40 MB/s |    27.9 KB |      52.58 |
| sample toJson            | 27.1 KB    |     100 |      72.27 |      0.723 |       36.66 MB/s |    42.6 KB |      72.27 |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
  Iterations: 200  |  Total wall: 124.85 ms
Scaling check – 100 vs 1k lines
=================================

| Document                 | Size       |   Iters |   Total ms |     Avg ms |       Throughput |        Out |     CPU ms |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
| 100 lines                | 4.2 KB     |     100 |      18.40 |      0.184 |       22.50 MB/s |     6.8 KB |      18.40 |
| 1k lines                 | 43.8 KB    |      20 |      36.95 |      1.848 |       23.13 MB/s |    68.9 KB |      36.95 |
|--------------------------|------------|---------|------------|------------|------------------|------------|------------|
  Iterations: 120  |  Total wall: 55.35 ms

Benchmark via tests/test_benchmark.nim plain-text table; Nim 2.2.0, macOS amd64 (clue). Re-run with clue test -d:release.

❤ Contributions & Support

Credits

Original illustration made by 💙 Olha remixed with Sora.

🎩 License

Marv | MIT License. Made by Humans from OpenPeeps.
Copyright © 2024 OpenPeeps & Contributors — All rights reserved.