- 开发工具
- CLI
【免费下载链接】markdown-it
Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed
本篇文章围绕 markdown-it 仓库中的基准测试样本benchmark/samples/block-bq-flat.md,拆解它在压测体系中的定位、文本内容对应的 Markdown 语义、期望的 HTML 渲染结果,以及源码中 引用块(blockquote)解析规则 与惰性延续(lazy continuation)机制的底层实现。读完本文,你将理解基准样本的编写规范、如何用benchmark/benchmark.mjs单独跑一个样本,以及 blockquote 规则在StateBlock上的偏移量处理细节,并掌握为项目新增压测样本的完整流程。
样本在基准测试体系中的定位
benchmark/samples/目录下的每个.md文件都是一份"压测载荷",代表一类典型的 Markdown 输入。整个基准测试体系由三个部分构成:
benchmark/samples/:压测输入样本(本文主角block-bq-flat.md就在其中);benchmark/implementations/:被对比的实现(current、current-commonmark、commonmark-reference、markdown-it-2.2.1-commonmark、marked),每个目录的index.mjs都导出一个run(data)函数;benchmark/benchmark.mjs:调度脚本,负责读取样本、逐个构建tinybench基准并输出结果。
benchmark.mjs的核心逻辑(见 benchmark/benchmark.mjs)是:遍历samples目录下所有文件,把文件内容以fs.readFileSync(filepath, 'utf8')读入内存,并以字节数生成标题(如(246 bytes)),然后为每一个实现注册一个bench.add(impl.name, ...)任务,所有实现用同一份字符串作为输入执行impl.code.run(content.string)。因此,样本文件本身不包含任何配置或元信息——它纯粹是"喂给解析器的原文",这正是block-bq-flat.md全文只有 Markdown 文本的原因。
运行基准测试的方式在 docs/benchmark.md 中有记载:
# 先安装被对比实现的依赖(commonmark、marked、markdown-it 2.2.1) npm run benchmark-deps # 只跑匹配 "block-bq" 的样本(正则不区分大小写) node benchmark/benchmark.mjs block-bq # 不带参数则跑全部 28 个样本 node benchmark/benchmark.mjsbenchmark.mjs会把命令行参数process.argv.slice(2)映射为正则new RegExp(source, 'i'),与样本文件名(去扩展名后的部分,如block-bq-flat)做匹配(见 benchmark/benchmark.mjs)。匹配不到时输出There isn't any sample matches any of these patterns并提前退出。
逐行拆解样本内容与 Markdown 语义
block-bq-flat.md全文共 16 行、246 字节(wc -c可验证),完整内容如下:
> the simple example of a blockquote > the simple example of a blockquote > the simple example of a blockquote > the simple example of a blockquote ... continuation ... continuation ... continuation ... continuation empty blockquote: > > > >从 Markdown 语义上,这份样本被刻意分成两个区块:
第一部分(前 8 行)——一个带惰性延续的"扁平"引用块。前 4 行以>开头,构成一个引用块的主体;后 4 行... continuation没有>前缀,按 CommonMark 规范,它们作为前一个段落内容的"惰性延续行"(lazy continuation)被吸收进引用块内的段落,而不是终止引用块。这正是样本名中 "flat"(扁平)的含义:所有行属于同一个引用块,没有嵌套层级。值得一提的是,第一行> the simple example of a blockquote末尾带有一个多余空格(即行尾blockquote后有一个 space),这模拟了真实文档中常见的行尾空格场景。
第二部分(后 5 行)——四个空引用块。第 10 行empty blockquote:是一个独立的 ATX 标题;随后连续的 4 行>是空引用块。按 CommonMark 规则,连续的空>行不会合并成一个引用块,而是各自成为独立的空块引用。
对应解析结果(期望 HTML)
使用 markdown-it(默认 preset)渲染该样本,输出应为:
<blockquote> <p>the simple example of a blockquote the simple example of a blockquote the simple example of a blockquote the simple example of a blockquote ... continuation ... continuation ... continuation ... continuation</p> </blockquote> <h2>empty blockquote:</h2> <blockquote></blockquote> <blockquote></blockquote> <blockquote></blockquote> <blockquote></blockquote>各部分的语义对应关系:
| 样本内容 | 解析结果 | 说明 |
|---|---|---|
| 第 1–8 行 | 单个<blockquote>内嵌一个<p> | 后 4 行是惰性延续行,被折叠进同一段落 |
empty blockquote: | <h2>empty blockquote:</h2> | 这是一个 ATX 标题,而非普通文本 |
第 12–15 行的 4 个> | 4 个独立的<blockquote></blockquote> | 空引用块之间不会合并 |
空引用块的行为可以在仓库测试夹具 test/fixtures/markdown-it/commonmark_extras.txt 中看到直接佐证:输入> [foo]: bar\n[foo]输出<blockquote></blockquote>,即引用块内只剩链接定义时渲染为空标签。
源码级解析原理
blockquote 规则的三类终止条件
blockquote规则的实现位于 src/rules_block/blockquote.ts。它在扫描引用块末尾时定义了三种终止条件(源码注释中明确列出):
- 引用块外的空行:后续行不以
>开头且当前行为空(pos >= max)时终止; - 引用块内的空行后出现普通内容:即
>之后直接是空行、随后出现不以>开头的行(lastLineEmpty为真时终止); - 被其他块级标签中断:调用
terminatorRules(fence、hr、list、blockquote等规则,见 src/parser_block.ts 中的alt声明)以 silent 模式探测下一行是否会被其他规则吃掉。
由于样本第一部分所有行都满足"以>开头或作为段落延续"的条件,这三类终止条件都不会在前 8 行触发,因此整个块引用被完整保留。
惰性延续行如何进入引用块
样本中... continuation这样的行之所以能进入引用块,关键在"负缩进"标记。在 src/rules_block/blockquote.ts 中,扫描过程中会把非>开头的延续行的sCount临时置为-1:
// A negative indentation means that this is a paragraph continuation state.sCount[nextLine] = -1随后调用state.md.block.tokenize(state, startLine, nextLine)对内层内容做二次解析时,段落规则 会识别到这个负缩进并跳过它:
// quirk for blockquotes, this line should already be checked by that rule if (state.sCount[nextLine] < 0) { continue }于是段落可以继续吸收后续行,直到遇到空行或 EOF——这正是样本中 4 行... continuation被并入同一<p>的原理。整个 blockquote 规则结束后,src/rules_block/blockquote.ts 会遍历保存的oldBMarks/oldBSCount/oldSCount/oldTShift数组,把被临时改写的行偏移量恢复原状,保证外层解析状态不被污染。
StateBlock 上的偏移量缓存
为了快速跳行,src/rules_block/state_block.ts 为每行预计算了 5 个偏移数组,blockquote 规则依赖其中大部分:
| 数组 | 含义 | 在 blockquote 中的用途 |
|---|---|---|
bMarks | 每行起始偏移 | 定位>标记的起点(bMarks[line] + tShift[line]) |
eMarks | 每行结束偏移 | 判断行内是否还有内容(pos >= max) |
tShift | 行首非空白字符的偏移(tab 未展开) | 计算首个字符位置 |
sCount | 每行缩进(tab 已展开为 4 的倍数) | 判断是否缩进超过 3(转代码块)、识别惰性延续(负值) |
bsCount | 虚拟空格数(blockquote 覆写bMarks时丢失信息的补偿) | tab 展开时的余数计算 |
blockquote.ts中有一个对缩进的防御性判断:若state.sCount[startLine] - state.blkIndent >= 4,则行应被当作缩进代码块而不是引用块(src/rules_block/blockquote.ts),这是 CommonMark "引用块内缩进 4 空格变代码块" 规则的落地。测试夹具 test/fixtures/markdown-it/commonmark_extras.txt 中的用例> foo\n> bar渲染为<blockquote>内先<pre><code>foo</code></pre>再<p>bar</p>,正是这条分支的验证。
从样本到压测:benchmark.mjs 如何消费它
样本在基准中的完整生命周期(benchmark/benchmark.mjs):
- 读取
samples/block-bq-flat.md为字符串; - 用字节数生成样本标题
(246 bytes),便于直接对比"载荷大小 × 吞吐量"; - 为每个实现注册
tinybench任务(commonmark-reference、current、current-commonmark、markdown-it-2.2.1-commonmark、marked,按目录名排序); - 逐个
bench.run(),在cycle事件中打印ops/sec ±RME% (samples)格式的结果(benchmark/benchmark.mjs)。
各实现的run()差异决定了样本能测出什么:current(benchmark/implementations/current/index.mjs)以html/linkify/typographer全开的方式调用当前仓库源码src/index.ts;current-commonmark(benchmark/implementations/current-commonmark/index.mjs)则以commonmarkpreset 运行,并把normalizeLink替换为原始mdurl.encode、normalizeLinkText直接透传,以获得"更诚实"的对比;commonmark-reference与marked分别走benchmark/extra/node_modules中的commonmark@0.31.2与marked@18.0.4(依赖版本见 benchmark/extra/package.json)。
对单一实现做 profiling 则使用 benchmark/profile.mjs:它对test/fixtures/commonmark/spec.txt(110 KB 的 CommonMark 规范全量用例)循环渲染 20 次,便于用采样器分析热点。
扩展现有样本的实操指南
如果你要为新的语法形态新增压测载荷,遵循以下步骤即可复用整套基准体系:
- 新建样本文件:在
benchmark/samples/下创建文件,命名建议遵循"主题-形态"模式,例如列表嵌套样本是block-list-nested.md、引用块嵌套样本是block-bq-nested.md(对比block-bq-flat.md,后者用>>>>>>等多层>前缀压测嵌套解析,字节数更大)。flat与nested的成对设计,正是为了对照"同一语法在不同复杂度下的吞吐差异"。 - 验证语义正确性:先用 test/helpers.mjs 或
npm run test:markdown-it确认期望输出,必要时把用例写入test/fixtures/markdown-it/commonmark_extras.txt(该文件已收录大量 blockquote 回归用例,如 tab 处理、列表内嵌套、outdent 终止、多级>>>嵌套等,见 test/fixtures/markdown-it/commonmark_extras.txt)。 - 只跑新样本:
node benchmark/benchmark.mjs block-list-nested(文件名前缀即过滤正则)。 - 对照参考实现:如关心 CommonMark 合规性,可对 test/fixtures/commonmark/spec.txt 跑
test:cmspec(node --test test/cmspec/**/*.test.mjs)。
小结
block-bq-flat.md虽只有 246 字节,却同时覆盖了"带惰性延续的扁平引用块"与"连续空引用块"两类典型输入,是观察 blockquote 解析与基准测试消费方式的最小标本。其背后是 StateBlock 的按行偏移缓存、blockquote 规则 的三类终止条件与负缩进标记、段落规则 的惰性延续吸收,以及 benchmark.mjs 的统一调度。理解这条从样本到源码的链路,你既能快速编写新的压测载荷,也能在阅读 parser 源码时拥有具体的输入样例作参照。
进一步阅读:基准测试说明、profile 脚本、blockquote 实现、StateBlock 实现、blockquote 回归用例集。
- 开发工具
- CLI
【免费下载链接】markdown-it
Markdown parser, done right. 100% CommonMark support, extensions, syntax plugins & high speed
相关推荐
markdown-it 围栏式代码块(Fenced Code Block)解析与基准测试样本深度解析
markdown it 围栏式代码块(Fenced Code Block)解析与基准测试样本深度解析 导读 本文围绕 markdown it 仓库中 bench
开发工具CLImarkdown-it 内联链接解析实战与基准测试:基于 inline-links-flat.md 样例的深度剖析
markdown it 内联链接解析实战与基准测试:基于 inline links flat.md 样例的深度剖析 本篇文章以 markdown it 仓库内基
开发工具CLImarkdown-it HTML 块解析实战:从 block-html 基准样本到源码级原理
markdown it HTML 块解析实战:从 block html 基准样本到源码级原理 导读 :HTML 块(HTML Block)是 CommonMar
开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考