Tolaria 原始编辑器语法高亮实践:以 @codemirror/lang-markdown 替代正则装饰(ADR-0037)
2026/9/13 13:34:13 网站建设 项目流程

Tolaria 原始编辑器语法高亮实践:以 @codemirror/lang-markdown 替代正则装饰(ADR-0037)

【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria

本文基于 Tolaria 的架构决策记录 0037-codemirror-language-markdown-highlighting.md,详解其 Raw Editor(CodeMirror 6 底层编辑器)如何从脆弱的正则装饰方案迁移到官方的语言级 Markdown 解析,并结合 src/extensions/markdownHighlight.ts 与 src/extensions/frontmatterHighlight.ts 的源码,说明HighlightStyle标签映射、frontmatter YAML 保留策略以及扩展装配链路。读完你可以理解“语言包解析 + 自定义高亮样式”这一 CodeMirror 6 标准工作模式,并知道未来给任务列表、表格等语法新增高亮时应改动哪一层。

背景:正则装饰为何走不下去

Tolaria 采用双编辑器架构(见 ADR-0022):富文本编辑器负责日常写作,Raw Editor 则直接编辑 Markdown 源文件,底层是 CodeMirror 6。Raw Editor 最初只带了一个自定义的frontmatterHighlight扩展——用基于正则的行级Decoration.mark给 YAML frontmatter 和标题着色。这意味着 Markdown 正文完全没有语法高亮:粗体、斜体、链接、列表、引用块、代码全部是“裸”文本,编辑器虽然是完整的 CodeMirror 实例,观感却与普通<textarea>无异。

当时摆在面前的两条路:

  1. 把正则插件继续扩展到覆盖全部 Markdown 语法;
  2. 换一个真正的语言解析器。

ADR-0037 给出的判断很直接:正则分词覆盖所有 Markdown 语法“脆弱且难以维护”。证据是当时已经出现过一次真实缺陷——标题装饰与 frontmatter 装饰在同一行发生重叠,说明两套基于行文本匹配的规则在边界情况下会互相踩踏。正则无法表达嵌套结构(例如粗体里套斜体、链接文本里带强调),而 Markdown 恰恰充满这类嵌套。

决策:官方语言包 + 自定义 HighlightStyle

ADR-0037 的决策(原文 Decision 一节)可以拆成三句话:

  • 替换:移除frontmatterHighlight.ts中的标题(heading)正则装饰,改用@codemirror/lang-markdown——CodeMirror 官方的语言包,基于 Lezer 解析器,是社区持续维护的语法实现;
  • 自定义样式:新增一个自定义HighlightStyle,把 CodeMirror 的高亮标签(tags)映射到视觉样式,覆盖标题、粗体、斜体、删除线、链接、列表、引用块与行内代码;
  • 保留 YAML 插件:frontmatter 的 YAML 专用着色插件继续保留(因为语言包对 frontmatter 的着色不如定制需求精细),只是删掉它不再需要的标题装饰,避免与语言解析器重复着色。

这个组合正是 CodeMirror 6 处理高亮的标准分层:解析归语言包,外观归 HighlightStyle。新增一条高亮规则只需要加一行样式声明,不碰解析逻辑。

源码印证:markdownLanguage()如何装配语言栈

markdownHighlight.ts 是该 ADR 落地的核心文件。最关键的导出是markdownLanguage()

export function markdownLanguage(): Extension { return [ yamlFrontmatter({ content: markdown({ codeLanguages: markdownCodeLanguages }) }), rawEditorSyntaxHighlighting(), ] }

这一小段代码体现了三层设计:

  1. yamlFrontmatter包裹markdown@codemirror/lang-yaml提供的yamlFrontmatter会把文档开头的--- ... ---块切出来按 YAML 解析,块内正文再交给markdown()。这解决了正则时代“frontmatter 里的# comment被当成标题”的歧义——现在是解析树层面的区分,而非行文本启发式。

  2. 嵌套代码语言markdown({ codeLanguages })传入了一份代码块内嵌语言列表,当前注册了 HTML:

    const markdownCodeLanguages = [ LanguageDescription.of({ name: 'html', alias: ['htm'], extensions: ['html', 'htm'], support: html(), }), ]

    因此围栏代码块html 内部的内容会走 `@codemirror/lang-html` 解析,标签名和属性值都有独立语法节点。测试 [markdownHighlight.test.ts](https://link.gitcode.com/i/dafc180b7882617280eeeaa68c08ad54) 验证了这一点:对 `html块内的

    syntaxTree中应能解析出TagNameAttributeValue` 节点。
  3. 样式层rawEditorSyntaxHighlighting()返回syntaxHighlighting(markdownHighlightStyle),把样式声明挂到语法树上。

HighlightStyle:一份标签到视觉的映射表

样式定义在 markdownHighlight.ts,用HighlightStyle.define([...])声明式地列出规则,每条规则由tags.*标签与 CSS 声明组成。摘录其骨架(颜色全部引用 CSS 变量,跟随应用主题切换):

const SYNTAX_COLORS = { heading: 'var(--syntax-heading)', link: 'var(--syntax-link)', monospace: 'var(--syntax-monospace)', monospaceBackground: 'var(--syntax-monospace-bg)', // ...其余键略 } const markdownHighlightStyle = HighlightStyle.define([ { tag: tags.heading1, color: SYNTAX_COLORS.heading, fontWeight: '700', fontSize: '1.4em' }, { tag: tags.heading2, color: SYNTAX_COLORS.heading, fontWeight: '700', fontSize: '1.25em' }, { tag: tags.heading3, color: SYNTAX_COLORS.heading, fontWeight: '600', fontSize: '1.1em' }, { tag: tags.heading4, color: SYNTAX_COLORS.heading, fontWeight: '600' }, { tag: tags.heading5, color: SYNTAX_COLORS.heading, fontWeight: '600' }, { tag: tags.heading6, color: SYNTAX_COLORS.heading, fontWeight: '600' }, { tag: tags.strong, fontWeight: '700' }, { tag: tags.emphasis, fontStyle: 'italic' }, { tag: tags.strikethrough, textDecoration: 'line-through' }, { tag: tags.link, color: SYNTAX_COLORS.link, textDecoration: 'underline' }, { tag: tags.monospace, color: SYNTAX_COLORS.monospace, backgroundColor: SYNTAX_COLORS.monospaceBackground, borderRadius: '3px' }, { tag: tags.quote, color: SYNTAX_COLORS.muted, fontStyle: 'italic' }, { tag: tags.separator, color: SYNTAX_COLORS.muted }, // 代码块内部的通用标签:comment / keyword / atom / number / string / operator ... ])

几个值得注意的细节:

  • 标题做了字号阶梯:H1 1.4em、H2 1.25em、H3 1.1em,且字重从 700 逐级降到 600。这让 Raw Editor 里直接浏览文档时,标题层级在纯文本环境中也有视觉区分——这正是当年被删掉的正则标题装饰想达到、但做不稳健的效果。
  • 规则粒度细于 ADR 摘要:ADR 只提到“headings, bold, italic, strikethrough, links, lists, blockquotes, inline code”,实际样式表还覆盖了commentkeywordatomnumberstringoperatorpunctuation等标签。这些标签主要服务于 frontmatter 内的 YAML 与代码块内嵌语言,说明样式表是按“同一张色卡服务多种语法”来设计的,所有颜色都走--syntax-*--text-*主题变量。
  • 列表只高亮标记符:测试 markdownHighlight.test.ts 专门断言- item one- nested item1. ordered item三行中,DOM 里唯一的<span>就是-/1.这样的标记符,其余文本保持普通内容节点。这是一种克制的视觉策略:不污染正文,只给结构标记着色。

保留件:frontmatterHighlight 插件现在只管什么

frontmatterHighlight.ts 并没有被删除,而是收窄了职责。ADR 中“保留 YAML 插件、移除标题装饰”的说法可以在源码中逐条对上:

  • 文件开头已无任何标题匹配逻辑。插件现在只处理---包围的 frontmatter 块:findFrontmatterEnd()从第一行---起找到闭合的---decorateFrontmatterLine()对分隔行打cm-frontmatter-delimiter类、对冒号前的键打cm-frontmatter-key、对值打cm-frontmatter-value
  • YAML 错误波浪线:这是正则时代不具备的能力。decorateYamlErrors()(L62-L79)截取 frontmatter 块内容,用yamlLanguage.parser.parse(source)真正做一次 YAML 解析,遍历解析游标收集cursor.type.isError的区间,映射回文档绝对位置后打上cm-frontmatter-error装饰——主题里表现为红色背景加波浪下划线(L123-L134)。也就是说,frontmatter 写坏了会在编辑器里即时可见,而不仅是保存时才报错。
  • 实现形态:插件仍是ViewPlugin.fromClass,仅在update.docChanged时重建装饰集合(L108-L121),成本可控。

测试 frontmatterHighlight.test.ts 覆盖了:分隔行/键/值类名、无 frontmatter 时零装饰、broken line这类畸形 YAML 被波浪线标注、嵌套集合与块标量不产生误报、编辑后装饰随dispatch重算。

装配链路:两个扩展如何进入编辑器实例

ADR 原文说“两个扩展在useCodeMirror.ts中组合”。从源码结构看,实际的组合点收敛到了一个更小的分派函数里:

  1. useCodeMirror.ts 在创建EditorState时,把rawEditorLanguageExtensionsForPath(sourcePath)列入 extensions 数组(L369),与lineNumbershistory、主题、键位等并排。注意这里传入的是文件路径——Raw Editor 并非只编辑 Markdown,它还承担 JSON、YAML、Python、SQL 等文件类型的查看。

  2. rawEditorLanguage.ts 按语言 ID 分派,markdown 分支正是 ADR 描述的“两者组合”:

    case 'markdown': return [markdownLanguage(), frontmatterHighlightTheme(), frontmatterHighlightPlugin]

    即:yamlFrontmatter+markdown语言栈负责整体解析与正文高亮,frontmatterHighlightPlugin/Theme只负责 frontmatter 块的精细着色与错误反馈。两者作用域不重叠——这正是 ADR 决策里“移除标题装饰、避免双重着色”要达成的状态。

备选方案复盘

ADR 完整记录了三个选项,值得作为技术选型的参照:

选项内容结论
A(采纳)@codemirror/lang-markdown+ 自定义 HighlightStyle使用官方维护的语言解析器;未来高亮规则“一条 CSS 声明”即可扩展。代价:新增一个 npm 依赖;frontmatter 插件需单独保留
B扩展自定义正则插件覆盖全部 Markdown无新依赖;但正则分词对嵌套格式脆弱,且标题/frontmatter 重叠 bug 已证明其维护成本高
C换成 Markdown 专用编辑器(Milkdown、Monaco 等)功能最全;但属于大迁移,会破坏 ADR-0022 的双编辑器架构,范围不可控

选项 A 的“唯一下游成本”——新增运行时依赖@codemirror/lang-markdown——可在 package.json 中确认。这也是 ADR 明确列出的 Consequences 第一条:这是该变更引入的唯一新运行时依赖。

影响面与后续扩展路径

ADR 的 Consequences 一节给出的影响清单,对照当前仓库状态可以这样理解:

  • 文件职责变化frontmatterHighlight.ts被简化(不再碰标题);markdownHighlight.ts成为正文高亮的唯一负责者。这一点在两份源码中已完全落地。
  • 扩展成本低:后续想给任务列表(- [x])、表格等新语法着色,只需扩展HighlightStyle,不改解析器。由于标签体系来自@lezer/highlight,规则本身与解析器版本解耦。
  • 预留的重估触发点:ADR 提醒“当编辑器演进时(例如 frontmatter 块需要从装饰文本改为按代码块解析),要重新评估@codemirror/lang-markdown与自定义 frontmatter YAML 处理是否冲突”。当前yamlFrontmatter({ content: markdown(...) })的写法让 frontmatter 作为独立顶层节点存在(测试 markdownHighlight.test.ts 断言块内# comment的解析链包含Frontmatter而不含ATXHeading1),两个系统的作用域边界目前是清晰的。

小结

ADR-0037 示范了一次低成本的编辑器体验升级:不重写编辑器、不换技术栈,只把“正则装饰”这一层换成“语言解析 + 声明式样式”,就补上了 Raw Editor 缺失的正文高亮,同时用官方 YAML 解析器为 frontmatter 增加了正则做不到的错误反馈。关键取舍有三条——解析归官方语言包、外观归自定义HighlightStyle、作用域重叠的装饰逻辑只保留一份。对同样使用 CodeMirror 6 的团队,这条路径(yamlFrontmatter包裹markdown、按标签表声明样式、用测试断言解析树节点名)可以直接复用;对 Tolaria 自身,它也为后续表格、任务列表等高亮规则留下了“只加一行声明”的扩展口子。

【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询