High-performance Markdown to HTML converter powered by WebAssembly.
@nick/comrak is a fast and efficient Markdown to HTML converter written in
Rust, compiled to WebAssembly, and wrapped with a high-level TypeScript API. It
renders HTML, CommonMark, and CommonMark XML, and mirrors the configurability of
the upstream comrak crate.
Convert Markdown to HTML with a single function call:
import assert from "node:assert";
import { markdownToHTML } from "@nick/comrak";
const markdown = "# Hello, **world**!";
const html = markdownToHTML(markdown);
assert.strictEqual(html, "<h1>Hello, <strong>world</strong>!</h1>\n");Render any format you need:
import assert from "node:assert";
import {
markdownToCommonMark,
markdownToHTML,
markdownToXML,
} from "@nick/comrak";
const md = "# Hello, **world**!";
assert.strictEqual(
markdownToHTML(md),
"<h1>Hello, <strong>world</strong>!</h1>\n",
);
assert.strictEqual(
markdownToXML(md),
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<!DOCTYPE document SYSTEM "CommonMark.dtd">\n' +
'<document xmlns="http://commonmark.org/xml/1.0">\n' +
' <heading level="1">\n' +
' <text xml:space="preserve">Hello, </text>\n' +
" <strong>\n" +
' <text xml:space="preserve">world</text>\n' +
" </strong>\n" +
' <text xml:space="preserve">!</text>\n' +
" </heading>\n" +
"</document>\n",
);
assert.strictEqual(markdownToCommonMark(md), "# Hello, **world**\\!\n");Parse once and render anywhere:
import assert from "node:assert";
import {
type Options,
parseMarkdown,
renderCommonMark,
renderHTML,
renderXML,
} from "@nick/comrak";
const options = { extension: { tasklist: true } } satisfies Options;
const ast = parseMarkdown("# Hello, **world**!\n\n- [x] Done\n", options);
assert.strictEqual(
renderHTML(ast, options),
"<h1>Hello, <strong>world</strong>!</h1>\n" +
'<ul>\n<li><input type="checkbox" checked="" disabled="" /> Done</li>\n</ul>\n',
);
assert.strictEqual(
renderXML(ast, options),
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<!DOCTYPE document SYSTEM "CommonMark.dtd">\n' +
'<document xmlns="http://commonmark.org/xml/1.0">\n' +
' <heading level="1">\n' +
' <text xml:space="preserve">Hello, </text>\n' +
" <strong>\n" +
' <text xml:space="preserve">world</text>\n' +
" </strong>\n" +
' <text xml:space="preserve">!</text>\n' +
" </heading>\n" +
' <list type="bullet" tasklist="true" tight="true">\n' +
' <taskitem completed="true">\n' +
" <paragraph>\n" +
' <text xml:space="preserve">Done</text>\n' +
" </paragraph>\n" +
" </taskitem>\n" +
" </list>\n" +
"</document>\n",
);
assert.strictEqual(
renderCommonMark(ast, options),
"# Hello, **world**\\!\n\n- [x] Done\n",
);Add plugins for custom rendering:
import assert from "node:assert";
import { markdownToHTML, Options } from "@nick/comrak";
const options = Options.default();
options.plugins.render.codefenceSyntaxHighlighter = {
highlight: (code, lang) => `highlighted:${lang ?? "none"}:${code.trim()}`,
pre: () => '<pre class="custom-pre">',
code: () => '<code class="custom-code">',
};
const html = markdownToHTML("```ts\nlet x = 1;\n```\n", options);
assert.strictEqual(
html,
'<pre class="custom-pre"><code class="custom-code">highlighted:ts:let x = 1;</code></pre>\n',
);import assert from "node:assert";
import { markdownToHTML, Options } from "@nick/comrak";
const options = Options.default();
options.render.sourcepos = true;
options.plugins.render.headingAdapter = {
enter: ({ level, content }, sourcepos) => {
const attrs = [`data-level="${level}"`, `data-text="${content}"`];
if (sourcepos) {
const { start, end } = sourcepos;
attrs.push(
`data-sourcepos="${start.line}:${start.column}-${end.line}:${end.column}"`,
);
}
return `<h${level} ${attrs.join(" ")}>`;
},
exit: ({ level }) => `</h${level}>`,
};
const html = markdownToHTML("# Hello!\n\n## Subheading\n", options);
assert.strictEqual(
html,
'<h1 data-level="1" data-text="Hello!" data-sourcepos="1:1-1:8">Hello!</h1>\n' +
'<h2 data-level="2" data-text="Subheading" data-sourcepos="3:1-3:13">Subheading</h2>',
);Install via your preferred package manager:
deno add jsr:@nick/comrakpnpm add jsr:@nick/comrakyarn add jsr:@nick/comrakbunx jsr add @nick/comraknpx jsr add @nick/comrakImportant
Support for the jsr: protocol is available in PNPM v10.2+ and Yarn v4.2+. If
you're using an older version of these package managers, you can use the dlx
command instead.
pnpm dlx jsr add @nick/comrakyarn dlx jsr add @nick/comrakThis package is also distributed on npm as comrak.
deno add npm:comrakpnpm add comrakyarn add comrakbun add comraknpm i comrakmarkdownToHTML(markdown, options?)Render Markdown to HTML.markdownToXML(markdown, options?)Render Markdown to CommonMark XML.markdownToCommonMark(markdown, options?)Render Markdown back to CommonMark.parseMarkdown(markdown, options?)Parse Markdown into an AST.renderHTML(ast, options?)Render an AST to HTML.renderXML(ast, options?)Render an AST to CommonMark XML.renderCommonMark(ast, options?)Render an AST to CommonMark text.Options.default()Get a fresh, fully-populated options object.HeadingAdapter/SyntaxHighlighterAdapterPlug custom heading rendering or code fence highlighting into Comrak.
The Options interface mirrors the Rust crate's configuration surface, spanning
extensions, parsing, rendering, and plugins.
-
autolink?: booleanEnables the autolink extension (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Hello www.github.com.\n", { extension: { autolink: true }, }); assert.strictEqual( html, '<p>Hello <a href="http://www.github.com">www.github.com</a>.</p>\n', );
-
descriptionLists?: booleanEnables description lists (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Term\n\n: Definition", { extension: { descriptionLists: true }, }); assert.strictEqual( html, "<dl>\n<dt>Term</dt>\n<dd>\n<p>Definition</p>\n</dd>\n</dl>\n", );
-
footnotes?: booleanEnables footnotes support (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Hi[^x].\n\n[^x]: A greeting.\n", { extension: { footnotes: true }, }); assert.strictEqual( html, '<p>Hi<sup class="footnote-ref"><a href="#fn-x" id="fnref-x" data-footnote-ref>1</a></sup>.</p>\n' + '<section class="footnotes" data-footnotes>\n<ol>\n<li id="fn-x">\n<p>A greeting. <a href="#fnref-x" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>\n</li>\n</ol>\n</section>\n', );
-
inlineFootnotes?: booleanEnables inline footnotes (default:false).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.extension.footnotes = true; options.extension.inlineFootnotes = true; const html = markdownToHTML("Hi^[An inline note].\n", options); assert.strictEqual( html, '<p>Hi<sup class="footnote-ref"><a href="#fn-__inline_1" id="fnref-__inline_1" data-footnote-ref>1</a></sup>.</p>\n' + '<section class="footnotes" data-footnotes>\n<ol>\n<li id="fn-__inline_1">\n<p>An inline note <a href="#fnref-__inline_1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>\n</li>\n</ol>\n</section>\n', );
-
frontMatterDelimiter?: string | nullProcesses front matter. Defaults to"---".import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("---\nlayout: post\n---\nText\n", { extension: { frontMatterDelimiter: "---" }, }); assert.strictEqual(html, "<p>Text</p>\n");
-
headerIDs?: string | nullGenerates header IDs with an optional prefix (default:"").import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("# README\n", { extension: { headerIDs: "user-content-" }, }); assert.strictEqual( html, '<h1><a href="#readme" aria-hidden="true" class="anchor" id="user-content-readme"></a>README</h1>\n', );
-
table?: booleanEnables table support (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("| a | b |\n|---|---|\n| c | d |\n", { extension: { table: true }, }); assert.strictEqual( html, "<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>c</td>\n<td>d</td>\n</tr>\n</tbody>\n</table>\n", );
-
tagfilter?: booleanFilters disallowed raw HTML tags (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Hello <xmp>.\n\n<xmp>", { extension: { tagfilter: true }, render: { unsafe: true }, }); assert.strictEqual(html, "<p>Hello <xmp>.</p>\n<xmp>\n");
-
tasklist?: booleanEnables task list items (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("* [x] Done\n* [ ] Not done\n", { extension: { tasklist: true }, }); assert.strictEqual( html, '<ul>\n<li><input type="checkbox" checked="" disabled="" /> Done</li>\n<li><input type="checkbox" disabled="" /> Not done</li>\n</ul>\n', );
-
multilineBlockQuotes?: booleanEnables multiline block quotes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML(">>>\nparagraph\n>>>", { extension: { multilineBlockQuotes: true }, }); assert.strictEqual(html, "<blockquote>\n<p>paragraph</p>\n</blockquote>\n");
-
alerts?: booleanEnables GitHub-style alerts (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("> [!note]\n> Something of note", { extension: { alerts: true }, }); assert.strictEqual( html, '<div class="markdown-alert markdown-alert-note">\n<p class="markdown-alert-title">Note</p>\n<p>Something of note</p>\n</div>\n', );
-
mathDollars?: booleanEnables math using dollar syntax (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("$1 + 2$ and $$x = y$$", { extension: { mathDollars: true }, }); assert.strictEqual( html, '<p><span data-math-style="inline">1 + 2</span> and <span data-math-style="display">x = y</span></p>\n', );
-
mathCode?: booleanEnables math using code syntax (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("$`1 + 2`$", { extension: { mathCode: true }, }); assert.strictEqual( html, '<p><code data-math-style="inline">1 + 2</code></p>\n', );
-
wikilinksTitleBeforePipe?: booleanEnables wikilinks where the title is before the pipe (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("[[link label|url]]", { extension: { wikilinksTitleBeforePipe: true }, }); assert.strictEqual( html, '<p><a href="url" data-wikilink="true">link label</a></p>\n', );
-
wikilinksTitleAfterPipe?: booleanEnables wikilinks where the title is after the pipe (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("[[url|link label]]", { extension: { wikilinksTitleAfterPipe: true }, }); assert.strictEqual( html, '<p><a href="url" data-wikilink="true">link label</a></p>\n', );
-
underline?: booleanEnables underlines using double underscores (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("__underlined text__", { extension: { underline: true }, }); assert.strictEqual(html, "<p><u>underlined text</u></p>\n");
-
strikethrough?: booleanEnables strikethrough formatting (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Hello ~world~ there.\n", { extension: { strikethrough: true }, }); assert.strictEqual(html, "<p>Hello <del>world</del> there.</p>\n");
-
superscript?: booleanEnables superscript formatting (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("e = mc^2^.\n", { extension: { superscript: true }, }); assert.strictEqual(html, "<p>e = mc<sup>2</sup>.</p>\n");
-
subscript?: booleanEnables subscript text using single tildes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("H~2~O", { extension: { subscript: true }, }); assert.strictEqual(html, "<p>H<sub>2</sub>O</p>\n");
-
spoiler?: booleanEnables spoilers using double vertical bars (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Darth Vader is ||Luke's father||", { extension: { spoiler: true }, }); assert.strictEqual( html, '<p>Darth Vader is <span class="spoiler">Luke\'s father</span></p>\n', );
-
greentext?: booleanRequires a space after>for blockquotes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML(">implying implications", { extension: { greentext: true }, }); assert.strictEqual(html, "<p>>implying implications</p>\n");
-
shortcodes?: booleanReplaces:emoji:shortcodes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Happy Friday! :smile:", { extension: { shortcodes: true }, }); assert.strictEqual(html, "<p>Happy Friday! 😄</p>\n");
-
imageURLRewriter?: URLRewriter | nullRewrites image URLs (default:null).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.extension.imageURLRewriter = (url: string) => `https://cdn.example.com/images/${encodeURIComponent(url)}`; const html = markdownToHTML("", options); assert.strictEqual( html, '<p><img src="https://cdn.example.com/images/image.png" alt="alt text" /></p>\n', );
-
linkURLRewriter?: URLRewriter | nullRewrites link URLs (default:null).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.extension.linkURLRewriter = (url: string) => `https://safe.example.com/norefer?url=${encodeURIComponent(url)}`; const html = markdownToHTML( "[my link](http://unsafe.example.com/bad)", options, ); assert.strictEqual( html, '<p><a href="https://safe.example.com/norefer?url=http%3A%2F%2Funsafe.example.com%2Fbad">my link</a></p>\n', );
-
cjkFriendlyEmphasis?: booleanRecognizes emphasis in CJK contexts (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("**この文は重要です。**但这句话并不重要。", { extension: { cjkFriendlyEmphasis: true }, }); assert.strictEqual( html, "<p><strong>この文は重要です。</strong>但这句话并不重要。</p>\n", );
-
subtext?: booleanEnables block-scoped subtext (default:false).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.extension.subtext = true; const html = markdownToHTML("-# subtext", options); assert.strictEqual(html, "<p><sub>subtext</sub></p>\n");
-
highlight?: booleanEnables highlighting using==(default:false).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.extension.highlight = true; const html = markdownToHTML("Hey, ==this is important==!", options); assert.strictEqual(html, "<p>Hey, <mark>this is important</mark>!</p>\n");
-
defaultInfoString?: string | nullDefault info string for fenced code blocks (default:null).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; assert.strictEqual( markdownToHTML("```\nfn hello();\n```\n"), "<pre><code>fn hello();\n</code></pre>\n", ); assert.strictEqual( markdownToHTML("```\nfn hello();\n```\n", { parse: { defaultInfoString: "rust" }, }), '<pre><code class="language-rust">fn hello();\n</code></pre>\n', );
-
smart?: booleanEnable smart punctuation conversion (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; assert.strictEqual( markdownToHTML("'Hello,' \"world\" ..."), "<p>'Hello,' "world" ...</p>\n", ); assert.strictEqual( markdownToHTML("'Hello,' \"world\" ...", { parse: { smart: true } }), "<p>‘Hello,’ “world” …</p>\n", );
-
relaxedTasklistMatching?: booleanAllow symbols beyondx/Xfor tasklists (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const markdown = "* [x] Done\n* [ ] Not done\n* [-] Also done\n* [ ] Also not done\n"; const relaxed = markdownToHTML(markdown, { extension: { tasklist: true }, parse: { relaxedTasklistMatching: true }, }); assert.strictEqual( relaxed, '<ul>\n<li><input type="checkbox" checked="" disabled="" /> Done</li>\n<li><input type="checkbox" disabled="" /> Not done</li>\n<li><input type="checkbox" checked="" disabled="" /> Also done</li>\n<li><input type="checkbox" disabled="" /> Also not done</li>\n</ul>\n', ); const strict = markdownToHTML(markdown, { extension: { tasklist: true }, parse: { relaxedTasklistMatching: false }, }); assert.strictEqual( strict, '<ul>\n<li><input type="checkbox" checked="" disabled="" /> Done</li>\n<li><input type="checkbox" disabled="" /> Not done</li>\n<li>[-] Also done</li>\n<li><input type="checkbox" disabled="" /> Also not done</li>\n</ul>\n', );
-
tasklistInTable?: booleanParse tasklist items inside tables (default:false).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.extension.table = true; options.extension.tasklist = true; const markdown = "| val |\n| - |\n| [ ] |\n"; assert.strictEqual( markdownToHTML(markdown, options), "<table>\n<thead>\n<tr>\n<th>val</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>[ ]</td>\n</tr>\n</tbody>\n</table>\n", ); options.parse.tasklistInTable = true; assert.strictEqual( markdownToHTML(markdown, options), '<table>\n<thead>\n<tr>\n<th>val</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>\n<input type="checkbox" disabled="" /> </td>\n</tr>\n</tbody>\n</table>\n', );
-
relaxedAutolinks?: booleanDetect links inside brackets and allow all URL schemes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("[https://foo.com]", { extension: { autolink: true }, parse: { relaxedAutolinks: true }, }); assert.strictEqual( html, '<p>[<a href="https://foo.com">https://foo.com</a>]</p>\n', );
-
ignoreSetext?: booleanIgnore setext headings in input (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("setext heading\n---", { parse: { ignoreSetext: true }, }); assert.strictEqual(html, "<p>setext heading</p>\n<hr />\n");
-
brokenLinkCallback?: BrokenLinkCallback | nullHandle undefined link references (default:null).import assert from "node:assert"; import { markdownToHTML, Options } from "@nick/comrak"; const options = Options.default(); options.parse.brokenLinkCallback = (ref) => ({ url: "https://img.shields.io/badge/placeholder-lightgrey.svg", title: `Placeholder Badge (original: ${ref.original})`, }); const html = markdownToHTML("![Build Status][undefined-badge]\n", options); assert.strictEqual( html, '<p><img src="https://img.shields.io/badge/placeholder-lightgrey.svg" alt="Build Status" title="Placeholder Badge (original: undefined-badge)" /></p>\n', );
-
leaveFootnoteDefinitions?: booleanKeep footnote definitions in place within the AST (default:false).import assert from "node:assert"; import { Options, parseMarkdown, renderCommonMark } from "@nick/comrak"; const options = Options.default(); options.extension.footnotes = true; options.parse.leaveFootnoteDefinitions = true; const ast = parseMarkdown("Hi[^x].\n\n[^x]: A greeting.\n", options); const cm = renderCommonMark(ast, options); assert.strictEqual(cm, "Hi[^x].\n\n[^x]:\n A greeting.\n");
-
escapedCharSpans?: booleanKeep escaped characters as spans in the AST (default:false). Enablingrender.escapedCharSpansalso enables this.import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Notify user \\@example", { render: { escapedCharSpans: true }, }); assert.strictEqual( html, "<p>Notify user <span data-escaped-char>@</span>example</p>\n", );
-
escape?: booleanEscape raw HTML instead of clobbering it (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; assert.strictEqual( markdownToHTML("<i>italic text</i>"), "<p><!-- raw HTML omitted -->italic text<!-- raw HTML omitted --></p>\n", ); assert.strictEqual( markdownToHTML("<i>italic text</i>", { render: { escape: true } }), "<p><i>italic text</i></p>\n", );
-
githubPreLang?: booleanUse GitHub-style<pre lang="xyz">for fenced code blocks (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; assert.strictEqual( markdownToHTML("```rust\nfn hello();\n```\n", { render: { githubPreLang: true }, }), '<pre lang="rust"><code>fn hello();\n</code></pre>\n', );
-
hardbreaks?: booleanConvert soft line breaks to hard breaks (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; assert.strictEqual( markdownToHTML("Hello.\nWorld.\n"), "<p>Hello.\nWorld.</p>\n", ); assert.strictEqual( markdownToHTML("Hello.\nWorld.\n", { render: { hardbreaks: true } }), "<p>Hello.<br />\nWorld.</p>\n", );
-
unsafe?: booleanAllow rendering of raw HTML and potentially dangerous links (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const markdown = "<script>\nalert('xyz');\n</script>\n\n" + "Possibly <marquee>annoying</marquee>.\n\n" + "[Dangerous](javascript:alert(document.cookie)).\n\n" + "[Safe](http://commonmark.org)."; assert.strictEqual( markdownToHTML(markdown), "<!-- raw HTML omitted -->\n" + "<p>Possibly <!-- raw HTML omitted -->annoying<!-- raw HTML omitted -->.</p>\n" + '<p><a href="">Dangerous</a>.</p>\n' + '<p><a href="http://commonmark.org">Safe</a>.</p>\n', ); assert.strictEqual( markdownToHTML(markdown, { render: { unsafe: true } }), "<script>\nalert('xyz');\n</script>\n" + "<p>Possibly <marquee>annoying</marquee>.</p>\n" + '<p><a href="javascript:alert(document.cookie)">Dangerous</a>.</p>\n' + '<p><a href="http://commonmark.org">Safe</a>.</p>\n', );
-
width?: numberWrap column for CommonMark output (default:0).import assert from "node:assert"; import { markdownToCommonMark } from "@nick/comrak"; const markdown = "Hello, **world**!\n\nNew line of text that should wrap when width is set.\n"; assert.strictEqual( markdownToCommonMark(markdown), "Hello, **world**\\!\n\nNew line of text that should wrap when width is set.\n", ); assert.strictEqual( markdownToCommonMark(markdown, { render: { width: 20 } }), "Hello, **world**\\!\n\nNew line of text\nthat should wrap\nwhen width is set.\n", );
-
fullInfoString?: booleanUse the full info string for fenced code blocks (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("```rust extra info\nfn hello();\n```\n", { render: { fullInfoString: true }, }); assert.strictEqual( html, '<pre><code data-meta="extra info" class="language-rust">fn hello();\n</code></pre>\n', );
-
listStyle?: "dash" | "plus" | "star"List marker style for CommonMark output (default:"dash").import assert from "node:assert"; import { markdownToCommonMark } from "@nick/comrak"; assert.strictEqual( markdownToCommonMark("* Item\n* Item\n", { render: { listStyle: "star" } }), "* Item\n* Item\n", ); assert.strictEqual( markdownToCommonMark("* Item\n* Item\n", { render: { listStyle: "dash" } }), "- Item\n- Item\n", );
-
sourcepos?: booleanInclude source position attributes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Hello *world*!", { render: { sourcepos: true }, }); assert.strictEqual( html, '<p data-sourcepos="1:1-1:14">Hello <em data-sourcepos="1:7-1:13">world</em>!</p>\n', );
-
escapedCharSpans?: booleanWrap escaped characters in<span>tags (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("Notify user \\@example", { render: { escapedCharSpans: true }, }); assert.strictEqual( html, "<p>Notify user <span data-escaped-char>@</span>example</p>\n", );
-
ignoreEmptyLinks?: booleanIgnore empty links in input (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("[]()", { render: { ignoreEmptyLinks: true }, }); assert.strictEqual(html, "<p>[]()</p>\n");
-
gfmQuirks?: booleanEnable GFM quirks in HTML output (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("****abcd****", { render: { gfmQuirks: true }, }); assert.strictEqual(html, "<p><strong>abcd</strong></p>\n");
-
preferFenced?: booleanPrefer fenced code blocks in CommonMark output (default:false).import assert from "node:assert"; import { markdownToCommonMark } from "@nick/comrak"; assert.strictEqual( markdownToCommonMark(" indented code\n"), " indented code\n", ); assert.strictEqual( markdownToCommonMark(" indented code\n", { render: { preferFenced: true }, }), "```\nindented code\n```\n", );
-
figureWithCaption?: booleanRender images as<figure>elements with captions (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML( '', { render: { figureWithCaption: true } }, ); assert.strictEqual( html, '<p><figure><img src="https://example.com/image.png" alt="image" title="this is an image" /><figcaption>this is an image</figcaption></figure></p>\n', );
-
tasklistClasses?: booleanAdd task list CSS classes (default:false).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const html = markdownToHTML("- [ ] Foo", { extension: { tasklist: true }, render: { tasklistClasses: true }, }); assert.strictEqual( html, '<ul class="contains-task-list">\n<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="" /> Foo</li>\n</ul>\n', );
-
olWidth?: numberMinimum marker width when rendering ordered lists (default:0).import assert from "node:assert"; import { markdownToHTML } from "@nick/comrak"; const markdown = "1. one\n10. ten\n"; assert.strictEqual( markdownToHTML(markdown, { render: { olWidth: 0 } }), "<ol>\n<li>one</li>\n<li>ten</li>\n</ol>\n", ); assert.strictEqual( markdownToHTML(markdown, { render: { olWidth: 3 } }), "<ol>\n<li>one</li>\n<li>ten</li>\n</ol>\n", );
-
experimentalMinimizeCommonmark?: booleanMinimize escapes in CommonMark output (default:false).import assert from "node:assert"; import { markdownToCommonMark } from "@nick/comrak"; assert.strictEqual( markdownToCommonMark("Hello, **world**!\n"), "Hello, **world**\\!\n", ); assert.strictEqual( markdownToCommonMark("Hello, **world**!\n", { render: { experimentalMinimizeCommonmark: true }, }), "Hello, **world**!\n", );
render.codefenceSyntaxHighlighterPlug in custom syntax highlighting. See the plugin example above for a minimal adapter that injects custom<pre>/<code>tags and HTML.render.headingAdapterCustomize heading rendering. The heading adapter example in the usage section demonstrates adding data attributes and propagating source positions.
This package is designed to work seamlessly across multiple JavaScript runtimes.
While the original comrak module required the Deno runtime, this fork boasts
wide compatibility that's been verified to run on Deno, Node.js, Bun, Cloudflare
Workers, and even web browsers. Theoretically, it should be fully compatible by
any JavaScript runtime that supports WebAssembly.
@nick/comrak is built using a combination of Rust, WebAssembly, and
TypeScript. Internally, this project wraps the comrak crate by
Talya Connor with the necessary Rust to generate a WebAssembly module.
The WebAssembly is built using @deno/wasmbuild, and inlined into a
JavaScript file for portability and the best compatibility. The API that is
exposed as a thin TypeScript wrapper around the generated JavaScript bindings,
consisting mostly of type definitions.
The brotli compression algorithm is used to compress the WebAssembly binary
during the build step, making it ~75% smaller and a lot quicker to load.
Decompression is performed immediately on import with debrotli.
When all is said and done, this results in a very snappy experience for users, with minimal overhead during initial load and fast Markdown rendering times. It also reduces the amount of data transferred over the network, making it ideal for use in web applications and serverless environments.
The original comrak module was published on the Deno registry, which cannot be
used as a dependency in other JavaScript runtimes. JSR has forbidden the use of
HTTPS-based dependencies in all of its packages (yup, including deno.land),
thereby rendering the original comrak module incompatible with JSR. And thus,
@nick/comrak was born, on an overcast Saturday evening in late March of 2025.