Markdown-it, Marked, Remark, Remarkable, and Showdown are JavaScript libraries that parse Markdown and typically render HTML, each with different parsing models, extension hooks, and ergonomics. Markdown-it and Remarkable expose a tokenizer/renderer pipeline with rule-level control; Marked exposes tokenizers and a pluggable renderer; Remark is AST-first (mdast) and compiles to HTML through rehype; Showdown prioritizes configurability via options and extensions without a formal AST. Turndown is the outlier: it converts HTML to Markdown using a rule-based DOM traversal—useful for round-tripping content from WYSIWYG editors or existing HTML sources into Markdown. Together, these tools cover the spectrum from quick Markdown→HTML rendering to fully programmable content pipelines and reverse conversion.
Most frontend and Node.js applications that surface user-authored content eventually confront the Markdown/HTML boundary: parse Markdown, render safe HTML, optionally enrich output (syntax highlighting, custom attributes), and sometimes move in the opposite direction when importing or round-tripping content from WYSIWYG editors. Markdown-it, Marked, Remark, Remarkable, and Showdown all render HTML from Markdown with different parsing models and extension hooks, while Turndown converts HTML back to Markdown using DOM rules.
Goal: Convert a Markdown snippet to HTML with syntax highlighting and external links opening in a new tab with rel attributes. For Turndown (HTML→Markdown), perform the equivalent customization when converting the produced HTML back to Markdown by preserving fenced code language and normalizing links.
# Demo
Visit https://example.com and [MDN](https://developer.mozilla.org/).
```js
console.log('hi')
| Name | Score |
|---|---|
| Foo | 42 |
### markdown-it
```js
import MarkdownIt from 'markdown-it';
import prism from 'prismjs';
const md = new MarkdownIt({
html: false,
linkify: true,
highlight: (code, lang) => {
try {
const grammar = prism.languages[lang] || prism.languages.markup;
const highlighted = prism.highlight(code, grammar, lang);
return `<pre class="language-${lang}"><code class="language-${lang}">${highlighted}</code></pre>`;
} catch {
return `<pre><code>${md.utils.escapeHtml(code)}</code></pre>`;
}
}
});
const defaultOpen = md.renderer.rules.link_open || ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('target', '_blank');
tokens[idx].attrSet('rel', 'noopener noreferrer');
return defaultOpen(tokens, idx, options, env, self);
};
const html = md.render(markdown);
Notes: linkify makes bare URLs clickable; raw HTML is disabled. Add a sanitizer for untrusted input if you enable raw HTML.
import { marked } from 'marked';
import prism from 'prismjs';
const renderer = new marked.Renderer();
renderer.link = (href, title, text) => {
const t = title ? ` title="${title}"` : '';
return `<a href="${href}"${t} target="_blank" rel="noopener noreferrer">${text}</a>`;
};
marked.setOptions({
gfm: true,
highlight: (code, lang) => {
const grammar = prism.languages[lang] || prism.languages.markup;
return prism.highlight(code, grammar, lang);
}
});
const html = marked.parse(markdown, { renderer });
Notes: bare URLs may not auto-link without an extension/preprocess. Pair with a sanitizer when rendering untrusted Markdown.
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeExternalLinks from 'rehype-external-links';
import rehypeHighlight from 'rehype-highlight';
import rehypeStringify from 'rehype-stringify';
const file = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: false })
.use(rehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] })
.use(rehypeHighlight)
.use(rehypeStringify)
.process(markdown);
const html = String(file);
Notes: rehype-external-links centralizes link policy; highlighting is pluggable; add rehype-sanitize if allowing raw HTML.
import Remarkable from 'remarkable';
import prism from 'prismjs';
const md = new Remarkable('full', {
html: false,
linkify: true,
highlight: (code, lang) => {
try {
const grammar = prism.languages[lang] || prism.languages.markup;
const highlighted = prism.highlight(code, grammar, lang);
return `<pre class="language-${lang}"><code class="language-${lang}">${highlighted}</code></pre>`;
} catch {
return '';
}
}
});
const defaultOpen = md.renderer.rules.link_open || ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('target', '_blank');
tokens[idx].attrSet('rel', 'noopener noreferrer');
return defaultOpen(tokens, idx, options, env, self);
};
const html = md.render(markdown);
Notes: control is similar to markdown-it with renderer rules and a highlight hook.
import showdown from 'showdown';
const converter = new showdown.Converter({
tables: true,
strikethrough: true,
openLinksInNewWindow: true,
ghCodeBlocks: true
});
// Add rel attributes to links post-render
converter.addExtension(function() {
return [{
type: 'output',
filter: (html) => html.replace(/<a\s+href=/g, '<a rel="noopener noreferrer" href=')
}];
}, 'relNoopener');
const html = converter.makeHtml(markdown);
Notes: extensions operate on strings; autolinking bare URLs depends on options and may be less strict; add a sanitizer externally for untrusted content.
import TurndownService from 'turndown';
const turndown = new TurndownService({ codeBlockStyle: 'fenced' });
// Preserve language info from <pre><code class="language-...">
turndown.addRule('fencedCodeWithLang', {
filter: (node) => node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE',
replacement: (content, node) => {
const codeEl = node.firstChild;
const cls = codeEl.getAttribute('class') || '';
const match = cls.match(/language-([\w-]+)/);
const lang = match ? match[1] : '';
const code = codeEl.textContent || '';
return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
}
});
// Normalize links (ignore target/rel attributes in Markdown)
turndown.addRule('normalizeLinks', {
filter: 'a',
replacement: (content, node) => {
const href = node.getAttribute('href') || '';
const title = node.getAttribute('title');
const t = title ? ` "${title}"` : '';
return href ? `[${content}](${href}${t})` : content;
}
});
const markdownOut = turndown.turndown(html /* HTML produced by any renderer above */);
Notes: turndown operates on DOM nodes, making it reliable for controlled HTML. Complex interactive HTML may need additional rules to avoid lossy mappings.
Each library reflects a different assumption about how Markdown should flow through your system. Token/renderer pipelines (markdown-it, remarkable) prioritize predictable rendering with precise, small surface-area hooks. marked optimizes for a pragmatic, minimal-ceremony path to GFM HTML with extension points that cover most customization needs. remark treats content as data—parse to a typed tree, transform deliberately, and compile with explicit sanitization and HTML policies. showdown favors quick wins with options and string-level extensions. turndown completes the picture by enabling HTML→Markdown ingestion, codifying reversal rules when the source of truth lives in HTML rather than Markdown.
Choose markdown-it when you need a CommonMark-oriented parser with fine-grained control over tokenization and rendering, and you value a large ecosystem of plugins. Its renderer rules make it straightforward to inject attributes (e.g., target and rel on links) or alter specific node types, and the highlight callback integrates cleanly with Prism or highlight.js. It’s a strong fit for SSR and browser contexts alike, but remember to add a sanitizer for untrusted input. The plugin model is well-suited to extending syntax (containers, attributes) while keeping core behavior predictable.
Choose marked if you want a fast, straightforward Markdown→HTML renderer with GFM support and a simple but capable extension API. Renderer overrides, custom tokenizers, and walkTokens let you adjust output without managing a full AST. It’s practical for apps that need quick, safe customizations with minimal code, especially in browsers. As with most parsers, pair it with a sanitizer when handling untrusted Markdown.
Choose remark when you need a programmable content pipeline: parse Markdown to an AST, apply transformations, and compile to HTML (via rehype) or other targets. It excels at migrations, linting, structured content transforms, and integrating with MDX or custom formats. While the learning curve and plugin plumbing are higher than direct renderers, the resulting control and composability are unmatched. Add rehype-sanitize when rendering untrusted content and opt into remark-gfm for GFM support.
Choose remarkable if you prefer a markdown-it–style API with a leaner footprint and straightforward renderer rules. It offers good control over link rendering and syntax highlighting via a highlight callback, and it’s easy to drop into server or client code. The ecosystem is smaller and maintenance activity appears more limited compared to markdown-it, so evaluate long-term needs and extension requirements before committing. It remains capable for conventional Markdown→HTML pipelines without complex transformations.
Choose showdown for a configuration-driven renderer with an approachable extension system, especially if you value options like openLinksInNewWindow and built-in conveniences. It’s tolerant of Markdown variants and can be made to work in legacy contexts or simple widgets. However, its extension model is less structured than AST/token pipelines, and behavior may diverge from strict CommonMark in edge cases. Plan for external sanitization if rendering untrusted input.
Choose turndown when you need HTML→Markdown conversion, such as importing from WYSIWYG editors, CMS exports, or legacy HTML. Its rule-based DOM traversal makes it easy to encode house style (e.g., map
to fenced code blocks with language). It complements, rather than replaces, Markdown renderers—use it upstream of a renderer when you want round-trippable content. For complex HTML, expect to write a few custom rules to preserve semantics.Markdown parser done right. Fast and easy to extend.
Table of content
node.js:
npm install markdown-it
browser (CDN):
See also:
// node.js
// can use `require('markdown-it')` for CJS
import markdownit from 'markdown-it'
const md = markdownit()
const result = md.render('# markdown-it rulezz!');
// browser with UMD build, added to "window" on script load
// Note, there is no dash in "markdownit".
const md = window.markdownit();
const result = md.render('# markdown-it rulezz!');
Single line rendering, without paragraph wrap:
import markdownit from 'markdown-it'
const md = markdownit()
const result = md.renderInline('__markdown-it__ rulezz!');
(*) presets define combinations of active rules and options. Can be
"commonmark", "zero" or "default" (if skipped). See
API docs for more details.
import markdownit from 'markdown-it'
// commonmark mode
const md = markdownit('commonmark')
// default mode
const md = markdownit()
// enable everything
const md = markdownit({
html: true,
linkify: true,
typographer: true
})
// full options list (defaults)
const md = markdownit({
// Enable HTML tags in source
html: false,
// Use '/' to close single tags (<br />).
// This is only for full CommonMark compatibility.
xhtmlOut: false,
// Convert '\n' in paragraphs into <br>
breaks: false,
// CSS language prefix for fenced blocks. Can be
// useful for external highlighters.
langPrefix: 'language-',
// Autoconvert URL-like text to links
linkify: false,
// Enable some language-neutral replacement + quotes beautification
// For the full list of replacements, see https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.mjs
typographer: false,
// Double + single quotes replacement pairs, when typographer enabled,
// and smartquotes on. Could be either a String or an Array.
//
// For example, you can use '«»„“' for Russian, '„“‚‘' for German,
// and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp).
quotes: '“”‘’',
// Highlighter function. Should return escaped HTML,
// or '' if the source string is not changed and should be escaped externally.
// If result starts with <pre... internal wrapper is skipped.
highlight: function (/*str, lang*/) { return ''; }
});
import markdownit from 'markdown-it'
const md = markdownit
.use(plugin1)
.use(plugin2, opts, ...)
.use(plugin3);
Apply syntax highlighting to fenced code blocks with the highlight option:
import markdownit from 'markdown-it'
import hljs from 'highlight.js' // https://highlightjs.org
// Actual default values
const md = markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (__) {}
}
return ''; // use external default escaping
}
});
Or with full wrapper override (if you need assign class to <pre> or <code>):
import markdownit from 'markdown-it'
import hljs from 'highlight.js' // https://highlightjs.org
// Actual default values
const md = markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre><code class="hljs">' +
hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
'</code></pre>';
} catch (__) {}
}
return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + '</code></pre>';
}
});
linkify: true uses linkify-it. To
configure linkify-it, access the linkify instance through md.linkify:
md.linkify.set({ fuzzyEmail: false }); // disables converting email to link
If you are going to write plugins, please take a look at Development info.
Embedded (enabled by default):
Via plugins:
By default all rules are enabled, but can be restricted by options. On plugin load all its rules are enabled automatically.
import markdownit from 'markdown-it'
// Activate/deactivate rules, with currying
const md = markdownit()
.disable(['link', 'image'])
.enable(['link'])
.enable('image');
// Enable everything
const md = markdownit({
html: true,
linkify: true,
typographer: true,
});
You can find all rules in sources:
Here is the result of readme parse at MB Pro Retina 2013 (2.4 GHz):
npm run benchmark-deps
benchmark/benchmark.mjs readme
Selected samples: (1 of 28)
> README
Sample: README.md (7774 bytes)
> commonmark-reference x 1,222 ops/sec ±0.96% (97 runs sampled)
> current x 743 ops/sec ±0.84% (97 runs sampled)
> current-commonmark x 1,568 ops/sec ±0.84% (98 runs sampled)
> marked x 1,587 ops/sec ±4.31% (93 runs sampled)
Note. CommonMark version runs with simplified link normalizers for more "honest" compare. Difference is ≈1.5×.
As you can see, markdown-it doesn't pay with speed for its flexibility.
Slowdown of "full" version caused by additional features not available in
other implementations.
Available as part of the Tidelift Subscription.
The maintainers of markdown-it and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. Learn more.
markdown-it is the result of the decision of the authors who contributed to 99% of the Remarkable code to move to a project with the same authorship but new leadership (Vitaly and Alex). It's not a fork.
Big thanks to John MacFarlane for his work on the CommonMark spec and reference implementations. His work saved us a lot of time during this project's development.
Related Links:
Ports