MarkText muya 链接语法 Round-Trip 测试:Links.md 夹具与参考链接解析的源码机制
【免费下载链接】marktext📝A simple and elegant markdown editor, available for Linux, macOS and Windows.项目地址: https://gitcode.com/gh_mirrors/ma/marktext
本文以 muya 编辑器(MarkText 的核心编辑器库)中的链接 Round-Trip 夹具Links.md为主体,完整解读它覆盖的链接语法场景——行内链接、列表/任务项内链接、以及 full/collapsed/shortcut 三种参考链接形态——并结合 Round-Trip 测试框架 与 InlineRenderer 参考定义收集实现,说明“Markdown → 状态树 → Markdown”往返稳定性是如何被断言和保证的。读完本篇,你将掌握该夹具的设计意图、参考链接标签(label)解析的两条核心正则,以及如何为同类语法扩展往返测试。
一、Links.md 夹具覆盖的链接语法全景
该夹具位于 Links.md,是 common(CommonMark)目录下 8 个链接相关夹具之一。全文如下,共包含四个语法场景:
# Links [title](http://127.0.0.1) [title with spaces](https://localhost) - [title](http://127.0.0.1) - [title with spaces](https://localhost) - [ ] [title](http://127.0.0.1) - [x] [title with spaces](https://localhost) ## Reference Links You can also put the [link URL][1] below the current paragraph like [this][2]. [1]: http://url.local [2]: http://another.url Or you can use a [shortcut][] reference, which links the text "shortcut" to the link named "[shortcut]" on the next paragraph. [shortcut]: http://goes/with/the/link/name/text四个场景分别对应:
- 段落中的行内链接(inline link):
[title](http://127.0.0.1),特意包含带空格的链接文本title with spaces,用于验证序列化时文本原样保留; - 无序列表项内的链接:
- title,验证链接 token 与列表结构可以正确嵌套往返; - GFM 任务列表项内的链接:
- [ ] title与- [x] title with spaces,同时检验未勾选([ ])与已勾选([x])两种复选框状态; - 参考链接(reference links):这是该夹具的重点,覆盖了三种形态与两类定义行:
- Full 形式:
[link URL][1],链接文本与标签名不同; - 定义行:
[1]: http://url.local、[2]: http://another.url,标签与 URL 分离,定义可以放在当前段落之后的段落里; - Collapsed/Shortcut 形式:
[shortcut][],链接文本本身即为标签名,定义[shortcut]: http://goes/with/the/link/name/text定义在“下一段”。
- Full 形式:
二、Round-Trip 测试框架:Links.md 是如何被校验的
夹具本身只是数据,真正的校验逻辑在 roundTrip.spec.ts 中。测试入口注册了全部 11 个夹具,其中common / Links对应本夹具:
const fixtures: IFixture[] = [ { label: 'common / Basic Text Formatting', file: 'common/BasicTextFormatting.md' }, // ... { label: 'common / Links', file: 'common/Links.md' }, // ... ];往返流水线与收敛性断言
核心流程见 roundTrip 函数:先用MarkdownToState把 Markdown 解析为状态树,再用StateToMarkdown重新序列化,固定参数为:
const states = new MarkdownToState({ footnote: false, math: true, isGitlabCompatibilityEnabled: true, trimUnnecessaryCodeBlockEmptyLines: false, frontMatter: true, }).generate(markdown); return new StateToMarkdown({ listIndentation: 1 }).generate(states);断言策略上,测试并不要求首轮输出与原文逐字节相等(源码注释明确说明对大多数夹具这在 marktext 时代就已经不成立,如列表缩进差异),而是要求收敛:对原文跑一次往返得到once,再对once跑一次得到twice,要求twice === once(见 isStableUnderRoundTrip)。这意味着Links.md的首轮输出允许与原文有差异(例如列表缩进规范化),但必须从第一轮起就“定型”,不再漂移。
比较前有一个刻意的归一化函数 normalise:
function normalise(md: string): string { return md .replace(/\r\n?/g, '\n') .replace(/\n+$/, ''); }它只做两件事:CRLF 统一为 LF(新 muya 内部统一使用 LF),以及去掉文末多余换行(序列化器总是补一个尾部换行)。注释特别强调故意不去除每行行尾空白——因为 CommonMark §6.7 中两个行尾空格是硬换行标记,若折叠掉会掩盖真实的往返不稳定。
值得注意的是,测试另有一组“恒等”夹具(首轮输出与原文完全一致):identityFixtures 只包含common/Images、common/Escapes、gfm/BasicTextFormatting、gfm/Tables四个。Links.md不在其中,即参考链接场景只承诺收敛性、不承诺首轮字节级恒等——这与参考链接序列化存在规范化空间(定义行缩进、标签大小写等)的事实相符。
三、参考链接的实现机制:labels Map 与两条核心正则
夹具中的 full/collapsed/shortcut 形态要能正确渲染与往返,依赖 muya 内联渲染管线中的标签收集与解析。从源码结构看,整体是“定义收集 → 内联分词 → 标签查表”三步。
定义收集:定义行就是普通段落文本
在 state/types.ts 中,ILinkReferenceDefinitionState被标记为@deprecated,注释说明了当前模型:参考定义不以独立节点存储,而是保留为普通paragraph状态节点,其text就是原始的[label]: url "title"一行(与旧 marktext 的 “definition is paragraph text” 模型一致)。这正是Links.md中[1]: http://url.local这类定义行在往返中能以原文形式存活的结构基础。
标签的收集由 InlineRenderer._collectReferenceDefinitions 完成:每次patch渲染一个内容块之前,先递归遍历整棵状态树,对每个paragraph节点调用 getLabelInfo,把命中定义正则的段落登记进labels: Map<string, { href, title }>。其中标签键统一小写化((tokens[2] + tokens[3]).toLowerCase()),这解释了 CommonMark 的标签大小写不敏感规则。
定义行正则:beginRules.reference_definition
匹配整行的定义行正则位于 rules.ts:
reference_definition: /^( {0,3}\[)([^\]]+?)(\\*)(\]: *)(<?)([^\s>]+)(>?)(?:( +)(["'(]?)([^\n"'()]+)\9)?( *)$/,各捕获组的分工:
| 捕获组 | 匹配内容 | 对应语法 |
|---|---|---|
| 1 | 行首最多 3 个空格 +[ | 缩进上限(CommonMark 要求 ≤3 空格) |
| 2–3 | 标签文本(允许内部转义) | [1]、[shortcut]中的1/shortcut |
| 4 | ]:分隔符 | 定义行固定结构 |
| 5–7 | URL(可选尖括号包裹) | http://url.local,也兼容<url>形式 |
| 8–10 | 可选 title(引号或括号样式) | "title"/(title)/'title',组 9 与组 10 回引确保引号配对 |
| 11 | 行尾空白 | 允许定义行尾随空格 |
getLabelInfo中href取组 6、title取组 10 或空串,与上表的分组一致。
内联引用解析:reference_link 正则支持嵌套括号
内联层匹配[text]、[text][label]以及省略第二个方括号的 shortcut 形式,规则是 rules.ts 中的 reference_link:
// Link text can hold balanced brackets — notably an image `alt` — // so mirror `link`'s nesting-capable anchor group instead of the bracket- // free `[^\]]+?`, which stopped at the image's inner `]` and broke // `[alt][ref]` (#4865). reference_link: /^\[((?:\[[^\]]*\]|[^[\]]|\](?=[^[]*\]))*?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/,两处实现细节值得注意:
- 链接文本支持平衡括号:与行内
link规则使用同一组可嵌套的括号锚点,专门修复了[alt][ref](链接文本里嵌图片)会被内层]截断的问题(源码注释标注对应 issue #4865); - 第二个方括号组是可选的(
(?:\[...\])?):[shortcut][](显式空标签的 collapsed 形式)与[shortcut](shortcut 形式)都能被同一条规则消费,随后由分词器拿标签去labelsMap 中查表得到 href/title。
单元测试对解析行为的完整覆盖
上述管线有一条专门的状态机级单测 referenceLink.spec.ts,它不启动真实 Muya 实例,而是复刻MarkdownToState → collectLabels → tokenizer全链路,共 8 个用例,与Links.md的场景一一对应:
- 定义行
[1]: https://example.com "title"以 paragraph 状态保留,title 不丢失; - 往返序列化输出仍包含完整的定义行(正则断言
\[1\]:\s*https://example\.com); - full 形式
[bar][1]在 labels 已知后产出reference_linktoken,且label === '1'; - Full / Collapsed / Shortcut 三种形式全部产出
reference_linktoken,并校验isFullLink标志([full][1]为 true,其余两者为 false); - 定义中的 title 通过 label 查表传播(
"Ref Title"原样保留); - 标签匹配大小写不敏感(
[bar][REF]命中[ref]定义); - 重复标签以第一条定义为准(
first definition wins); - 无匹配定义的孤儿引用
[missing][nope]保持为纯文本,不产生reference_linktoken。
四、小结:夹具、框架与实现三者如何闭环
- 夹具(Links.md)固化了 CommonMark 行内链接与三种参考链接形态的最小完备样本,刻意覆盖段落、无序列表、任务列表三种宿主结构与空格文本、跨段落定义等边界;
- 测试框架(roundTrip.spec.ts)以“二次往返收敛”而非首轮字节恒等作为
Links.md的断言标准,并用保留行尾空白的归一化避免掩盖硬换行回归; - 实现层通过“定义即段落文本”的状态模型、beginRules.reference_definition 整行正则、InlineRenderer 的标签收集 与可嵌套括号的 reference_link 规则,保证夹具中的 full/collapsed/shortcut 语法在渲染与导出两个方向上都稳定;
- 状态级单测(referenceLink.spec.ts)再对大小写、重复标签、孤儿引用这些夹具无法表达的语义细节做了补充断言。
若要为链接相关语法(例如带尖括号 URL 的定义行、带 title 的参考链接)补充往返保障,正确的扩展方式是:向marktext-round-trip/common/夹具追加样本、确认其是否满足首轮恒等(满足则加入identityFixtures),并在referenceLink.spec.ts中补充对应 labels 查表断言。
【免费下载链接】marktext📝A simple and elegant markdown editor, available for Linux, macOS and Windows.项目地址: https://gitcode.com/gh_mirrors/ma/marktext
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考