markdown-it, marked, and showdown are libraries that convert Markdown text into HTML, enabling rich text rendering in web applications. turndown and node-html-markdown perform the reverse operation, converting HTML content back into Markdown format. These tools are essential for content management systems, comment sections, documentation sites, and any application requiring flexible text formatting or content migration.
When building content-driven applications, you often need to transform text between Markdown and HTML. The ecosystem offers several mature tools, but they serve different directions and architectural needs. markdown-it, marked, and showdown convert Markdown to HTML. turndown and node-html-markdown convert HTML back to Markdown. Let's explore how they differ in practice.
The most critical distinction is the direction of conversion. Using the wrong tool for the direction will break your workflow.
markdown-it converts Markdown to HTML with a focus on plugins.
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
const result = md.render('# Hello World');
// Output: <h1>Hello World</h1>
marked converts Markdown to HTML with a focus on speed.
import { marked } from 'marked';
const result = marked('# Hello World');
// Output: <h1>Hello World</h1>
showdown converts Markdown to HTML with a focus on extensions.
import { Converter } from 'showdown';
const converter = new Converter();
const result = converter.makeHtml('# Hello World');
// Output: <h1>Hello World</h1>
turndown converts HTML to Markdown, running in Node or browser.
import TurndownService from 'turndown';
const turndownService = new TurndownService();
const result = turndownService.turndown('<h1>Hello World</h1>');
// Output: # Hello World
node-html-markdown converts HTML to Markdown, optimized for Node.js.
import { NodeHtmlMarkdown } from 'node-html-markdown';
const result = NodeHtmlMarkdown.translate('<h1>Hello World</h1>');
// Output: # Hello World
How you customize the output varies significantly between these libraries. Some use a plugin system, while others rely on configuration objects.
markdown-it uses a token-based plugin system.
You can insert rules into the parsing chain to modify how tokens are generated.
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
md.use(require('markdown-it-emoji'));
const result = md.render(':smile:');
marked uses a renderer object or async extensions.
You override methods on the renderer to change HTML output for specific elements.
import { marked } from 'marked';
const renderer = new marked.Renderer();
renderer.heading = (text, level) => `<h${level} class="custom">${text}</h${level}>`;
const result = marked('# Hello', { renderer });
showdown uses a flavor system and custom extensions.
You can enable predefined flavors like 'github' or write custom language extensions.
import { Converter } from 'showdown';
const converter = new Converter({ flavor: 'github' });
const result = converter.makeHtml('# Hello');
turndown uses a rule-based system for HTML elements.
You add rules to handle specific tags or classes during the HTML to Markdown conversion.
import TurndownService from 'turndown';
const turndownService = new TurndownService();
turndownService.addRule('strikethrough', {
filter: ['del', 's', 'strike'],
replacement: (content) => `~~${content}~~`
});
node-html-markdown uses configuration options for specific tags.
It has fewer extension points but allows configuring how specific elements like images or links are handled.
import { NodeHtmlMarkdown } from 'node-html-markdown';
const result = NodeHtmlMarkdown.translate('<img src="a.jpg">', {
ignore: ['img'] // Example config to ignore specific tags
});
Rendering user-generated HTML carries security risks like XSS. Some libraries include sanitization, while others expect you to handle it separately.
markdown-it does not sanitize HTML by default.
You must pair it with a sanitizer like sanitize-html to prevent script injection.
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
const raw = md.render(userInput);
const clean = sanitizeHtml(raw);
marked has a built-in sanitizer option but it is deprecated.
The official recommendation is to use a dedicated library like dompurify for security.
import { marked } from 'marked';
import DOMPurify from 'dompurify';
const raw = marked(userInput);
const clean = DOMPurify.sanitize(raw);
showdown has a built-in sanitize option.
It can strip dangerous tags directly during the conversion process without extra dependencies.
import { Converter } from 'showdown';
const converter = new Converter({ sanitize: true });
const result = converter.makeHtml(userInput);
turndown focuses on conversion, not sanitization.
Since it outputs Markdown, the risk is lower, but you should still validate input HTML before conversion.
import TurndownService from 'turndown';
const turndownService = new TurndownService();
// Validate HTML string before passing to turndown
const result = turndownService.turndown(unsafeHtml);
node-html-markdown does not include sanitization features.
It assumes the input HTML is trusted or has been sanitized prior to conversion in your Node.js pipeline.
import { NodeHtmlMarkdown } from 'node-html-markdown';
// Ensure htmlInput is safe before translating
const result = NodeHtmlMarkdown.translate(htmlInput);
Performance matters when processing large documents or running on edge devices. Environment support (Browser vs Node) also dictates your choice.
markdown-it is highly optimized for Node and browser.
It parses into tokens first, which adds a slight overhead but allows for complex transformations.
// markdown-it works in both environments
const md = new MarkdownIt();
console.log(md.render('# Test'));
marked is known for being very fast.
It compiles Markdown directly to HTML strings, making it efficient for high-volume rendering.
// marked is lightweight and fast
console.log(marked('# Test'));
showdown is slightly heavier due to its extension system.
It works well in browsers but may be slower than marked on large documents.
// showdown is versatile but can be slower
const converter = new Converter();
console.log(converter.makeHtml('# Test'));
turndown relies on DOM parsing in the browser.
In Node.js, it requires a DOM implementation like jsdom to function correctly.
// turndown needs a DOM in Node.js
// const { JSDOM } = require('jsdom');
// global.window = new JSDOM('').window;
const turndownService = new TurndownService();
node-html-markdown is built specifically for Node.js.
It does not rely on a virtual DOM, making it faster and lighter for server-side scripts.
// node-html-markdown is Node-only
const result = NodeHtmlMarkdown.translate('<h1>Test</h1>');
Despite their differences, these libraries share common goals and patterns.
// All handle basic syntax similarly
// markdown-it
md.render('- item');
// marked
marked('- item');
// showdown
converter.makeHtml('- item');
// All allow some config
// markdown-it: md.set({ html: true })
// marked: marked.setOptions({ breaks: true })
// showdown: new Converter({ tables: true })
// Ecosystem examples
// markdown-it: markdown-it-container
// marked: marked-highlight
// turndown: turndown-plugin-gfm
| Feature | markdown-it | marked | showdown | turndown | node-html-markdown |
|---|---|---|---|---|---|
| Direction | MD โ HTML | MD โ HTML | MD โ HTML | HTML โ MD | HTML โ MD |
| Extensibility | Plugin System | Renderer Override | Extensions | Rule System | Config Options |
| Sanitization | External Required | External Recommended | Built-in Option | None | None |
| Environment | Node & Browser | Node & Browser | Node & Browser | Node & Browser | Node Only |
| Primary Use | Complex Docs | Fast Rendering | Legacy/Extensions | Content Editors | Server Scripts |
markdown-it is the choice for complex documentation systems. Its plugin architecture allows you to build custom content experiences, but it requires more setup to secure properly.
marked is the workhorse for general-purpose rendering. If you need to display comments or blog posts quickly and safely, pair this with a sanitizer and move on.
showdown remains useful for legacy systems or specific extension needs. For new greenfield projects, marked or markdown-it usually offer better long-term support.
turndown is the standard for HTML to Markdown conversion. If you are building a rich-text editor that exports to Markdown, this is the most reliable option across environments.
node-html-markdown is a niche tool for server-side Node.js scripts. Use it when you need to process HTML files in a build pipeline without the overhead of a DOM implementation.
Final Thought: Direction matters most. Pick markdown-it or marked for displaying content. Pick turndown for editing or migrating content. Always sanitize HTML output when rendering user input to keep your application secure.
Choose markdown-it if you need a highly extensible parser with a robust plugin ecosystem. It is ideal for projects requiring strict CommonMark compliance or custom syntax rules. Its architecture allows deep customization of the token stream, making it suitable for complex documentation platforms.
Choose marked if you prioritize speed and simplicity without sacrificing essential features. It is a solid default for rendering user-generated content where performance matters. Recent versions support async rendering, which helps when integrating with asynchronous highlighters or custom renderers.
Choose node-html-markdown if you are working exclusively in a Node.js environment and need a lightweight HTML to Markdown converter. It is optimized for server-side processing where browser DOM APIs are unavailable. It is less flexible than turndown but requires fewer dependencies.
Choose showdown if you are maintaining a legacy project that already depends on it or need specific older extensions. For new projects, consider modern alternatives, as its development pace is slower compared to marked or markdown-it. It remains viable for simple client-side rendering tasks.
Choose turndown if you need to convert HTML to Markdown in the browser or Node.js with high fidelity. It is the standard choice for content editors that allow switching between rich text and Markdown sources. It handles complex DOM structures well and supports custom rules.
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