CommonMark
Unreviewed · 2026-04-19 Node + Browser TextCommonMark + GFM renderer powered by pulldown-cmark.
- Targets
- Node + Browser
Install
pnpm add @amigo-labs/commonmarkBenchmarks
Trend (13 pts)Benchmark
small (~0.1 KB)
- @amigo-labs/commonmark renderBytesFast napi 392.47K hz · 8.88×
- @amigo-labs/commonmark renderFast napi 390.18K hz · 8.83×
- @amigo-labs/commonmark renderBytes napi 317.06K hz · 7.18×
- @amigo-labs/commonmark render napi 303.34K hz · 6.87×
- @amigo-labs/commonmark render (fast opts) napi 264.26K hz · 5.98×
- markdown-it 59.08K hz · 1.34×
- marked 44.18K hz
Benchmark
medium (~2.8 KB)
- @amigo-labs/commonmark render (fast opts) napi 38.78K hz · 11.08×
- @amigo-labs/commonmark renderBytes napi 32.21K hz · 9.20×
- @amigo-labs/commonmark render napi 30.19K hz · 8.63×
- markdown-it 5.16K hz · 1.47×
- marked 3.50K hz
Benchmark
large (~81 KB)
- @amigo-labs/commonmark render (fast opts) napi 846 hz · 10.77×
- @amigo-labs/commonmark renderBytes napi 750 hz · 9.55×
- @amigo-labs/commonmark render napi 376 hz · 4.79×
- markdown-it 134 hz · 1.71×
- marked 78.5 hz
Benchmark
batch — renderMany (500 × medium docs)
- @amigo-labs/commonmark renderMany (parallel) napi 125 hz · 18.92×
- @amigo-labs/commonmark per-call loop napi 60.8 hz · 9.21×
- markdown-it per-call loop 9.28 hz · 1.41×
- marked per-call loop 6.60 hz
README
@amigo-labs/commonmark
Blazing-fast native CommonMark + GFM renderer. Powered by Rust and pulldown-cmark, delivered as prebuilt binaries via NAPI-RS.
Not a drop-in for
markedormarkdown-it. This package targets the CommonMark 0.30 spec plus standard GFM extensions. Output is not byte-identical to eithermarkedor GitHub’s renderer — see__conformance__/divergences.md.
Install
npm install @amigo-labs/commonmark
The correct prebuilt binary for your platform is selected automatically. Supported: Linux x64 (glibc+musl), Linux arm64, macOS x64, macOS arm64, Windows x64.
Usage
import { render, renderMany, Renderer } from '@amigo-labs/commonmark'
// One-shot
render('# Hello, **world**')
// → '<h1 id="hello-world">Hello, <strong>world</strong></h1>\n'
// Batch (site builder shape)
renderMany(['# Page 1', '# Page 2', '# Page 3'])
// → ['<h1 id="page-1">Page 1</h1>\n', …]
// Reusable renderer (same options across many calls)
const r = new Renderer({ gfm: true, unsafeHtml: false })
r.render('## Section')
// Buffer-input overload — skips V8 UTF-16 → UTF-8 copy on the FFI boundary.
// Measurably faster on small inputs; parity on medium/large where
// rendering dominates.
import { renderBytes } from '@amigo-labs/commonmark'
renderBytes(Buffer.from('# fast'))
Options
All options are optional.
| Option | Type | Default | Description |
|---|---|---|---|
gfm | boolean | true | Enable GFM extensions: tables, strikethrough, task lists, autolinks. |
footnotes | boolean | false | Enable footnote syntax ([^1]). |
smartPunctuation | boolean | false | Convert -- → en-dash, ... → ellipsis, straight quotes → curly. |
unsafeHtml | boolean | false | Allow raw HTML blocks in Markdown. Disabled by default — raw HTML is dropped silently. |
headingIds | boolean | true | Auto-generate ASCII slug IDs for headings (# Hello World → <h1 id="hello-world">). |
Safety
The default (unsafeHtml: false) drops raw HTML blocks and inline HTML. This is the right choice for untrusted input (user-submitted Markdown). It is not a full XSS sanitizer — link scheme filtering (javascript:, data:, etc.) is not applied. For fully-sanitized output, chain with @amigo-labs/sanitize-html:
import { render } from '@amigo-labs/commonmark'
import { sanitize } from '@amigo-labs/sanitize-html'
const unsafe_but_html_free = render(userMarkdown)
const safe = sanitize(unsafe_but_html_free)
Install for the browser
The same import works in Angular, React, Vite, esbuild, and webpack ≥ 5 — the bundler picks the WASM build via the browser conditional export:
import { render } from '@amigo-labs/commonmark'
pulldown-cmark is ~150–200 KB gzipped — under the 500 KB browser budget. The napi-only renderBytes / renderBytesFast variants are dropped from the browser build (no Buffer); renderMany runs serially in WASM (no rayon).
The XSS warning above applies doubly to browser usage — pair with @amigo-labs/sanitize-html when rendering untrusted Markdown into the DOM.
When to choose this package
- You control the Markdown source (docs, README files, CMS authored by trusted editors) and want faster site builds.
- Your app renders Markdown in a hot path (AI chat responses, realtime editor preview) and FFI overhead is amortized.
- You can accept CommonMark+GFM-spec output and don’t depend on
marked-specific rendering quirks.
When not to choose this package
- You rely on
marked’s tight-vs-loose list heuristics, plugin API, or raw-HTML passthrough. - You need
markdown-it’s plugin ecosystem (anchors, containers, footnote styles, custom tokenizers). - You need output byte-identical to GitHub’s renderer (usernames, issue refs, SHA autolinks).
Performance
Live benchmark numbers vs marked and markdown-it are on the dashboard and in docs/data.json. Notes on the options that move them:
renderBytes(Buffer)avoids the V8 UTF-16 → UTF-8 copy on input — faster thanrender(string)on small inputs; roughly parity once rendering dominates.{ headingIds: false, unsafeHtml: true }enables the streaming fast-path: no event collection, no filter pass.renderManyparallelises across cores viarayonfor batches ≥ 8 docs where at least one doc is ≥ 512 bytes.
Conformance
- Spec compliance: CommonMark 0.30 + GFM as per
pulldown-cmark’s test suite. - Safety invariants verified via property-based fuzzing (
fast-check). - Divergences from other renderers documented in
__conformance__/divergences.md.
pnpm test # unit tests
pnpm test:conformance # parity + upstream + fuzz
pnpm test:all # everything
pnpm bench # vs marked + markdown-it
License
MIT
Perf review
Candidate review: commonmark
Status: GO (as a new package, not a drop-in for
marked) · Predicted: 🟢 Green · Reviewed: 2026-04-19
Verdict
pulldown-cmark is a textbook Green shape: bytes-in / bytes-out, substantial compute work per byte, no object traversal, no callback-boundary problem. As a new package with honest positioning (CommonMark + GFM spec-strict, not marked-compatible) it sidesteps the parity trap that blocks marked itself.
JS package
- npm: no direct candidate as a drop-in target — this package is a new product. Comparison alternatives in JS:
marked(~30M/week),markdown-it(~25M/week),commonmark.js(~2M/week) - Downloads: n/a (newcomer)
- Exports / API surface: kept small —
render(md: string, opts?): string, possiblyparse(md) → token-arrayfor streaming/walk use-cases - Typical input: Markdown document 1 KB – 1 MB
- Typical output: HTML string
- Realistic median use-case: site builders (Astro/Docusaurus-style tools) rendering 500–5000 docs per build; CLI README viewers; AI chat UIs that render Markdown responses server-side
Rust replacement
- Candidate crate(s):
pulldown-cmark(primary — minimal, fast, CommonMark-compliant, GFM extensions via feature flags),comrak(more feature-rich, more GFM parity with GitHub, larger bundle) - Maintenance / license:
pulldown-cmarkactive (raphlinus + contributors), MIT;comrakactive, BSD-2 - Known gotchas / divergences: CommonMark 0.30 spec as the baseline — if we communicate that cleanly, “divergence” is not a bug but a feature
BACKLOG check
No existing BACKLOG entry. marked is listed there as a drop-in NO-GO — this package is explicitly not a marked replacement but a stand-alone offering.
FFI-overhead prediction
| Factor | Assessment |
|---|---|
| Per-call algorithmic work | Substantial: 100 KB Markdown ~500 µs – 1 ms in pulldown-cmark, JS baseline marked ~5 ms → 5–10× headroom |
| Input size distribution | 1 KB – 1 MB, Buffer input possible → FFI input cost negligible |
| Output size distribution | HTML string ~1.5× input size; 0.35 ns/byte FFI output cost = ~50 µs at 150 KB output — tolerable |
| Reusable setup (stateful potential) | Low — options are small, no expensive setup |
| Batch-usage realism | High: site builders render hundreds of docs per build; a renderMany(docs: string[]) API makes sense |
| FFI-share estimate vs. Rust work | <15% on documents ≥10 KB; ~40% at 1 KB but speedup is still ≥2× |
Classification reasoning
The shape matches sanitize-html and inflate in the repo exactly: bytes-in, substantial compute, bytes-out. Not a deep-equal shape (no object traversal), not a handlebars shape (no callbacks), not a mime shape (no FFI trap). pulldown-cmark is also a pull parser, streams internally — memory footprint is good.
The single condition for Green instead of Yellow: the smallest realistic input has to perform cleanly. At 1 KB Markdown JS marked is ~50 µs, pulldown-cmark over FFI comes in at an estimated ~15–20 µs — above 2×. At 100 KB it becomes 8–10×. The 2×-at-smallest-input gate holds.
GFM parity with pulldown-cmark is good: tables, strikethrough, task lists, footnotes, autolinks via feature flags. It’s not marked-compatible, but it is spec-compatible — and a spec-compatible CommonMark/GFM is an honest, defensible position.
If GO — proposed port
-
Recommended crate name:
@amigo-labs/commonmark -
Primary API sketch:
export interface CommonMarkOptions { gfm?: boolean; // default true (tables, strike, task-lists, autolinks) footnotes?: boolean; // default false smartPunctuation?: boolean; // default false unsafeHtml?: boolean; // default false — filter raw HTML headingIds?: boolean; // default true — slugify headings } export function render(markdown: string | Buffer, opts?: CommonMarkOptions): string; // Batch API for site builders export function renderMany(docs: Array<string | Buffer>, opts?: CommonMarkOptions): string[]; // Optional: stateful renderer class for repeated calls with the same opts set export class Renderer { constructor(opts?: CommonMarkOptions); render(markdown: string | Buffer): string; } -
Must-have benchmark scenarios:
- small: 1 KB Markdown (typical blog paragraph) vs.
marked,markdown-it - medium: 50 KB (long blog post / README) vs. same
- large: 500 KB (Docusaurus API reference) vs. same
- batch:
renderMany(500 × 10KB docs)— site-build shape - realistic median: AI chat response shape, 2–5 KB with code blocks + inline formatting
- small: 1 KB Markdown (typical blog paragraph) vs.
-
Acceptance thresholds (Green gate):
- ≥2× vs.
markedat 1 KB - ≥5× at 50 KB
- ≥8× at 500 KB
renderManyper-item overhead ≤15% vs. single call (otherwise no batch gain)
- ≥2× vs.
-
Risks:
- Feature-request drift: users want
markedplugins ormarkdown-itplugins ported — clearly document “spec-only, no plugin API in v1” - Heading IDs / slug behavior:
github-sluggeris the de-facto standard in JS; we either have to reuseslug/slugify(we ship@amigo-labs/slugify) or introduce a newheadingSlugger - HTML sanitizing interaction:
unsafeHtml: falsemust be clearly documented; users who need raw HTML will turn it on and then have an XSS incident → README warning, link to@amigo-labs/sanitize-htmlas the recommended chain - GFM edge cases vs. GitHub:
pulldown-cmark’s GFM ≈ GitHub’s GFM, but not byte-identical. Irrelevant for most users, problematic for GitHub-rendering clones — document in the README
- Feature-request drift: users want
If NO-GO — BACKLOG entry
n/a — recommendation is GO.