逐行解剖 Quarkdown 行内词法分析:17 行 inline.md 测试语料与行内分词器实现
【免费下载链接】quarkdown🪐 Markdown with superpowers: from ideas to papers, presentations, websites, books, and knowledge bases.项目地址: https://gitcode.com/GitHub_Trending/qu/quarkdown
quarkdown-core/src/test/resources/lexing/inline.md是 Quarkdown 词法分析(lexing)阶段的核心回归语料:17 行、50 个期望词法单元(token),把转义、代码片段、换行、强调、链接、自动链接、引用式链接、图片、注释、行内数学与行内函数调用等全部行内语法压缩在一张“试卷”里。本文以这份语料为骨架,逐行拆解每一行考察的语法特性,给出 LexerTest 中逐一对应的期望 token 序列,并结合 行内 token 正则模式源码 解释每种写法为何能被(或不能被)识别,读完可以完整掌握 Quarkdown 行内分词器的工作原理与边界条件。
1. inline.md 的角色:行内词法分析的回归语料
Quarkdown 的文档处理流水线分为词法分析与语法分析等阶段(见 流水线概览)。词法阶段由quarkdown-core中的Lexer完成,其中“行内(inline)词法”负责处理段落内部的所有语法:强调、链接、代码片段、数学、函数调用等。
inline.md本身不是一个用户文档,而是一份测试夹具(test fixture),与同目录下的 blocks.md、escape.md、emphasis.md、comment.md、entity.md、inlinefunction.md、linebreak.md、textreplacement.md 一起,构成LexerTest的回归语料集。每份文件对应一个测试方法,每次修改词法正则都必须通过它们,防止行内语法识别出现回归。
语料全文如下(这是本文所有分析的基础,17 行缺一不可):
Text \! text `code` ``code`` ``not code` title newline not newline **bold** *italic* ***bold & italic*** text <https://google.com>https://google.com [label](https://google.com) [label](https://google.com 'url') [label][reference] [reference][] [reference]  ![img] <!-- comm ent --> a **b c** [a*]* text $ math $ text .function {arg1} {arg2} x2. 语料如何被消费:LexerTest 的 inline() 方法
消费这份语料的测试位于 LexerTest.kt 的inline()方法。它通过一个统一的辅助函数(第 65–70 行)完成词法分析:
private fun inlineLex(source: CharSequence) = QuarkdownFlavor.lexerFactory .newInlineLexer(source.trim()) .tokenize() .filter { it !is NewlineToken } .iterator()三个关键细节:
- 使用
QuarkdownFlavor:这是 Quarkdown 完整方言,其行内正则模式由 QuarkdownInlineTokenRegexPatterns 提供,在 BaseMarkdownInlineTokenRegexPatterns 之上扩展了行内数学与行内函数调用。同文件flavors()测试证实:换用BaseMarkdownFlavor时数学与函数调用等 Quarkdown 特性不会被识别。 source.trim():语料首尾空白被裁剪,保证 token 序列从第一个字符开始。- 过滤
NewlineToken、保留LineBreakToken:前者是块级段落边界产物,后者是行内层对换行的识别结果,测试只关心后者。
随后inline()用 50 条assertIs<...>断言 token 类型序列,最后assertFalse(tokens.hasNext())确认没有多余 token。下面是语料与期望 token 的完整对照(行号指inline.md的行):
| 行 | 语料内容 | 产生的 token 序列(按出现顺序) |
|---|---|---|
| 1 | Text \! text \code` ``code`` ``not code` title` | PlainText,Escape, PlainText,CodeSpan, PlainText,CodeSpan, PlainText,Link |
| 2 | newline | LineBreak, PlainText, LineBreak |
| 3 | not newline | PlainText, LineBreak |
| 4 | **bold** | Strong, LineBreak |
| 5 | *italic* | Emphasis, LineBreak |
| 6 | ***bold & italic*** | StrongEmphasis, LineBreak |
| 7 | text <https://google.com>https://google.com | PlainText,DiamondAutolink,UrlAutolink, LineBreak |
| 8 | label label | Link, PlainText,Link, LineBreak |
| 9 | [label][reference] | ReferenceLink, LineBreak |
| 10 | [reference][] [reference] | ReferenceLink, PlainText,ReferenceLink, LineBreak |
| 11 |  ![img] | Image, PlainText,ReferenceImage, LineBreak |
| 12–13 | <!-- comm/ent --> | Comment, LineBreak |
| 14–15 | a **b/c** | PlainText,Strong(跨两行), LineBreak |
| 16 | [a*]* | ReferenceLink, PlainText(尾部裸*), LineBreak |
| 17 | text $ math $ text .function {arg1} {arg2} x | PlainText,InlineMath, PlainText,FunctionCall, PlainText |
加粗的正是被测的核心 token 类型,它们的定义集中在 InlineTokens.kt。下面按语料顺序,结合正则源码逐点解释这些识别行为背后的实现依据。
3. 转义与代码片段(第 1 行)
3.1\!为什么是 EscapeToken
转义模式的定义见 BaseMarkdownInlineTokenRegexPatterns.kt 第 31–38 行:
regex = "\\\\([!\"#$%&'()*+,\\-./:;<=>?@\\[\\]\\\\^_`{|}~])"即“反斜杠 + 一个 CommonMark 标点字符”。!在字符类中,所以\!匹配为EscapeToken;而字母不在集合内,\m不是转义(这一点由姊妹语料 escape.md 的断言显式验证:' text \m '不产生 EscapeToken)。
3.2 双反引号跨度与 ````not code` `` 的反例
代码片段模式(第 70–77 行):
regex = "(?<!`)(?<codebegin>`+)([^`]|[^`][\\s\\S]*?[^`])\\k<codebegin>(?!`)"三个要点:
- 反引号长度必须相同:
(?<codebegin>+)捕获起始反引号串,闭合端用反向引用\k强制等长。因此 ``code`` 与 ```` ``code`` ```` 各自独立成CodeSpanToken`。 - 内容必须不以反引号开头或结尾:内容分组是
([^]|[^][\s\S]*?[^])——单字符或“非反引号开头、非反引号结尾”的多字符串。语料中的 ```` ``not code```` 内容若是not code,它以反引号结尾,两个分支都不满足,整个模式失配,于是这段双反引号连同内部内容整体落入PlainTextToken。这正是第 1 行期望序列中第三个CodeSpan缺席、多出一个 PlainText 的原因。 - 前后不能紧贴额外反引号:
(?<!)与(?!)防止把更长的反引号串的一部分误当作跨度边界。
3.3title:带“相对地址”的内联链接
第 1 行末尾的title没有 scheme,label作为普通 href 被接受,产生LinkToken。链接模式(第 99–110 行)结构为:
\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)其中 label 用LABEL_HELPER,href 用尖括号包裹或PLAIN_HREF_HELPER。注意 第 335–337 行的注释 明确写明:按 CommonMark 6.4,普通链接目的地中的括号要么反斜杠转义、要么必须平衡,PLAIN_HREF_HELPER用嵌套正则实现了这一条。第 8 行第二个链接[label](https://google.com 'url')验证了可选的 title 分支。
4. 换行(第 2–3 行)
换行模式(第 85–92 行):
regex = "(?:( {2,}|\\\\))?\\R(?!\\s*$)"语义(与 KDoc 一致):段落内、非末尾的换行产生LineBreakToken;前面跟两个及以上空格或反斜杠时为硬换行(hard break),否则为软换行(soft break),区别记录在 token 的数据上而非类型上。语料中每行末尾的换行都产生一个LineBreakToken,这也是为什么测试序列里 LineBreak 穿插在所有行之间——inlineLex过滤的只是块级NewlineToken,行内LineBreakToken全部保留参与断言。
5. 强调家族(第 4–6 行与第 14–15 行)
**bold**→StrongToken、*italic*→EmphasisToken、***bold & italic***→StrongEmphasisToken,分别对应强(双星号)、斜体(单星号)、强调(三星号)三个模式(第 239–309 行)。它们的正则由私有函数delimiteredPattern(第 352–377 行)统一生成,并内嵌了 CommonMark 的 flanking(左/右倾向性)规则:
- 非严格模式(星号族):起始定界符须左倾向、结束定界符须右倾向;
- 严格模式(下划线族):定界符不得同时左右倾向。
delimiteredPattern的注释直接引用了 CommonMark 0.31.2 的强调规则(第 237 行)。
两个值得注意的边界行为都在语料里被覆盖:
- 强调可跨行:第 14–15 行的
a **b+c**期望是PlainText, Strong, LineBreak。内容分组写的是((.|\R)+?)(显式允许换行),因此强强调可以跨越软换行闭合——跨行的**依然被识别为StrongToken,中间换行被包在 token 内部。 - 裸星号不触发强调:第 16 行
[a*]*的期望是ReferenceLink, PlainText,尾部孤立*无法成对定界,落入普通文本(见下一节)。
6. 自动链接(第 7 行)
text <https://google.com>https://google.com一行里同时考察两种自动链接:
- 钻石自动链接
DiamondAutolinkToken(第 116–129 行):<scheme:...>或<email>,其中 scheme 限定为[a-zA-Z][a-zA-Z0-9+.-]{1,31}。<https://google.com>命中。 - 裸 URL 自动链接
UrlAutolinkToken(第 135–147 行):(?:ftp|https?)://或www\.开头,外加邮箱地址备选分支。紧跟尖括号之后的裸https://google.com命中。
语料故意把两者无空格相邻书写,验证词法器能切出两个独立 token 而不互相吞并——测试序列中DiamondAutolink与UrlAutolink前后相继即为证据。
7. 引用式链接与[a*]*的陷阱(第 9–10、16 行)
引用式链接模式(第 154–164 行):
regex = RegexBuilder("\\(label)\\?\\])?") .withReference("label", LABEL_HELPER) .withReference("ref", BLOCK_LABEL_HELPER)它同时覆盖三种写法,语料逐行验证:
[label][reference](第 9 行):标签 + 引用标签;[reference][](第 10 行):短形式,方括号留空;[reference](第 10 行):标签自身即引用标签。
label 匹配规则LABEL_HELPER(第 327 行)允许方括号内出现除[ ] \ 反引号以外的任意字符(含、反斜杠转义序列与行内代码)。第 16 行[a*]正是利用这一点:在方括号内被LABEL_HELPER当作标签内容吃掉,[a*]整体成为ReferenceLinkToken;闭合括号后剩下的裸无法定界强调,只能落为PlainTextToken`。这个用例守护的是“链接标签内的星号不得触发强调解析”这一类词法优先级问题。
8. 图片与引用式图片(第 11 行)
与![img]分别产生ImageToken与ReferenceImageToken。两个模式(第 190–223 行)结构相同:!(?:\(imgsize\))?+ 内联/引用链接体。值得注意的是 Quarkdown 在此做了扩展:!之后可以跟一个可选的尺寸段(WxH)(W/H 为整数或_表示自适应,分隔符支持*、空格、x等,见 第 340 行 的IMAGE_SIZE_DIVIDER_HELPER),语料未覆盖该分支,但模式本身已就位。
9. 注释(第 12–13 行)
<!-- comm+ent -->跨两行书写,期望只产生一个CommentToken。注释模式使用PatternHelpers.COMMENT(第 229–235 行),KDoc 说明其可忽略<!-- ... -->包裹的内容,且横杠数量可变。语料让注释内部含换行、前后各有文本,验证注释在行内层被整体摘除后,其余文本正常参与分词。姊妹语料 comment.md 则覆盖更多正/反例组合。
10. Quarkdown 扩展:行内数学与行内函数调用(第 17 行)
第 17 行是整份语料的收尾重头戏,一行之内考察两个纯 Quarkdown 特性:
10.1$ math $→ InlineMathToken
行内数学模式定义在 QuarkdownInlineTokenRegexPatterns.kt 第 22–31 行,KDoc 明确其语义为“同一行内、两侧带空格的美元符号之间的内容”。正则两侧各有一个(?<=^|\s|\W)/(?=$|\s|\W)环视约束,避免误吞标识符中的$符号。语料中text $ math $ text前后都有空格,$ math $被完整摘出为InlineMathToken。
10.2.function {arg1} {arg2}→ FunctionCallToken
行内函数调用模式来自FunctionCallPatterns().inlineFunctionCall(第 14–16 行),形态为“.前缀的函数名 + 若干{...}花括号参数”。FunctionCallToken携带的 walker 结果由 FunctionCallWalkerParser 解析。对语料这一段,LexerTest.functionCall()(第 495–501 行)给出了精确语义断言:函数名为function,两个位置参数值分别为arg1、arg2——与inline()序列里FunctionCallToken后接PlainText(尾部x)的切分完全吻合。
此外,同一测试文件还有一组针对性守卫(可直接查阅 LexerTest.kt 第 811–842 行):
\.func {x}:反斜杠转义后不识别为函数调用(Escape + PlainText);.a {x} .b {y}:相邻调用切为两个独立 token;- 行尾的
.func {x}同样完整识别。
这些用例与inline.md共同保证行内函数调用在真实段落里(有前后文本、标点包围、转义场景)行为稳定。
11. 复现与扩展方式
查看与运行方式如下(仓库为只读参考,命令仅用于本地验证):
- 语料文件:inline.md,同目录还有 8 个姊妹语料文件,共同覆盖块级、转义、实体、强调、注释、行内函数等维度;
- 消费测试:LexerTest.kt,其中
inline()对应本语料,escapedFunctionCall()、adjacentInlineFunctionCalls()、functionCallAtEndOfSource()等补充行内函数调用边界; - 正则模式实现:BaseMarkdownInlineTokenRegexPatterns.kt 与 QuarkdownInlineTokenRegexPatterns.kt,方言组装可见
flavor包下的QuarkdownFlavor/BaseMarkdownFlavor; - 本地运行该语料对应的测试:
./gradlew :quarkdown-core:test --tests "com.quarkdown.core.LexerTest"如果日后要给行内词法新增识别能力(例如新的定界符语法),标准做法是:先在本语料同目录新增或扩展.md夹具,在LexerTest中追加对应的 token 序列断言,再在*InlineTokenRegexPatterns中新增TokenRegexPattern并按方言装配——词法行为因此始终有“语料 + 断言 + 正则”三位一体的可验证依据。
12. 小结
inline.md以 17 行覆盖了 Quarkdown 行内词法的全部关键路径:转义、代码片段(含反引号反例)、软/硬换行、三种强调、两种自动链接、内联/引用式链接与图片、跨行注释、跨行强强调、链接标签内的星号陷阱,以及 Quarkdown 扩展的行内数学与函数调用。配合 LexerTest.inline() 的 50 条类型断言和 行内正则模式源码,它构成了一份可直接引用的“行内分词行为规格”——理解这份语料,就理解了 Quarkdown 在词法阶段如何把一段原始段落文本切分为可被语法分析器消费的结构化 token 流。
【免费下载链接】quarkdown🪐 Markdown with superpowers: from ideas to papers, presentations, websites, books, and knowledge bases.项目地址: https://gitcode.com/GitHub_Trending/qu/quarkdown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考