npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

markdown-it-ts

v1.0.0

Published

TypeScript-first Markdown parser and renderer compatible with the markdown-it public API, with streaming, chunked parsing, and async render.

Readme

markdown-it-ts

一个 TypeScript-first、兼容 markdown-it public API 的 Markdown 解析/渲染器,支持流式/增量解析与异步渲染。

English | 简体中文

快速入口:文档索引 · 流式/分块优化 · 性能报告 · 兼容性报告

运行时说明

markdown-it-ts 是 ESM-only 包,要求 Node.js >= 18。

import MarkdownIt from 'markdown-it-ts'

如果你的项目仍然是 CommonJS,请在 async 函数里使用动态导入:

async function main() {
  const { default: MarkdownIt } = await import('markdown-it-ts')

  const md = MarkdownIt()
  console.log(md.render('# ok'))
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

一个在 markdown-it 基础上重构的 TypeScript 版本,采用更模块化的架构,支持 tree-shaking,并将 parse/render 职责解耦。

兼容性边界

markdown-it-ts 目标是兼容 markdown-it public API 中常见的 parser、renderer、plugin 用法。不支持 private markdown-it/lib/... 导入、依赖上游未文档化内部状态、直接 CommonJS require('markdown-it-ts'),也不支持 Node.js < 18。

| 层级 | API 面 | | --- | --- | | 稳定目标 | MarkdownIt()parserenderrenderInlinerenderAsyncrenderer.rulesToken、公开 ruler/plugin API | | 高级用法 | Root withRenderer,以及已文档化的子路径导出,例如 core、renderer helper、common utilities | | 实验性 | streamchunkedParseStreamBufferUnboundedBufferEditableBufferPieceTable、iterable/sink parsing、chunk strategy recommender 通过 markdown-it-ts/experimental 使用;部分 helper 也有显式子路径,例如 markdown-it-ts/stream/buffermarkdown-it-ts/stream/chunkedmarkdown-it-ts/stream/debouncedmarkdown-it-ts/support/chunk_recommend |

根入口不再以顶层 named export 暴露实验性 helper。部分高级实例方法和选项仍保留给既有的大输入集成使用,并会在类型声明中标记为 experimental。

常见 0.x import 迁移:

| 0.x import | 1.0 import | | --- | --- | | import { StreamBuffer } from 'markdown-it-ts' | import { StreamBuffer } from 'markdown-it-ts/experimental'markdown-it-ts/stream/buffer | | import { chunkedParse } from 'markdown-it-ts' | import { chunkedParse } from 'markdown-it-ts/experimental'markdown-it-ts/stream/chunked | | import { recommendFullChunkStrategy } from 'markdown-it-ts' | import { recommendFullChunkStrategy } from 'markdown-it-ts/support/chunk_recommend' | | import { UnboundedBuffer } from 'markdown-it-ts' | import { UnboundedBuffer } from 'markdown-it-ts/experimental' |

安装

npm install markdown-it-ts

使用示例

import markdownIt from 'markdown-it-ts'

const md = markdownIt()
const html = md.render('# 你好,世界')
console.log(html)

安全提醒:markdown-it-ts 不是 HTML sanitizer。默认会转义 raw HTML,但 html: true 会直接放行 raw HTML,插件写入的属性也会被视为可信输出。处理不可信作者内容时,请在应用边界额外做 HTML sanitize。

大文本输入

普通场景继续使用原来的 markdown-it 兼容 API 即可:

const md = markdownIt()
const tokens = md.parse(hugeMarkdown)
const html = md.render(hugeMarkdown)

默认的 parse / render 在输入超过大文档阈值时,可能切到内部的大文本优化路径。为了兼容插件生态,这个隐式路径只会在没有安装插件、parser ruler 没有被修改时启用;调用 .use() 或自定义 parser rule 后默认继续走 plain full parse。如需显式启用分块路径,请使用 experimental.fullChunkedFallback

只有当你的上游输入本来就是 chunk 流,而且你不想先 join('') 成一个超大字符串时,才需要显式使用下面这些高级入口:

import MarkdownIt from 'markdown-it-ts'
import { UnboundedBuffer } from 'markdown-it-ts/experimental'

const md = MarkdownIt()

const tokens = md.parseIterable(fileChunks)

const buffer = new UnboundedBuffer(md, { mode: 'stream' })
for await (const chunk of logChunks) {
  buffer.feed(chunk)
  buffer.flushAvailable()
}
const finalTokens = buffer.flushForce()

大输入调参选项建议放在 experimental 命名空间下:

const md = MarkdownIt({
  experimental: {
    autoUnbounded: false,
    fullChunkedFallback: true,
  },
})

旧的顶层实验选项在 1.x 中仍然保留兼容,但推荐新代码使用命名空间写法。

parseIterable / parseAsyncIterable 用于“输入本身就是 Iterable<string> / AsyncIterable<string>”的高级场景;UnboundedBuffer 用于 append-only 的 chunk 流,只保留有界尾部,而不是把历史全文一直留在一个大字符串里。

如果显式使用 chunk 流,而且连输出也不想保留完整 token 数组,可以直接走 sink 形式:

md.parseIterableToSink(fileChunks, (tokens, info) => {
  consumeTokenChunk(tokens, info)
})

如果是任意位置的中间编辑,可以用 EditableBuffer。它内部用 piece-table 保存源码,并从受影响块之前的锚点开始重解析,而不是每次都把整篇文本重新摊平成一个大字符串再 full parse。现在 full parse 和局部重解析都会直接把 PieceTableSourceView 交给 md.core.parseSource(...),因此被解析的区间也不需要先物化成一个超大的中间字符串。

chunked / streaming 正确性说明

Markdown 并不总是 chunk-local 的语言。某些语法依赖整篇文档状态,例如 reference definitions、footnote definitions、abbreviation definitions,以及插件自定义的全局状态。

chunkedParse() 和完整字符串的 unbounded parsing 默认采用 correctness-first 策略:遇到已知全局状态语法时会 fallback 到 full parse。强制分块如果落在非空行边界,也会退回 full parse,因为长列表、blockquote、HTML block 和普通段落都不能随意切开后再拼 token。

Iterable/sink 解析偏 streaming 场景;它不一定能在提交前面的 chunk 前看到后续的 reference/footnote/abbr definition。因此如果需要严格 full-parse 等价,包含全局定义的文档应优先使用完整字符串解析,或避免过早 flush。

检测器是保守的:即使 definition-like 文本出现在代码块或普通文本里,也可能触发 fallback。这个策略优先保证正确性,而不是极限性能。

你只能显式关闭已知全局状态语法触发的 fallback:

chunkedParse(md, source, env, {
  fallbackOnGlobalState: false,
})

非空行上的 unsafe chunk boundary 仍然会退回 full parse,因为在那里切分无法保证 token 流安全。

关闭全局状态 fallback 属于性能优先模式;对于包含全局状态的文档,输出可能和 full parse 不一致。

需要异步渲染规则(例如异步语法高亮)?使用 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 的模块并加入流式/分块能力。普通 parse / render 调用方式保持不变,超大但有限的字符串会自动启用内部大文本优化;如果是编辑器输入,还可以额外启用 streamstreamChunkedFallback 等策略,仅重算新增内容,而不是每次重跑整篇文档。
  • 对比 markdown-exit:两者都强调性能,但 markdown-it-ts 保留 markdown-it API/插件面、typed API 与 async render(renderAsync),并提供更丰富的调参组合(例如块级 fence 感知、混合模式 fallback)。在本仓库 5k~100k 字符 synthetic harness 中,markdown-it-ts 的 parse one-shot 延迟领先(见“Parse 排名”表);流式路径对长文 append 的延迟也低于每次汇总重解析。
  • 对比 remark:remark 生态非常适合 AST 转换,真实项目通常还会叠加 unified/rehype 阶段。这里的数字只比较本仓库 Markdown → HTML harness;markdown-it-ts 直接输出 HTML、保留 markdown-it renderer 语义,并兼容异步高亮、Token 后处理等常见需求。
  • 对比 micromark:micromark 是面向 CommonMark 的参考实现,目标和 API 都不同。markdown-it-ts 以 markdown-it 的插件 API 与 renderer 语义兼容为目标;下方数字只代表本仓库 harness 覆盖的特定 parse/render 场景。
  • 工程体验:代码与类型全部开源且随发布同步,可以配合 docs/stream-optimization.mdmarkdown-it-ts/experimental 以及 recommend*StrategyStreamBufferchunkedParse 等已开放显式子路径工具,快速搭建自适应流式管线;CI 中的基准脚本 (perf:generate, perf:update-readme) 也能确保团队持续看到最新对比数据,减少性能回退的顾虑。
  • 生态/兼容:继承 markdown-it 的 ruler、Token、插件管线,迁移现有插件或自己写的 renderer 通常只需改 import;CommonMark fixture 和插件矩阵在 CI 中默认运行。
  • 生产准备:内置 async render、基于 Token 的后处理钩子、流式缓冲区以及 chunked fallback 让它适用于 SSR、实时协作编辑器以及大 Markdown 文档的批量处理,配合 docs/perf-report.md / docs/perf-history/*.json 可以观察长期性能趋势。

性能说明(概览)

  • 目标:在本仓库 synthetic paragraph-heavy / append-heavy harness 下验证默认路径和流式路径的性能;真实项目请用自己的语料复现,不把这些数字理解成“所有 workload 都更快”。
  • 可复现:本仓库附带快速基准脚本与对比脚本,便于在本机环境复现与比较。

本地复现基准:

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.1448ms vs 0.2630ms → ~1.8× faster, ~45% less time
  • 20,000 chars: 0.5903ms vs 0.6548ms → ~1.1× faster, ~10% less time
  • 100,000 chars: 3.7770ms vs 4.6536ms → ~1.2× faster, ~19% less time
  • 500,000 chars: 24.26ms vs 24.78ms → ~1× faster, ~2% less time
  • 1,000,000 chars: 47.06ms vs 59.67ms → ~1.3× faster, ~21% less time

注意:数字会因环境与内容不同而变化,建议在本地按上文“本地复现基准”步骤生成你自己的对比报告。若需在 CI 中进行回归检测,可运行:pnpm run perf:check

与 remark 的解析性能对比(仅解析)

我们也会比较 remark(仅解析)的吞吐表现,以了解在纯解析任务中的差距。

单次解析耗时(越低越好):

  • 5,000 chars: 0.1448ms vs 5.0124ms → 34.6× faster
  • 20,000 chars: 0.5903ms vs 24.43ms → 41.4× faster
  • 100,000 chars: 3.7770ms vs 150.38ms → 39.8× faster

增量工作负载(append workload):

  • 5,000 chars: 0.2794ms vs 16.46ms → 58.9× faster
  • 20,000 chars: 1.1127ms vs 77.51ms → 69.7× faster
  • 100,000 chars: 5.4172ms vs 489.66ms → 90.4× faster

说明:

  • remark 常与其他 rehype/插件配合,真实项目的耗时可能更高;这里仅对其解析吞吐进行对比。
  • 结果依赖于机器配置与内容形态,建议参考 docs/perf-latest.jsondocs/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.1448ms vs 4.2385ms → 29.3× faster
  • 20,000 chars: 0.5903ms vs 17.81ms → 30.2× faster
  • 100,000 chars: 3.7770ms vs 96.96ms → 25.7× faster

追加工作负载(appendWorkloadMs)—— markdown-it-ts vs micromark-based parse:

  • 5,000 chars: 0.2794ms vs 12.99ms → 46.5× faster
  • 20,000 chars: 1.1127ms vs 56.41ms → 50.7× faster
  • 100,000 chars: 5.4172ms vs 319.31ms → 58.9× faster

渲染性能(markdown → HTML)

除了纯解析,我们也持续跟踪 md.render(markdown) 这一整条 render API 调用的耗时,也就是“解析 + HTML 输出”的总成本,而不是单独比较低层 renderer 热路径。以下数据来自最近一次 pnpm run perf:generate

对于超大但有限的字符串,stock parser 实例可能使用内部大文本优化;插件或自定义 parser rule 的实例默认保留 full parse 语义。parseIterable / UnboundedBuffer 这类 API 只保留给“输入本来就是 chunk 流”的高级场景。

对比 markdown-it render API

  • 5,000 chars: 0.1793ms vs 0.2059ms → ~1.1× faster
  • 20,000 chars: 0.6677ms vs 0.7787ms → ~1.2× faster
  • 100,000 chars: 4.6034ms vs 5.3503ms → ~1.2× faster
  • 500,000 chars: 31.15ms vs 37.07ms → ~1.2× faster
  • 1,000,000 chars: 66.77ms vs 78.51ms → ~1.2× faster

对比 remark + rehype render API

  • 5,000 chars: 0.1793ms vs 4.2929ms → ~23.9× faster
  • 20,000 chars: 0.6677ms vs 24.47ms → ~36.6× faster
  • 100,000 chars: 4.6034ms vs 170.78ms → ~37.1× faster

对比 micromark(CommonMark 参考实现)

  • 5,000 chars: 0.1793ms vs 3.6437ms → ~20.3× faster
  • 20,000 chars: 0.6677ms vs 19.91ms → ~29.8× faster
  • 100,000 chars: 4.6034ms vs 107.22ms → ~23.3× 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.1448ms | 0.3136ms | | 20,000 | 0.5903ms | 0.8680ms | | 50,000 | 1.5677ms | 2.2452ms | | 100,000 | 3.7770ms | 5.5021ms | | 200,000 | 10.10ms | 12.17ms |

说明:markdown-it-ts 在较小文档上通过流式/分片策略获得显著 one-shot 优势;在非常大的文档(200k)上,各实现的绝对差距缩小。

与 markdown-exit 渲染器的对比

来自最近一次 perf 快照的 render API(parse + HTML 输出)汇总:

  • 5,000 chars: 0.1793ms vs 0.2487ms → ~1.4× faster
  • 20,000 chars: 0.6677ms vs 0.9981ms → ~1.5× faster
  • 50,000 chars: 1.8301ms vs 2.6056ms → ~1.4× faster
  • 100,000 chars: 4.6034ms vs 6.3543ms → ~1.4× faster
  • 200,000 chars: 11.28ms vs 14.93ms → ~1.3× faster

Parse / Render 对比排名(5k~200k)

为了更直观地查看四个实现(markdown-it-ts、markdown-it、markdown-exit、remark)在不同规模下的 parse / render 名次,下面直接基于最新 docs/perf-latest.json 的快照生成。 其中 parse 排名取 markdown-it-ts 在对应规模下 oneShotMs 最低的场景(S1~S5);render 排名则使用默认 MarkdownIt().render() 的端到端耗时,因此两张表不能直接理解为“同一条 parse + renderer 链路”的组合排名。

Parse 排名(one-shot 解析耗时,单位:ms)

| Size | Rank | Library | oneShotMs | |---:|---:|---|---:| | 5,000 | 1 | markdown-it-ts | 0.1448ms | | 5,000 | 2 | markdown-it | 0.2630ms | | 5,000 | 3 | markdown-exit | 0.3136ms | | 5,000 | 4 | remark | 5.0124ms | | 20,000 | 1 | markdown-it-ts | 0.5903ms | | 20,000 | 2 | markdown-it | 0.6548ms | | 20,000 | 3 | markdown-exit | 0.8680ms | | 20,000 | 4 | remark | 24.43ms | | 50,000 | 1 | markdown-it-ts | 1.5677ms | | 50,000 | 2 | markdown-it | 1.7021ms | | 50,000 | 3 | markdown-exit | 2.2452ms | | 50,000 | 4 | remark | 67.49ms | | 100,000 | 1 | markdown-it-ts | 3.7770ms | | 100,000 | 2 | markdown-it | 4.6536ms | | 100,000 | 3 | markdown-exit | 5.5021ms | | 100,000 | 4 | remark | 150.38ms | | 200,000 | 1 | markdown-it-ts | 10.10ms | | 200,000 | 2 | markdown-it | 10.27ms | | 200,000 | 3 | markdown-exit | 12.17ms | | 200,000 | 4 | remark | 349.73ms |

Render 排名(解析 + HTML 输出耗时,单位:ms)

| Size | Rank | Library | renderMs | |---:|---:|---|---:| | 5,000 | 1 | markdown-it-ts | 0.1793ms | | 5,000 | 2 | markdown-it | 0.2059ms | | 5,000 | 3 | markdown-exit | 0.2487ms | | 5,000 | 4 | remark + rehype | 4.2929ms | | 20,000 | 1 | markdown-it-ts | 0.6677ms | | 20,000 | 2 | markdown-it | 0.7787ms | | 20,000 | 3 | markdown-exit | 0.9981ms | | 20,000 | 4 | remark + rehype | 24.47ms | | 50,000 | 1 | markdown-it-ts | 1.8301ms | | 50,000 | 2 | markdown-it | 2.0047ms | | 50,000 | 3 | markdown-exit | 2.6056ms | | 50,000 | 4 | remark + rehype | 74.42ms | | 100,000 | 1 | markdown-it-ts | 4.6034ms | | 100,000 | 2 | markdown-it | 5.3503ms | | 100,000 | 3 | markdown-exit | 6.3543ms | | 100,000 | 4 | remark + rehype | 170.78ms | | 200,000 | 1 | markdown-it-ts | 11.28ms | | 200,000 | 2 | markdown-it | 13.16ms | | 200,000 | 3 | markdown-exit | 14.93ms | | 200,000 | 4 | remark + rehype | 377.11ms |

回归检查与对比

  • 使用最近一次的基线进行回归检查(同一采集方法/同一机器更稳):
    • pnpm run perf:check:latest
  • 运行按 token 类型拆分的 render 对比基准(对照 markdown-it):
    • pnpm run perf:render-rules
    • 若也想看零输出/低信号类别,可追加 --include-noise
    • 若想在有意义类别上做失败退出检查,可运行 pnpm run perf:render-rules:check
  • 查看详细差异(按“最差”排序,便于定位):
    • pnpm run perf:diff
  • 在人工确认后将最新结果设为新的基线:
    • pnpm run perf:accept

StreamBuffer(增量编辑建议)

当输入以“逐字符”方式到达时,直接调用 md.stream.parse 往往无法命中追加快路径(append fast-path)。 StreamBuffer 会聚合字符输入,只在安全的块级边界调用解析,从而保证正确性并提升命中率:

import markdownIt from 'markdown-it-ts'
import { StreamBuffer } from 'markdown-it-ts/stream/buffer'

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。