
前端【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址https://gitcode.com/gh_mirrors/ma/marked点击查看免费下载导读本文以 marked 开源仓库中的 tricky_list 测试用例 为切入点剖析 Markdown 解析中一个极易出错的问题——紧邻段落与无序列表的边界判定。通过阅读本文你将掌握 marked 在强调文本段落 空行 无序列表这一典型场景下的真实解析结果、其底层块级语法规则与列表分词实现以及如何在项目里复现、验证并规避此类边界歧义。一、从测试用例说起tricky_list 到底在验证什么tricky_list.md 位于test/specs/new/目录下是 marked 自带的新增行为new behavior测试规格之一。该目录下的每个.md文件都对应一个同名的.html文件二者构成一组输入 → 期望输出的断言。tricky_list 的输入 Markdown 全文如下**hello** _world_ * hello world **hello** _world_ * hello world **hello** _world_ * Hello world **hello** _world_ * hello world而 tricky_list.html 给出的期望输出为pstronghello/strong emworld/em/p ul lihello world/li /ul pstronghello/strong emworld/em/p ul lihello world/li /ul pstronghello/strong emworld/em/p ul liHello world/li /ul pstronghello/strong emworld/em/p ul lihello world/li /ul这份测试的核心意图非常明确当一行内容为**hello** _world_的段落之后紧跟一个空行再出现一行以*开头的列表项时marked 必须把前者解析为独立的p段落、把后者解析为独立的ul列表而不是把它们合并成一个段落也不得把强调语法与列表混在一起。值得注意的是四个列表项中第三个是* Hello world首字母大写其余是* hello world首字母小写期望输出分别保留了Hello与hello的原始大小写——这说明列表项内容会被原样继承不受前面段落中强调标记的影响。二、为什么这个用例tricky段落与列表的边界是歧义高发区Markdown 解析中最难处理的规则之一就是哪些块级元素可以中断一个段落。在 CommonMark 规范中一个段落由连续的文本行组成而列表项、标题、围栏代码等结构在某些条件下可以打断段落。tricky_list 之所以棘手是因为它同时踩中了两个敏感点空行blank line的存在每个**hello** _world_段落与后面的* hello world之间都隔着一个空行。空行是块级元素的分隔符它明确地把前面的内容封闭成一个段落。强调emphasis语法紧邻段落末尾段落内容**hello** _world_本身就是行内强调语法如果解析器对段落能否被列表中断的判定过于宽松或过于严格就容易出现错误合并。如果我们把空行去掉、让列表紧跟段落行为就会完全不同——这一点从 marked 的块级规则中可以得到印证。三、源码级解析marked 如何判定列表边界3.1 块级列表正则list与bullet列表的块级语法在 src/rules.ts 中定义const bullet / {0,3}(?:[*-]|\d{1,9}[.)])/; const list edit(/^(bull)([ \t][^\n]*?)?(?:\n|$)/) .replace(/bull/g, bullet) .getRegex();从源码结构可以推断列表标记bullet允许最多 3 个前导空格{0,3}支持无序列表的*、、-以及有序列表的1.、1)等形式\d{1,9}[.)]列表项首行([ \t][^\n]*?)?要求标记与内容之间至少有一个空格或制表符随后捕获到换行前的全部内容list正则只匹配列表的起始标记行真正的多行列表项扫描发生在 Tokenizer 中。3.2 段落中断规则只有非空列表才能打断段落在 src/rules.ts 中定义了顶层层面的段落正则// only non-empty lists starting from 1 can interrupt paragraphs const paragraph createParagraph(/ {0,3}(?:[*-]|1[.)])[ \t][^ \t\n]/);这一行注释与正则揭示了 CommonMark 的关键规则一个无序列表*//-或从 1 开始的有序列表1./1)只有在其内容非空[ \t][^ \t\n]表示标记后必须跟有非空白字符时才能中断一个进行中的段落。反之空列表项不能中断段落会作为段落的延续文本处理。这正是 tricky_list 中段落与列表能正确分离的规则基础由于每个* hello world列表项都包含非空内容它们具备中断段落的资格而空行又先一步终结了段落因此两者被解析为相互独立的块。3.3 行内列表项识别listItemRegex与nextBulletRegex当块级正则命中后Tokenizer 会循环收集同一列表内的所有列表项。核心匹配逻辑见 src/rules.tslistItemRegex: (bull: string) new RegExp(^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))), nextBulletRegex: cachedIndentRegex((indent: number) new RegExp(^ {0,${indent}}(?:[*-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$)))),listItemRegex用于匹配当前列表的第一个列表项bull是第一次匹配时确定的标记类型如\*nextBulletRegex用于判断后续行是否为同一缩进级别的兄弟列表项它会依据当前列表项的缩进深度动态生成正则二者都依赖cachedIndentRegexsrc/rules.ts做正则缓存把缩进值映射到 0~3 的缓存槽位避免每次扫描重复构造正则这是 marked 追求解析速度的体现。在 tricky_list 中每个* hello world是孤立的单行列表前后均有空行因此listItemRegex命中第一项后nextBulletRegex立即发现下一行不是列表项列表随即结束。3.4 Tokenizer 主循环完整的分组流程列表分词的主体实现在 src/Tokenizer.ts 的list(src)方法中其关键流程为用this.rules.block.list.exec(src)尝试匹配列表起始标记行src/Tokenizer.ts根据捕获的标记判断ordered与start有序列表的起始序号并在 src/Tokens.ts 定义的Tokens.List结构中初始化loose: false与空items进入while (src)循环用itemRegex反复抓取每个列表项在循环内会依次用hr、fencesBeginRegex、headingBeginRegex、htmlBeginRegex、blockquoteBeginRegex、nextBulletRegex检查后续行一旦遇到新的块级结构就终止当前列表项src/Tokenizer.ts计算每个列表项内容的缩进indent 4时视为 1即超过 4 个空格的缩进代码块按 1 处理并以此决定后续行归入哪个列表项src/Tokenizer.ts最后把list.items交给this.lexer.blockTokens(item.text, [])递归分词并在 src/Tokens.ts 定义的Tokens.ListItem中填充tokenssrc/Tokenizer.ts。正是第 3 步的遇到新块级结构即终止机制保证了 tricky_list 中每个* hello world不会吞掉后续的**hello** _world_段落——因为下一行**hello** _world_不满足任何列表项延续条件。3.5 渲染端ul/li的生成分词完成后渲染由 src/Parser.ts 的case list分发到 src/Renderer.tslist(token: Tokens.List): RendererOutput { const ordered token.ordered; const start token.start; ... const type ordered ? ol : ul; const startAttr (ordered start ! 1) ? ( start start ) : ; return type startAttr \n body / type \n as RendererOutput; }这里可以验证 tricky_list 的输出特征由于是*无序列表ordered为false因此渲染为ul而listitem方法为每个列表项输出li${this.parser.parse(item.tokens)}/li列表项内嵌的文本hello world不含任何行内标记原样输出——与期望 HTML 完全吻合。四、在本地仓库中复现与验证4.1 构建 marked当前仓库使用 esbuild 进行构建产物输出到lib/目录构建配置见 esbuild.config.js。可以先执行构建npm install npm run build4.2 手动验证 tricky_list 的解析结果规格测试驱动位于 test/run-spec-tests.js它通过markedjs/testutils的getTests加载五类规格CommonMark、GFM、new、original、redos再用Marked实例逐一解析比对。其中new目录的测试直接以默认选项运行test/run-spec-tests.js。要单独验证 tricky_list 的解析行为可以用Marked类直接解析输入import { Marked } from ./lib/marked.esm.js; const marked new Marked(); const md **hello** _world_\n\n* hello world\n\n**hello** _world_\n\n* Hello world; console.log(marked.parse(md));预期输出为pstronghello/strong emworld/em/p ul lihello world/li /ul pstronghello/strong emworld/em/p ul liHello world/li /ul4.3 运行完整规格测试若希望把整个new目录的规格都跑一遍可运行npm test或直接运行 test/run-spec-tests.jsnode test/run-spec-tests.js测试框架会逐一执行 CommonMark、GFM、new、original、redos 五类断言若任何.md的解析输出与同名.html不一致即判定失败。tricky_list 通过即代表本节描述的段落/列表边界行为得到回归保障。五、边界对照紧邻与分隔的行为差异为加深理解这里补充一组基于同一测试主题的对照实验可在本地按上文方式验证实验 A列表紧邻段落无空行**hello** _world_ * hello world由于没有空行终结段落此时* hello world是否能中断段落取决于 3.2 节讨论的段落中断规则——非空列表可以中断段落但解析结果会与 tricky_list 存在差异。实验 B空列表项紧邻段落paragraph text **后没有非空白内容属于空列表项不能中断段落*会作为段落文本的一部分被继续吸收。实验 C带空行的独立列表即 tricky_list 场景**hello** _world_ * hello world空行先终结段落* hello world作为全新的块级结构被解析为独立列表段落中的**hello**与_world_只会被解析为行内强调不会泄漏到列表中去。这三个实验从正反两面印证了 tricky_list 测试所锁定的行为契约。六、工程启示与最佳实践测试规格即文档test/specs/new/目录下每个成对的.md/.html文件就是 marked 团队针对边界行为的活文档。阅读仓库时遇到解析行为疑问优先在 test/specs/new 中检索同名用例比直接读源码更直观。段落中断规则是理解 Markdown 解析的钥匙无论是自己阅读解析器源码还是排查渲染差异都应先确认空行是否存在列表项是否非空列表起始序号是否为 1这三个维度它们共同决定段落与列表的边界。善用Marked类而非全局marked如 test/run-spec-tests.js 所示通过new Marked(options)可以隔离配置、避免全局状态污染适合做行为对比实验。关注缩进细节从 src/Tokenizer.ts 可以看出列表项的缩进计算包括超过 4 空格的代码块按 1 处理直接决定嵌套层级写作 Markdown 时保持统一缩进能有效规避列表意外中断类问题。七、总结tricky_list 测试以强调段落 空行 无序列表的重复结构为 marked 锁定了段落与列表之间清晰稳定的边界行为。透过这个用例我们不仅验证了p与ul的正确分离还顺藤摸瓜读懂了 src/rules.ts 中的块级列表正则、段落中断规则src/Tokenizer.ts 中的列表项扫描与缩进计算以及 src/Renderer.ts 中的ul/li渲染实现。这套测试驱动理解源码的方法同样适用于继续研读 test/specs/new 目录下的其余用例例如 list_loose、list_item_empty、list_wrong_indent 等列表边界系列测试。赞分享前端【免费下载链接】markedA markdown parser and compiler. Built for speed.项目地址https://gitcode.com/gh_mirrors/ma/marked点击查看免费下载相关推荐深入解析 Marked 对无空格列表标记的处理列表项与段落续行的边界判定深入解析 Marked 对无空格列表标记的处理列表项与段落续行的边界判定 在 Markdown 语法中 、 、 与有序列表数字如 1. 被用作列前端marked 的 GFM 表格解析从 space_after_table 测试用例看表格与段落的边界处理marked 的 GFM 表格解析从 space_after_table 测试用例看表格与段落的边界处理 本篇技术指南以 marked 仓库中的 test/s前端marked 的 Markdown 解析边界从 docs/broken.md 看引擎差异与列表/引用块实现原理marked 的 Markdown 解析边界从 docs/broken.md 看引擎差异与列表/引用块实现原理 本文以 docs/broken.md http前端上一篇微信防撤回工具WeChatIntercept告别错过重要信息的烦恼下一篇终极指南3分钟掌握macOS微信防撤回神器WeChatIntercept创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考