
This is Marvdown ⚡️ A stupid simple Markdown parser
nimble install marvdown
Note
Marv is still in early development. Some features are not fully implemented yet. Contributions are welcome!
- Extremely Fast & Lightweight! Check benchmarks
- CommonMark compliant
- Compiled cross-platform CLI app
- Nim library for easy integration in your 👑 Nim projects
- Markdown to HTML
- Auto-generate heading IDs for anchor links
- Table of contents data via
getSelectorItems
- Markdown to JSON (AST)
- GitHub Flavored Markdown (partial): strikethrough, tables, task lists, autolinks, alerts
- GitHub-style alerts (
NOTE,TIP,IMPORTANT,WARNING,CAUTION) - Footnotes
- Reference-style links
- Bare URL & email autolinks
- YAML front matter
- Components:
@includefiles,@attrprops &$variableinterpolation - Lazy-loading for iframes, videos & images
- Custom per-line transform hook (
customTransform) - Markdown to PDF
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.
Install Marvdown via Nimble
nimble install marvdown
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/$variablecomponents
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()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> &  src → data-src
parseYaml: true # parse YAML front matter (--- blocks); set false to skip for speed
)Build a table of contents from the generated anchors:
for item in md.getSelectorItems(): # seq[(level, anchor, title)]
echo item.level, ". ", item.title, " #", item.anchorSupports bold, italic, code, strikethrough and combinations.
Inline links with optional titles, bare https:// auto-links and mailto: email autolinks via enableEmailAutolinks.
Images with alt text and optional titles.
Unordered (-, *, +), ordered (1.) and task lists (- [x], - [ ]) with nesting.
Blockquotes and GitHub-style alerts. Supported markers: NOTE, TIP, IMPORTANT, WARNING, CAUTION.
Fenced code blocks with language info and indented code blocks (4 spaces).
| 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"]).
Footnote references and definitions rendered at the end of the document.
Supports explicit [text][ref], collapsed [text][] and shortcut [text] references.
Raw HTML is gated by the allowed / allowTagsByType options.
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$variableis resolved from the scope; unknown variables stay literal\$escapes to a literal$- Enable with
enableComponents: trueand setcomponentBaseDirto the folder containing the includes
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
)MarkdownOptions(lazyloadIframes: true)
MarkdownOptions(lazyloadVideos: true)
MarkdownOptions(lazyloadImages: true)<iframe src="…">, <video src>, <audio src>, <source src> and <img src> (both raw HTML and ) are rewritten from src to data-src, ready for an IntersectionObserver.
let header = md.getHeader() # YAMLObject (OrderedTable)
echo yamlmod.getStr(header["title"]) # "Marvdown"echo marvdown.getAst(readFile("sample.md"))[{"kind":"mdkHeading","level":1,"textAnchor":null,
"children":{"items":[{"kind":"mdkText","text":"Hello","children":null,"line":1}]},
"line":1}]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.mdMarvdown 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.
- 🐛 Found a bug? Create a new Issue
- 👋 Wanna help? Fork it!
Original illustration made by 💙 Olha remixed with Sora.
Marv | MIT License.
Made by Humans from OpenPeeps.
Copyright © 2024 OpenPeeps & Contributors — All rights reserved.