Quarkdown 粗体(Strong)解析全解:从 strong.md 测试夹具到 Strong AST 节点的完整管线

发布时间:2026/9/14 13:36:50
Quarkdown 粗体(Strong)解析全解:从 strong.md 测试夹具到 Strong AST 节点的完整管线 Quarkdown 粗体Strong解析全解从 strong.md 测试夹具到 Strong AST 节点的完整管线【免费下载链接】quarkdown Markdown with superpowers: from ideas to papers, presentations, websites, books, and knowledge bases.项目地址: https://gitcode.com/GitHub_Trending/qu/quarkdown本篇以 strong.md 测试夹具为核心拆解 Quarkdown 核心解析器对 Markdown 粗体strong emphasis的完整处理链路三行测试输入分别对应什么 AST 结构、正则词法模式如何落实 CommonMark 的 flanking左/右邻接规则、解析器如何递归重词法化以实现嵌套强调以及测试中记录的一处已知解析边界。读完后可掌握 Quarkdown 从源码文本到Strong/Emphasis/StrongEmphasis节点的全部关键实现与验证方式。1. strong.md 夹具三行输入定义的全部测试语义strong.md 位于quarkdown-core的测试资源目录src/test/resources/parsing/inline/它不是面向用户的文档而是行内inline解析测试的输入文件全文仅 5 行包含三条测试用例**foo** **foo*bar*baz** __foo_bar_baz__这三行覆盖了粗体解析的三种关键场景其在 InlineParserTest 中strong()测试方法里对应的期望结构如下输入期望解析结果**foo**一个Strong节点唯一子节点为Text(foo)**foo*bar*baz**一个Strong节点子节点依次为Text(foo)、Emphasis(Text(bar))、Text(baz)—— 星号粗体内部可嵌套斜体__foo_bar_baz__一个Strong节点唯一子节点为Text(foo_bar_baz)—— 下划线粗体内部的_不产生嵌套强调第二条与第三条的对比是整组用例最核心的信息同为内部夹带单个分隔符星号版本产生嵌套的Emphasis节点下划线版本则保持纯文本。这一差异并非偶然而直接由词法层为星号与下划线编写的两套不同严格程度的正则模式决定第 4 节展开。测试结尾的assertFalse(nodes.hasNext())则保证没有多余的Strong节点被误产生。2. 测试读取机制inlineIterator 与 flavorstrong()测试的入口只有一行val nodes inlineIteratorStrong(readSource(/parsing/inline/strong.md))其中readSource按测试资源路径读取文件文本/parsing/inline/strong.md正对应src/test/resources下的 strong.md。inlineIterator是 InlineParserTest 的私有泛型辅助方法其实现揭示了 Quarkdown 解析管线的通用入口形态private inline fun reified T : Node inlineIterator( source: CharSequence, assertType: Boolean true, flavor: MarkdownFlavor QuarkdownFlavor, ): IteratorT { val lexer flavor.lexerFactory.newInlineLexer(source) val parser flavor.parserFactory.newParser(MutableContext(flavor)) return nodesIterator(lexer, parser, assertType) }从源码结构看这里体现了三个设计点flavor 机制词法器lexer与解析器parser都通过MarkdownFlavor的工厂创建默认使用QuarkdownFlavor模式文件的 KDoc 也注明这些正则服务于BaseMarkdownFlavor的行内 token意味着不同 Markdown 方言可以替换模式与解析策略词法先行newInlineLexer(source)先把原始文本切分为 token 流再由解析器把 token 组装成 AST 节点词法与语法两层解耦assertType默认开启迭代出的每个节点都会被断言为泛型类型T此处即Strong因此该测试隐含了“源文件中恰好产出 3 个Strong节点、且类型精确”的校验。3. AST 节点层Strong、Emphasis 与 StrongEmphasis解析目标节点定义在 Emphasis.kt 中四个强调类节点并列存在/** Weakly emphasized content. */ class Emphasis( Diverge override val text: InlineContent, ) : TextNode /** Strongly emphasized content. */ class Strong( Diverge override val text: InlineContent, ) : TextNode /** Heavily emphasized content. */ class StrongEmphasis( Diverge override val text: InlineContent, ) : TextNode /** Strikethrough content. */ class Strikethrough( Diverge override val text: InlineContent, ) : TextNode要点三类强调强度独立建类Emphasis弱强调*foo*/_foo_、Strong强强调**foo**/__foo__、StrongEmphasis双重强调***foo***/___foo___外加 GFM 风格的Strikethrough~~foo~~每个节点的text字段类型是InlineContent即可继续嵌套行内节点的内容列表这正是**foo*bar*baz**能表达出Strong内嵌Emphasis的数据基础四个类均继承TextNode并实现accept(visitor: NodeVisitorT)采用访问者模式使渲染器、重写器rewriter等下游阶段可以统一遍历而不依赖节点具体类型。4. 词法层token 类型与落实 flanking 规则的正则模式4.1 强调相关 tokenInlineTokens.kt 在文件下半部的 “Emphasis” 分组中定义了这些 token 的包裹类型每个 token 仅携带TokenData含文本与正则分组并实现accept(TokenVisitor)StrongToken**strong**与__strong__两种写法EmphasisToken*emphasis*与_emphasis_StrongEmphasisToken***emphasis***与___emphasis___。4.2 模式注册星号宽松、下划线严格模式集中定义在 BaseMarkdownInlineTokenRegexPatterns.kt文件内注释标明这些模式遵循 CommonMark 规范的 “emphasis and strong emphasis” 一节。与强调相关的注册项为模式属性起始分隔符结束分隔符strict对应 tokenstrongAsterisk\*{2}两个星号\*{2,}两个及以上星号falseStrongTokenstrongUnderscore_{2}_{2,}trueStrongTokenemphasisAsterisk\*\*falseEmphasisTokenemphasisUnderscore__trueEmphasisTokenstrongEmphasisAsterisk\*{3}\*{3,}falseStrongEmphasisTokenstrongEmphasisUnderscore_{3}_{3,}trueStrongEmphasisToken注意结束分隔符允许“两个及以上”**的闭合端可以匹配更长的星号串这是处理相邻强调如粗体内嵌斜体时正则能够正确切分的基础。4.3 delimiteredPatternCommonMark flanking 规则的正则化所有强调模式都由同一个私有函数 delimitedPattern 生成其 KDoc 对strict参数的语义给出了权威说明non-strict means the start delimiter must be left-flanking and end delimiter must be right-flanking; strict means any of the delimiters must not be left and right-flanking at the same time.生成的正则骨架为(?!start) [起始分隔符按 strict 与否采用不同的 flanking 约束] (?!start)((.|\R)?) // 内容非贪婪 [结束分隔符按 strict 与否采用不同的 flanking 约束]其中punct引用为\p{IsP}\p{IsS}Unicode 标点与符号字符类即“空白 标点/符号”共同构成分隔符两侧的判定上下文。翻译回 CommonMark 术语星号模式strict false起始分隔符只需“左邻接”后随非空白且要么非标点要么左侧为行首/空白/标点结束分隔符只需“右邻接”。星号可以出现在单词内部参与强调如foo*bar*下划线模式strict true分隔符不得同时左、右邻接即禁止“词内”下划线强调如foo_bar_baz中的单个_不构成分隔符。这与 CommonMark 对_的限制一致。夹具中第二条与第三条的分野*bar*生效、_bar_不生效正是这两套模式在同一解析流程下的直接体现。5. 解析器层emphasisContent 的递归重词法化token 到 AST 节点的组装发生在 InlineTokenParser。三个强调及删除线节点共用同一个内容提取逻辑private fun emphasisContent(token: Token): InlineContent { // The raw string content, without the delimiters. val text token.data.groups .iterator(consumeAmount 3) .next() return parseSubContent(text) } override fun visit(token: EmphasisToken): Node Emphasis(emphasisContent(token)) override fun visit(token: StrongToken): Node Strong(emphasisContent(token)) override fun visit(token: StrongEmphasisToken): Node StrongEmphasis(emphasisContent(token))这里有两个关键实现细节取“不含定界符的内容分组”consumeAmount 3对应delimitedPattern生成的三个捕获组起始分隔符、内容、结束分隔符迭代器跳过后取到的第一组即内容主体。对**foo*bar*baz**取出的是foo*bar*baz递归词法 解析parseSubContent会调用context.flavor.lexerFactory.newInlineLexer(source)对这段内容重新走一遍完整的行内词法与解析见 tokenizeAndParse。因此内容中的*bar*会再次命中emphasisAsterisk模式被解析为Emphasis节点嵌入Strong的子节点列表——嵌套强调结构完全由“内容递归解析”这一机制自然产生解析器没有为嵌套编写专门分支。6. 逐行解读夹具的三条输入第 1 行**foo**strongAsterisk模式匹配整行内容分组为foo递归解析只产生一个Text节点。测试断言children.first()是Text且text foo。第 2 行**foo*bar*baz**外层由strongAsterisk命中内容分组foo*bar*baz进入递归词法。其中的*bar*起始*前为字母o、后为字母b满足左邻接结束*前为字母r、后为字母a满足右邻接星号模式为 non-strict允许词内强调故匹配为Emphasis。最终子节点序列为Text(foo)→Emphasis(Text(bar))→Text(baz)与 strong() 测试 的逐项断言完全一致。第 3 行__foo_bar_baz__外层由strongUnderscore命中行首__与行尾__均满足 strict 模式的 flanking 约束。内容foo_bar_baz递归词法时单个_bar_的起始_左邻接且右邻接同时成立违反 strict 模式“不得同时左、右邻接”的约束因此不会被识别为Emphasis分隔符整体退化为纯文本。测试断言第三个Strong节点的子节点只有Text(foo_bar_baz)一个验证了词内下划线不产生强调。7. 已知边界被注释掉的**foo*bar***用例strong() 测试 的尾部保留了一段被注释的期望值并标注了明确的待办/* TODO fix for **foo*bar*** ... */这记录了当前实现的一个解析边界**foo*bar***这类定界符不平衡的混合用例粗体起始两个星号、末尾三个星号需要把***切分为“闭合斜体的* 闭合粗体的**”目前尚未按期望处理期望结构Strong内含Text(foo)、Emphasis(bar)等被整体注释待修复。撰写或审查粗体解析相关代码时应将此视为当前仓库的已知限制而非已支持能力。8. 相邻夹具strongemphasis.md 与 emphasis.md同一资源目录下还有与本文主题强相关的两个夹具构成完整的强调解析测试族strongemphasis.md内容为***foo***与___foo*bar*baz___对应 strongEmphasis() 测试验证StrongEmphasisToken三个及以上星号/下划线路径且下划线双重强调内部同样允许嵌套Emphasisemphasis.md覆盖斜体及其与粗体的互嵌、括号边界等更多场景对应 emphasis() 测试。三者共用同一套 token、模式与解析器实现仅由起始/结束分隔符的星号或下划线数量与 strict 标志区分这也解释了为何Strong的结束正则写作\*{2,}——它必须能与***、****等更长分隔符共存。9. 复现方式运行 InlineParserTest仓库根目录提供了 Gradle wrapper可在仓库根目录执行以下命令运行整个行内解析测试类其中包含 strong 用例./gradlew :quarkdown-core:test --tests com.quarkdown.core.InlineParserTest若需单独验证 strong 夹具的解析行为可将测试类限定后观察strong()方法的断言输出输入与期望一一对应 strong.md 与 InlineParserTest修改任一端的夹具或断言都可用于快速回归词法/解析改动。小结strong.md虽只有三行输入但它锚定了 Quarkdown 粗体解析的完整证据链词法层用 BaseMarkdownInlineTokenRegexPatterns 中的delimitedPattern把 CommonMark 的 flanking 规则编译进正则星号宽松、下划线严格InlineTokenParser 通过emphasisContent对分隔符内容递归重词法化生成嵌套结构Emphasis.kt 中独立的Strong/Emphasis/StrongEmphasis节点承载三级强调强度最终由访问者模式交给渲染阶段。结合**foo*bar***的 TODO 注释这份夹具与测试共同勾勒出当前实现已支持的能力与尚待补齐的边界。【免费下载链接】quarkdown Markdown with superpowers: from ideas to papers, presentations, websites, books, and knowledge bases.项目地址: https://gitcode.com/GitHub_Trending/qu/quarkdown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考