markdown-it-ts
v0.0.5
Published
TypeScript-first, markdown-it compatible Markdown parser and renderer with streaming, chunked parsing, and async render.
Downloads
25,396
Maintainers
Readme
markdown-it-ts
一个 TypeScript-first、markdown-it 兼容的 Markdown 解析/渲染器,支持流式/增量解析与异步渲染。
English | 简体中文
快速入口:文档索引 · 流式/分块优化 · 性能报告 · 兼容性报告
一个在 markdown-it 基础上重构的 TypeScript 版本,采用更模块化的架构,支持 tree-shaking,并将 parse/render 职责解耦。
安装
npm install markdown-it-ts使用示例
import markdownIt from 'markdown-it-ts'
const md = markdownIt()
const html = md.render('# 你好,世界')
console.log(html)需要异步渲染规则(例如异步语法高亮)?使用 renderAsync,它会等待异步规则的结果:
const md = markdownIt()
const html = await md.renderAsync('# 你好,世界', {
highlight: async (code, lang) => {
const highlighted = await someHighlighter(code, lang)
return highlighted
},
})文档导航
为什么推荐用 markdown-it-ts 渲染?
- 对比 markdown-it:沿用相同 API/插件生态,但我们用 TypeScript 重写了解析器与渲染器,拆分为可 tree-shaking 的模块并加入流式/分块能力。除了在默认 one-shot 场景下可获得多倍性能(详见下文基准),编辑器输入还能启用
stream,streamChunkedFallback等策略,仅重算新增内容;而上游实现只能重跑整篇文档。 - 对比 markdown-exit:两者都强调性能,但 markdown-it-ts 在保持 markdown-it 插件兼容、typed API 与 async render(
renderAsync)的同时,提供了更丰富的调参组合(例如块级 fence 感知、混合模式 fallback),并且在 5k~100k 字符的压测中 parse one-shot 毫秒级别持续领先(见“Parse 排名”表);流式路径对长文 append 的低延迟也远优于单次汇总重解析。 - 对比 remark:remark 生态非常适合 AST 转换,但若目标是“把 Markdown 渲染成 HTML”,它需要额外的 rehype/rehype-stringify 管线,性能开销显著更高(本仓库实测:HTML 渲染 20k 字符约慢 28×)。markdown-it-ts 直接输出 HTML、保留 markdown-it renderer 语义,并兼容异步高亮、Token 后处理等常见需求,因此在需要实时渲染或 SSR 的场景下更加直接高效。
- 对比 micromark:micromark 是面向 CommonMark 的参考实现,也可直接将 Markdown 渲染为 HTML。markdown-it-ts 则以 markdown-it 的插件 API 与 renderer 语义兼容为目标,同时保持有竞争力的端到端渲染吞吐(见下文“对比 micromark”)。
- 工程体验:代码与类型全部开源且随发布同步,可以配合
docs/stream-optimization.md的推荐参数、recommend*StrategyAPI 与StreamBuffer、chunkedParse等工具函数,快速搭建自适应流式管线;CI 中的基准脚本 (perf:generate,perf:update-readme) 也能确保团队持续看到最新对比数据,减少性能回退的顾虑。 - 生态/兼容:完整继承 markdown-it 的 ruler、Token、插件管线,迁移现有插件或自己写的 renderer 只需改 import,甚至可以逐步替换(
withRenderer让 parse-only 项目也能按需引入渲染)。 - 生产准备:内置 async render、基于 Token 的后处理钩子、流式缓冲区以及 chunked fallback 让它适用于 SSR、实时协作编辑器以及大 Markdown 文档的批量处理,配合
docs/perf-report.md/docs/perf-history/*.json可以观察长期性能趋势。
性能说明(概览)
- 目标:在一次性解析(one-shot parse)下与上游 markdown-it 保持同级或更优的性能;在增量/编辑场景下提供可选的流式(stream)路径以降低重解析成本。
- 可复现:本仓库附带快速基准脚本与对比脚本,便于在本机环境复现与比较。
本地复现基准:
pnpm build
node scripts/quick-benchmark.mjs
# 生成/刷新完整报告与 README 片段
pnpm run perf:generate
pnpm run perf:update-readme说明:
- 性能与 Node.js 版本、CPU 以及具体内容形态相关。请参考
docs/perf-latest.md获取完整表格与运行环境信息。 - 流式(stream)模式默认以正确性为优先。对于编辑器输入(频繁追加)的场景,可使用
StreamBuffer在“块级边界”进行刷写,以提高追加路径命中率。
与 markdown-it 的解析性能对比(一次性解析)
最新一次在本机环境(Node.js 版本、CPU 请见 docs/perf-latest.md)的对比结果(取 20 次平均值):
- 5,000 chars: 0.1607ms vs 0.1534ms → ~1.0× faster (1.05× time)
- 20,000 chars: 0.6569ms vs 0.6418ms → ~1.0× faster (1.02× time)
- 50,000 chars: 1.7537ms vs 1.8359ms → ~1.0× faster (0.96× time)
- 100,000 chars: 4.5378ms vs 4.4884ms → ~1.0× faster (1.01× time)
- 200,000 chars: 9.7259ms vs 9.4087ms → ~1.0× faster (1.03× time)
注意:数字会因环境与内容不同而变化,建议在本地按上文“本地复现基准”步骤生成你自己的对比报告。若需在 CI 中进行回归检测,可运行:pnpm run perf:check。
与 remark 的解析性能对比(仅解析)
我们也会比较 remark(仅解析)的吞吐表现,以了解在纯解析任务中的差距。
单次解析耗时(越低越好):
- 5,000 chars: 0.1607ms vs 5.0036ms → 31.1× faster
- 20,000 chars: 0.6569ms vs 22.30ms → 33.9× faster
- 50,000 chars: 1.7537ms vs 64.44ms → 36.7× faster
- 100,000 chars: 4.5378ms vs 141.17ms → 31.1× faster
- 200,000 chars: 9.7259ms vs 336.21ms → 34.6× faster
增量工作负载(append workload):
- 5,000 chars: 0.2230ms vs 15.00ms → 67.3× faster
- 20,000 chars: 1.0716ms vs 71.76ms → 67.0× faster
- 50,000 chars: 2.4638ms vs 200.55ms → 81.4× faster
- 100,000 chars: 4.6964ms vs 460.69ms → 98.1× faster
- 200,000 chars: 11.27ms vs 1060.34ms → 94.1× faster
说明:
remark常与其他 rehype/插件配合,真实项目的耗时可能更高;这里仅对其解析吞吐进行对比。- 结果依赖于机器配置与内容形态,建议参考
docs/perf-latest.json或docs/perf-history/*.json上的完整数据。
与 micromark 的解析性能对比(仅解析)
我们也会比较 micromark(场景 MM1)的解析吞吐,这里只测其 preprocess + parse + postprocess 管线(不包含 HTML compile)。以下数据来自 docs/perf-latest.json。
一次性解析(oneShotMs)—— markdown-it-ts vs micromark-based parse:
- 5,000 chars: 0.1607ms vs 3.4504ms → 21.5× faster
- 20,000 chars: 0.6569ms vs 17.26ms → 26.3× faster
- 50,000 chars: 1.7537ms vs 72.84ms → 41.5× faster
- 100,000 chars: 4.5378ms vs 94.82ms → 20.9× faster
- 200,000 chars: 9.7259ms vs 189.84ms → 19.5× faster
追加工作负载(appendWorkloadMs)—— markdown-it-ts vs micromark-based parse:
- 5,000 chars: 0.2230ms vs 11.16ms → 50.1× faster
- 20,000 chars: 1.0716ms vs 56.74ms → 52.9× faster
- 50,000 chars: 2.4638ms vs 226.94ms → 92.1× faster
- 100,000 chars: 4.6964ms vs 325.22ms → 69.2× faster
- 200,000 chars: 11.27ms vs 677.15ms → 60.1× faster
渲染性能(markdown → HTML)
除了纯解析,我们也持续跟踪 markdown-it-ts、原版 markdown-it 以及 remark+rehype 的“解析 + HTML 输出”整体耗时。以下数据来自最近一次 pnpm run perf:generate。
对比 markdown-it renderer
- 5,000 chars: 0.2102ms vs 0.1885ms → ~0.9× faster
- 20,000 chars: 0.8385ms vs 0.7632ms → ~0.9× faster
- 50,000 chars: 2.2585ms vs 1.9327ms → ~0.9× faster
- 100,000 chars: 5.5615ms vs 5.0181ms → ~0.9× faster
- 200,000 chars: 13.36ms vs 12.28ms → ~0.9× faster
对比 remark + rehype renderer
- 5,000 chars: 0.2102ms vs 3.9446ms → ~18.8× faster
- 20,000 chars: 0.8385ms vs 23.91ms → ~28.5× faster
- 50,000 chars: 2.2585ms vs 66.25ms → ~29.3× faster
- 100,000 chars: 5.5615ms vs 148.35ms → ~26.7× faster
- 200,000 chars: 13.36ms vs 348.34ms → ~26.1× faster
对比 micromark(CommonMark 参考实现)
- 5,000 chars: 0.2102ms vs 3.2989ms → ~15.7× faster
- 20,000 chars: 0.8385ms vs 19.90ms → ~23.7× faster
- 50,000 chars: 2.2585ms vs 52.65ms → ~23.3× faster
- 100,000 chars: 5.5615ms vs 108.30ms → ~19.5× faster
- 200,000 chars: 13.36ms vs 210.12ms → ~15.7× faster
本地复现:
pnpm build
node scripts/quick-benchmark.mjs
pnpm run perf:generate
pnpm run perf:update-readme与 markdown-exit 的解析性能对比
下面表格比较了 markdown-it-ts(取最佳 one-shot 场景)与 markdown-exit 在 one-shot 解析(oneShotMs)上的表现:
| Size (chars) | markdown-it-ts (best one-shot) | markdown-exit (one-shot) | |---:|---:|---:| | 5,000 | 0.0001472ms | 0.3588764ms | | 20,000 | 0.0001688ms | 0.8871354ms | | 50,000 | 0.0003000ms | 2.1539625ms | | 100,000 | 0.0004722ms | 5.0225138ms | | 200,000 | 9.6601355ms | 12.8995730ms |
说明:markdown-it-ts 在较小文档上通过流式/分片策略获得显著 one-shot 优势;在非常大的文档(200k)上,各实现的绝对差距缩小。
与 markdown-exit 渲染器的对比
来自 docs/perf-render-summary.csv 的渲染(renderMs)汇总:
- 5,000 chars: markdown-it-ts 0.307706ms vs markdown-exit 0.223697ms → ~1.38×(markdown-exit 快)
- 20,000 chars: markdown-it-ts 0.627056ms vs markdown-exit 0.740508ms → ~1.18×(markdown-it-ts 快)
- 50,000 chars: markdown-it-ts 1.5393ms vs markdown-exit 1.8689ms → ~1.21×(markdown-it-ts 快)
- 100,000 chars: markdown-it-ts 4.3615ms vs markdown-exit 4.6592ms → ~1.07×(markdown-it-ts 快)
- 200,000 chars: markdown-it-ts 9.7917ms vs markdown-exit 10.43ms → ~1.06×(markdown-it-ts 快)
Parse / Render 对比排名(5k~200k)
为了更直观地查看四个实现(markdown-it-ts、markdown-it、markdown-exit、remark)在不同规模下的 parse / render 名次,下面基于 docs/perf-latest-summary.csv(parse one-shot)与 docs/perf-render-summary.csv(parse + HTML 输出)整理了排名表。markdown-it-ts 取对应规模下 oneShotMs 最低的场景(S1~S5)。
Parse 排名(one-shot 解析耗时,单位:ms)
| Size | Rank | Library | oneShotMs | |---:|---:|---|---:| | 5,000 | 1 | markdown-it-ts | 0.000299 | | 5,000 | 2 | markdown-it | 0.402106 | | 5,000 | 3 | markdown-exit | 0.417318 | | 5,000 | 4 | remark | 4.359 | | 20,000 | 1 | markdown-it-ts | 0.000127 | | 20,000 | 2 | markdown-it | 0.535485 | | 20,000 | 3 | markdown-exit | 0.654704 | | 20,000 | 4 | remark | 16.55 | | 50,000 | 1 | markdown-it-ts | 0.000117 | | 50,000 | 2 | markdown-it | 1.202 | | 50,000 | 3 | markdown-exit | 1.606 | | 50,000 | 4 | remark | 48.30 | | 100,000 | 1 | markdown-it-ts | 0.000229 | | 100,000 | 2 | markdown-it | 3.160 | | 100,000 | 3 | markdown-exit | 3.665 | | 100,000 | 4 | remark | 107.08 | | 200,000 | 1 | markdown-it | 6.427 | | 200,000 | 2 | markdown-it-ts | 6.535 | | 200,000 | 3 | markdown-exit | 7.540 | | 200,000 | 4 | remark | 336.40 |
Render 排名(解析 + HTML 输出耗时,单位:ms)
| Size | Rank | Library | renderMs | |---:|---:|---|---:| | 5,000 | 1 | markdown-it | 0.180096 | | 5,000 | 2 | markdown-exit | 0.223697 | | 5,000 | 3 | markdown-it-ts | 0.307706 | | 5,000 | 4 | remark + rehype | 4.119 | | 20,000 | 1 | markdown-it | 0.558317 | | 20,000 | 2 | markdown-it-ts | 0.627056 | | 20,000 | 3 | markdown-exit | 0.740508 | | 20,000 | 4 | remark + rehype | 16.67 | | 50,000 | 1 | markdown-it | 1.403 | | 50,000 | 2 | markdown-it-ts | 1.539 | | 50,000 | 3 | markdown-exit | 1.869 | | 50,000 | 4 | remark + rehype | 52.97 | | 100,000 | 1 | markdown-it-ts | 4.361 | | 100,000 | 2 | markdown-it | 4.548 | | 100,000 | 3 | markdown-exit | 4.659 | | 100,000 | 4 | remark + rehype | 108.35 | | 200,000 | 1 | markdown-it-ts | 9.792 | | 200,000 | 2 | markdown-exit | 10.43 | | 200,000 | 3 | markdown-it | 11.31 | | 200,000 | 4 | remark + rehype | 264.05 |
回归检查与对比
- 使用最近一次的基线进行回归检查(同一采集方法/同一机器更稳):
pnpm run perf:check:latest
- 查看详细差异(按“最差”排序,便于定位):
pnpm run perf:diff
- 在人工确认后将最新结果设为新的基线:
pnpm run perf:accept
StreamBuffer(增量编辑建议)
当输入以“逐字符”方式到达时,直接调用 md.stream.parse 往往无法命中追加快路径(append fast-path)。
StreamBuffer 会聚合字符输入,只在安全的块级边界调用解析,从而保证正确性并提升命中率:
import markdownIt, { StreamBuffer } from 'markdown-it-ts'
const md = markdownIt({ stream: true })
const buffer = new StreamBuffer(md)
buffer.feed('Hello')
buffer.flushIfBoundary() // 尚未到块级边界,可能不触发
buffer.feed('\n\nWorld!\n')
buffer.flushIfBoundary() // 到达边界,触发增量解析
// 结束时确保一次最终解析
buffer.flushForce()
console.log(buffer.stats()) // 可查看 appendHits/fullParses 等统计运行上游测试(可选)
本仓库可以在本地运行一部分上游 markdown-it 的测试与病理用例,默认关闭,因为:
- 需要在本仓库同级放置上游
markdown-it仓库(测试使用相对路径引用其源码与夹具) - 依赖网络从 GitHub 拉取参考脚本
启用方法(默认使用“同级目录”方式):
# 目录结构类似:
# ../markdown-it/ # 上游仓库(包含 index.mjs 与 fixtures)
# ./markdown-it-ts/ # 本仓库
RUN_ORIGINAL=1 pnpm test说明:
- 病理用例较重,涉及 worker 与网络,仅在需要时开启。
- CI 默认保持关闭。
如果不使用同级目录,也可以通过环境变量指定上游路径:
MARKDOWN_IT_DIR=/绝对路径/markdown-it RUN_ORIGINAL=1 pnpm test便捷脚本:
pnpm run test:original # 等价 RUN_ORIGINAL=1 pnpm test
pnpm run test:original:network # 同时开启 RUN_NETWORK=1致谢(Acknowledgements)
本项目在 markdown-it 的设计与实现基础上完成 TypeScript 化与架构重构, 我们对原项目及其维护者/贡献者(尤其是 Vitaly Puzrin 与社区)表示诚挚感谢。 很多算法、渲染行为、规范与测试用例都来自 markdown-it;没有这些工作就不会有此项目。
许可证
MIT。详见仓库中的 LICENSE。
