markdown-it vs marked vs remark vs remarkable vs showdown vs turndown
Selecting JavaScript libraries for Markdown parsing, rendering, and HTML↔Markdown conversion
markdown-itmarkedremarkremarkableshowdownturndown

Selecting JavaScript libraries for Markdown parsing, rendering, and HTML↔Markdown conversion

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
markdown-it021,125768 kB58a month agoMIT
marked036,636444 kB114 days agoMIT
remark08,77215.7 kB72 years agoMIT
remarkable05,824-1326 years agoMIT
showdown014,836801 kB233-MIT
turndown010,883192 kB1504 months agoMIT

Parsing, Rendering, and Converting Markdown in JavaScript: A Technical Deep-Dive

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.

Design Philosophy

  • markdown-it: Tokenizer + renderer architecture with rule-level hooks. Aims at CommonMark compatibility and predictable rendering with opt-in extras. Emphasizes a plugin ecosystem for syntax extensions and renderer tweaks.
  • marked: A pragmatic renderer with GFM support and a concise extension API. Customization is done via renderer overrides, tokenizers, and token walking rather than an AST-first workflow.
  • remark: AST-first (mdast) via the unified pipeline. Markdown is parsed into a typed tree, then transformed and compiled (commonly to HTML via rehype). This favors complex transformations, content validation, and multi-target outputs.
  • remarkable: Similar surface area to markdown-it with renderer rules and a highlight hook. Simpler ecosystem and fewer moving parts.
  • showdown: Configuration-first with extension hooks. Less formal parsing model; trades strictness for convenience and broad compatibility with Markdown variants.
  • turndown: HTML-to-Markdown, not a renderer. Rule-based DOM traversal where each rule declares filter(s) and string replacements, enabling lossy or lossless mappings depending on your rules.

Parsing Model and Extensibility

  • Token streams (markdown-it, remarkable): Expose block/inline rules and renderer rules. You can inject attributes into specific tokens, alter nesting behavior, or add new syntaxes with plugins.
  • Tokenizers without a formal AST (marked): Offers tokenization with hooks. You can extend parsing via custom tokenizers and adjust output via a renderer object; walkTokens enables post-parse analysis.
  • AST-first (remark): remark-parse builds mdast; remark plugins transform that AST; remark-rehype converts to HAST; rehype plugins transform HTML AST; rehype-stringify emits HTML. The pipeline decomposes concerns and allows sophisticated invariants.
  • Regex/config-first (showdown): Options cover many behaviors; extensions can manipulate input or output with filter functions. Less structure than token/AST pipelines, but straightforward for light customization.
  • DOM rule-based (turndown): Each rule declares how to match HTML nodes (by tag, predicate) and how to produce Markdown. Reliable for controlled HTML; complex inputs may require multiple rules to avoid lossy conversion.

HTML Handling and Security Posture

  • None of the Markdown→HTML libraries sanitize by default in a security-hard sense. markdown-it and remarkable can disallow raw HTML entirely (html: false), but that’s not equivalent to sanitization. marked previously exposed sanitize-related options historically but now expects external sanitization (e.g., DOMPurify in browsers, or a server-side sanitizer). Showdown includes a sanitize toggle that strips HTML but is coarse and not a full sanitizer.
  • remark pipelines typically use rehype-sanitize, a configurable HTML AST sanitizer. This is the most programmable approach when you need to allow a controlled subset of HTML.
  • For browser contexts, DOMPurify remains a common final step regardless of parser if you allow raw HTML at any point.

Edge Cases and Spec Nuance

  • CommonMark and GFM: remark requires remark-gfm to support tables/strikethrough/task lists. marked supports GFM features behind options. markdown-it supports CommonMark and common GFM constructs, and its ecosystem covers the rest. remarkable and showdown handle many GFM-like features, but behaviors can diverge on corner cases (e.g., nested emphasis, list tight/loose spacing).
  • Autolink literals (bare URLs): markdown-it can auto-link with linkify: true. marked focuses on Markdown-specified links; autolinking bare URLs may require an extension/preprocess. remark can add autolinks with a plugin at either mdast or hast stage. showdown offers simplified autolink options that may link common URL patterns.
  • Raw HTML passthrough: markdown-it/remarkable can allow or block raw HTML. marked passes it through by default. For remark, raw HTML requires allowing it through remark-rehype plus rehype-raw and then sanitizing.

Performance Characteristics

  • Direct renderers (marked, markdown-it, remarkable) usually have lower overhead for “Markdown→HTML now” without AST transforms. They’re a good fit when you don’t need deep analysis or restructuring.
  • remark’s AST pipeline adds overhead but unlocks structured transformations, linting, and precise HTML control. In content-heavy builds (SSG/CMS), the clarity of transformations often offsets runtime cost.
  • showdown’s looser parsing and extension filters are simple to wire but can be less predictable on pathological inputs. turndown processes DOMs; cost scales with tree size and the complexity of your rules.

Output Control and Customization

  • markdown-it/remarkable: Customize via renderer.rules; add/remove attributes per token; implement syntax highlighting with a highlight callback.
  • marked: Customize via a Renderer instance or marked.use; walkTokens supports cross-cutting transformations. Highlighting is injected via a highlight option.
  • remark: Insert transformations where they make the most sense—mdast (structure), during mdast→hast (representation), or in HAST (HTML-specific policies like rel/target). Highlighting is typically handled with rehype plugins.
  • showdown: Configure via options; add small output filters as extensions. More brute-force than structural.
  • turndown: Precisely control Markdown output by authoring rules for tags or predicates; use it to preserve language hints for fenced code or normalize links/strong/emphasis the way you want.

The Same Task, Implemented Across Libraries

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.

Sample Input Markdown

# Demo

Visit https://example.com and [MDN](https://developer.mozilla.org/).

```js
console.log('hi')
NameScore
Foo42

### 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.

marked

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.

remark (unified + rehype)

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.

remarkable

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.

showdown

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.

turndown (HTML→Markdown with equivalent customizations)

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.

Observable Differences in Output

  • Link attributes: markdown-it/remarkable provide token-level hooks; marked centralizes link rendering via renderer.link; remark delegates to rehype, enabling site-wide policies with a single plugin; showdown needs an output filter.
  • Code highlighting: markdown-it/remarkable/marked accept a highlight function; remark integrates via rehype plugins; showdown typically relies on post-processing or output filters; turndown preserves language by inspecting class names.
  • Tables: remark requires remark-gfm; marked supports GFM tables with gfm enabled; markdown-it/remarkable parse common table syntax; showdown requires tables: true. Output HTML structure (thead/tbody alignment classes) varies—CSS hooks differ by library.
  • Bare URLs: markdown-it linkify auto-links; remark can add a plugin; marked and showdown may need opt-in or preprocessing.

Failure Modes and Limitations

  • Sanitization is not built-in at a security-hard level for renderers; failing to add a sanitizer can expose XSS if raw HTML is allowed.
  • Regex/string extensions (showdown) are easy to write but can break on nested or malformed HTML; prefer structural hooks when correctness is critical.
  • Deep structural rewrites (e.g., rewrite heading hierarchy, generate slugs, build TOCs) are far easier with remark’s AST pipeline than with token/regex-based systems.
  • Round-tripping is lossy by nature unless both directions agree on conventions. turndown mitigates this with targeted rules but complex interactive HTML or custom widgets won’t map cleanly to Markdown.

Synthesis

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.

How to Choose: markdown-it vs marked vs remark vs remarkable vs showdown vs turndown

  • markdown-it:

    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.

  • marked:

    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.

  • remark:

    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.

  • remarkable:

    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.

  • showdown:

    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.

  • turndown:

    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.

README for markdown-it

markdown-it

CI NPM version Coverage Status Gitter

Markdown parser done right. Fast and easy to extend.

Live demo

  • Follows the CommonMark spec + adds syntax extensions & sugar (URL autolinking, typographer).
  • Configurable syntax! You can add new rules and even replace existing ones.
  • High speed.
  • Safe by default.
  • Community-written plugins and other packages on npm.

Table of content

Install

node.js:

npm install markdown-it

browser (CDN):

Usage examples

See also:

Simple

// 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!');

Init with presets and options

(*) 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 ''; }
});

Plugins load

import markdownit from 'markdown-it'

const md = markdownit
  .use(plugin1)
  .use(plugin2, opts, ...)
  .use(plugin3);

Syntax highlighting

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

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

API

API documentation

If you are going to write plugins, please take a look at Development info.

Syntax extensions

Embedded (enabled by default):

Via plugins:

Manage rules

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:

Benchmark

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.

markdown-it for enterprise

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.

Authors

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.

References / Thanks

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