markdown-it、remarkable 和 showdown 都是用于将 Markdown 文本转换为 HTML 的 JavaScript 库,适用于在浏览器或 Node.js 环境中渲染用户输入的 Markdown 内容。它们都支持标准 CommonMark 语法,并提供扩展机制以支持自定义功能(如任务列表、脚注、高亮等)。这些库广泛用于博客系统、文档站点、富文本编辑器预览和内容管理系统中,核心目标是在保证安全性和性能的前提下,提供灵活、可配置的 Markdown 渲染能力。
在前端开发中,将用户输入的 Markdown 安全高效地渲染为 HTML 是常见需求。markdown-it、remarkable 和 showdown 是三个主流选择,但它们在架构、扩展性和维护状态上差异显著。本文从工程实践角度,对比三者的核心能力。
markdown-it 采用基于插件的模块化架构,所有核心功能(包括链接、图片)都可通过插件启用或替换。它暴露完整的解析器(parser)、渲染器(renderer)和规则链(rules),允许深度干预解析过程。
// markdown-it: 插件扩展
import MarkdownIt from 'markdown-it';
import markdownItTaskLists from 'markdown-it-task-lists';
const md = new MarkdownIt()
.use(markdownItTaskLists); // 启用任务列表插件
const html = md.render('- [x] done');
remarkable 提供有限的内置扩展(如 breaks、linkify),但不支持第三方插件。其扩展需通过直接修改内部规则实现,灵活性较低。
// remarkable: 内置选项配置
import Remarkable from 'remarkable';
const md = new Remarkable({
breaks: true, // 转换 \n 为 <br>
linkify: true // 自动识别 URL
});
const html = md.render('Visit https://example.com');
showdown 使用选项(options)和扩展(extensions)两种方式。扩展需通过全局注册或实例方法添加,但 API 较为冗长。
// showdown: 扩展注册
import showdown from 'showdown';
showdown.extension('myext', function() {
return [
{
type: 'output',
filter: function(text) {
return text.replace(/@\w+/g, '<code>$&</code>');
}
}
];
});
const converter = new showdown.Converter({ extensions: ['myext'] });
const html = converter.makeHtml('Mention @user');
💡 关键区别:
markdown-it的插件机制最灵活,适合复杂定制;showdown的扩展需手动编写过滤器;remarkable几乎无法扩展新语法。
三者默认均不自动转义 HTML 标签,这意味着如果用户输入包含 <script>,会直接输出到 HTML 中,造成 XSS 风险。开发者必须显式启用转义或使用 sanitize 库。
markdown-it 推荐通过 html: false 禁用原生 HTML,并配合 markdown-it-sanitizer 等插件处理。
// markdown-it: 禁用 HTML + 安全插件
const md = new MarkdownIt({ html: false })
.use(require('markdown-it-sanitizer'));
remarkable 同样需设置 html: false,但无官方安全插件。
// remarkable: 禁用 HTML
const md = new Remarkable({ html: false });
showdown 通过 noHeaderId、ghCompatibleHeaderId 等选项间接提升安全,但核心仍需依赖 sanitize-html 等外部库。
// showdown: 配合 sanitize-html
import sanitizeHtml from 'sanitize-html';
const dirty = converter.makeHtml(userInput);
const clean = sanitizeHtml(dirty);
⚠️ 重要提醒:无论选择哪个库,都必须额外处理 XSS 防护。不要依赖解析器默认行为。
markdown-it 严格遵循 CommonMark 0.30 规范,并通过官方测试套件验证。对 GFM(GitHub Flavored Markdown)的支持通过插件(如 markdown-it-gfm)实现。remarkable 基于早期 CommonMark 草案,部分边缘情况(如嵌套列表、引用块)与标准存在差异。showdown 兼容 CommonMark,但默认行为更接近传统 Markdown(如不支持表格),需开启 tables: true 等选项。// 表格支持对比
const tableMd = '| a | b |\n|---|---|\n| 1 | 2 |';
// markdown-it: 需插件
md.use(require('markdown-it-tables'));
// showdown: 需选项
new showdown.Converter({ tables: true });
// remarkable: 不支持表格(需手动 hack)
remarkable:已停止维护。其 GitHub 仓库 README 明确写道:“此项目不再积极开发。推荐使用 markdown-it。” 新项目应避免选用。markdown-it 和 showdown 均处于活跃维护状态,定期发布更新并修复安全问题。markdown-itmarkdown-it-highlightjs、markdown-it-container)可快速实现所需功能,且 AST 支持便于静态分析。// markdown-it 示例:自定义容器
md.use(require('markdown-it-container'), 'tip', {
validate: params => params.trim().match(/^tip\s+(.*)$/),
render: (tokens, idx) => {
if (tokens[idx].nesting === 1) {
return '<div class="tip">';
} else {
return '</div>';
}
}
});
showdownsanitize-html 即可满足安全需求。// showdown 示例:基础配置
const converter = new showdown.Converter({
simpleLineBreaks: true,
strikethrough: true
});
remarkable 集成,功能稳定无扩展需求。markdown-it。| 特性 | markdown-it | remarkable | showdown |
|---|---|---|---|
| 扩展机制 | 插件系统(高度灵活) | 内置选项(不可扩展) | 扩展 + 选项 |
| CommonMark 兼容 | ✅ 严格遵循 | ⚠️ 部分兼容 | ✅(需配置) |
| 维护状态 | ✅ 活跃 | ❌ 已停止 | ✅ 活跃 |
| XSS 防护 | 需手动配置 + 插件 | 需手动配置 | 需手动配置 + 外部库 |
| 适用场景 | 复杂定制、文档平台 | 遗留系统 | 快速集成、简单需求 |
markdown-it:它的插件生态和标准兼容性为未来扩展留足空间。remarkable:除非维护旧代码,否则不要引入已废弃的依赖。showdown 作为轻量备选:当项目简单、团队熟悉其 API 时可考虑,但需自行处理安全和高级功能。无论选择哪个库,请始终记住:Markdown 解析只是第一步,安全渲染才是关键。
选择 markdown-it 如果你需要高度可扩展的解析器,支持插件生态、细粒度的 AST 操作,以及对 CommonMark 标准的严格遵循。它适合构建需要深度定制(如自定义语法、安全过滤、语法高亮集成)的复杂应用,例如文档平台或开发者工具。
选择 remarkable 如果你追求极致的解析速度和简洁的 API,且不需要频繁扩展功能。但需注意:该库已不再积极维护,官方 GitHub 仓库明确建议新项目使用 markdown-it。因此,仅在遗留系统维护或对性能有极端要求且功能固定的场景下考虑。
选择 showdown 如果你需要一个简单易用、开箱即用的解析器,且偏好基于选项(options)而非插件的配置方式。它适合快速集成到中小型项目中(如评论系统、静态站点生成器),尤其当你不需要复杂的 AST 操作或高性能要求时。
Markdown parser done right. Fast and easy to extend.
Table of content
node.js:
npm install markdown-it
[!NOTE]
For a quick look at
dist/folder contents, see https://unpkg.com/markdown-it/.For browser you can use unpkg.com, esm.sh or any other CDN, wich mirror npm registry
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.
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