Tolaria 原始文本编辑器的扩展名驱动语法高亮:基于 CodeMirror 语言包的实现剖析
Tolaria 原始文本编辑器的扩展名驱动语法高亮基于 CodeMirror 语言包的实现剖析【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria导读本文围绕 Tolaria 的一项架构决策展开当用户在原始raw编辑器中打开非 Markdown 的文本型文件如.sql、.json、.py、.yaml时如何依据文件扩展名自动匹配对应的 CodeMirror 语言包实现真正的语法高亮而不是统一套用 Markdown 高亮。读完本文你将理解 Tolaria 从所有原始文件都用 Markdown 高亮演进到按扩展名映射语言的完整决策链路、两个核心模块扩展名映射表与编辑器扩展选择器的实现细节、Markdown frontmatter 高亮如何被隔离以及如何在此基础上扩展新的语言支持。背景原始编辑器为何需要按扩展名高亮Tolaria 的体系里Markdown 笔记走富文本编辑器BlockNote见 ADR-0022而仓库vault中的非 Markdown 文本文件则进入基于 CodeMirror 6 的原始编辑器。这一分类由 ADR-0041 引入的fileKind字段决定vault 扫描器现在会索引所有文件每个VaultEntry携带fileKindmarkdown/text/binary其中text类型的文件以文件名为标题、不做 frontmatter 解析、直接交给原始编辑器打开覆盖.yml、.json、.ts、.py、.sh等类型。问题在于在 ADR-0140 落地之前原始编辑器对所有 raw 文件统一安装了 Markdown 语言扩展。这带来的直接后果是——Markdown 笔记本身工作正常但.sql、.json、.py、.yaml等文件被当作普通文本或长得像 Markdown 的文本来渲染完全无法使用自身的语法文法grammar。用户提交的 issue讨论 #872明确要求这些文件按扩展名获得语法高亮期望与 Markdown 笔记中围栏代码块fenced code block已有的高亮体验保持一致。从源码结构看这条文件类型 → 打开方式的链路大致是vault 扫描器产出fileKind: text的条目ADR-0041→ 前端在列表中以文件图标呈现NoteItem.tsx→ 打开后渲染RawEditorViewRawEditorView.tsx由它创建 CodeMirror 实例并注入语言扩展。决策在编辑器创建时按扩展名映射 CodeMirror 语言包ADR-0140 的核心决策是在原始编辑器创建时将 raw 文件扩展名映射到 CodeMirror 语言包。映射规则如下Markdown 文件.md/.markdown继续走原有的 frontmatter-aware Markdown 路径YAML、JSON、Python、SQL、JavaScript、TypeScript 类文件使用官方 CodeMirror 语言包对应的文法未知的文本文件保持纯文本不再继承 Markdown 高亮仅限 Markdown 使用的 frontmatter 装饰decoration与 YAML 错误警告严格限定在 Markdown 文件内。之所以这样设计是因为此前所有 raw 文件统一装 Markdown 扩展的实现虽然零依赖但本质上把非 Markdown 文件错误地当成了 Markdown 处理。新的映射方案在保持单一编辑器表面仍是 CodeMirror的前提下用官方维护的增量解析器incremental parser按需加载文法既保证正确性又不引入第二套渲染管线。该决策与 ADR-0037 一脉相承ADR-0037 已经把 raw 编辑器的 Markdown 正文高亮从自研正则装饰切换为codemirror/lang-markdown官方语言包ADR-0140 则将这一官方语言包优先的原则推广到了所有文本型文件。实现一扩展名到语言 ID 的映射表扩展名映射的职责被收敛在 src/utils/rawEditorLanguage.ts该文件同时导出了类型与两个关键函数。语言 ID 类型export type RawEditorLanguageId | html | javascript | json | jsx | markdown | plain | python | sql | tsx | typescript | yaml这 11 个 ID 覆盖了当前全部可识别的语言类别其中plain是兜底值代表无高亮。扩展名映射表const LANGUAGE_BY_EXTENSION new Mapstring, RawEditorLanguageId([ [cjs, javascript], [cts, typescript], [htm, html], [html, html], [js, javascript], [json, json], [jsonc, json], [jsx, jsx], [markdown, markdown], [md, markdown], [mjs, javascript], [mts, typescript], [py, python], [pyw, python], [sql, sql], [ts, typescript], [tsx, tsx], [yaml, yaml], [yml, yaml], ])这张表体现了几个值得注意的设计细节同源变体合并.cjs/.mjs归入javascript.cts/.mts归入typescript.jsonc归入json.htm与.html合并.yml与.yaml合并.py与.pyw合并——避免为每个变体重复登记大小写不敏感扩展名在取用时统一toLowerCase()因此.SQL、.HTM也能正确命中测试中专门覆盖了dashboard.HTM映射到html的用例无扩展名或未知扩展名 →plain.txt、无扩展名文件、以及任何不在表中的扩展名一律回退到纯文本。路径解析函数function filenameFromPath(path: string): string { return path.split(/[\\/]/u).at(-1) ?? } function extensionFromFilename(filename: string): string | null { const match /\.([^.])$/u.exec(filename) return match?.[1]?.toLowerCase() ?? null } export function rawEditorLanguageIdForPath(path?: string | null): RawEditorLanguageId { if (!path) return plain const extension extensionFromFilename(filenameFromPath(path)) if (!extension) return plain return LANGUAGE_BY_EXTENSION.get(extension) ?? plain }rawEditorLanguageIdForPath是整条链路的第一级入口先取出路径末尾的文件名兼容/与\分隔符再提取最后一个点号后的扩展名并转小写最后查表路径为空、无扩展名、查不到映射三种情况统一返回plain。这一函数同时也是 RawEditorView.tsx 判断是否显示 frontmatter 警告的依据只有当rawEditorLanguageIdForPath(path) markdown时界面才会展示 YAML 错误提示条。实现二语言 ID 到 CodeMirror 扩展的组装扩展名映射只解决是什么语言真正把语言变成 CodeMirror 编辑器扩展的组装逻辑在 src/extensions/rawEditorLanguage.ts。它导入 6 个官方语言包并对外暴露唯一入口rawEditorLanguageExtensionsForPath(path)。核心组装逻辑import { html } from codemirror/lang-html import { javascript } from codemirror/lang-javascript import { json } from codemirror/lang-json import { python } from codemirror/lang-python import { sql } from codemirror/lang-sql import { yaml } from codemirror/lang-yaml import { rawEditorLanguageIdForPath, type RawEditorLanguageId } from ../utils/rawEditorLanguage import { frontmatterHighlightPlugin, frontmatterHighlightTheme } from ./frontmatterHighlight import { markdownLanguage, rawEditorSyntaxHighlighting } from ./markdownHighlight function javascriptLanguage(id: RawEditorLanguageId): Extension { if (id typescript) return javascript({ typescript: true }) if (id tsx) return javascript({ jsx: true, typescript: true }) if (id jsx) return javascript({ jsx: true }) return javascript() } function highlighted(language: Extension): Extension[] { return [language, rawEditorSyntaxHighlighting()] } function markupLanguage(id: RawEditorLanguageId): Extension[] | null { switch (id) { case html: return highlighted(html()) case json: return highlighted(json()) case markdown: return [markdownLanguage(), frontmatterHighlightTheme(), frontmatterHighlightPlugin] case plain: return [] case python: return highlighted(python()) case sql: return highlighted(sql()) case yaml: return highlighted(yaml()) default: return null } } function scriptLanguage(id: RawEditorLanguageId): Extension[] { switch (id) { case javascript: return highlighted(javascriptLanguage(javascript)) case jsx: return highlighted(javascriptLanguage(jsx)) case tsx: return highlighted(javascriptLanguage(tsx)) case typescript: return highlighted(javascriptLanguage(typescript)) default: return [] } } function rawEditorLanguage(id: RawEditorLanguageId): Extension[] { return markupLanguage(id) ?? scriptLanguage(id) } export function rawEditorLanguageExtensionsForPath(path?: string | null): Extension[] { return rawEditorLanguage(rawEditorLanguageIdForPath(path)) }这段代码里有几处值得展开的实现细节单一 JavaScript 语言包复用.js、.jsx、.ts、.tsx四个 ID 全部经由codemirror/lang-javascript的javascript()工厂处理只是通过typescript与jsx两个配置开关区分方言而不是引入独立的 TS 语言包统一的高亮样式组合highlighted()把语言包与统一语法高亮样式rawEditorSyntaxHighlighting()打包成扩展数组保证所有语言共享同一套颜色变量体系Markdown 特例markdown分支不是走highlighted()而是组装markdownLanguage()frontmatter-aware Markdown加上frontmatterHighlightTheme()与frontmatterHighlightPlugin两个专属装饰扩展——这正是Markdown-only frontmatter 装饰与警告保持作用域隔离的落地位置兜底策略rawEditorLanguage()用markupLanguage(id) ?? scriptLanguage(id)串联两张表未知 ID 最终落到空数组即纯文本、零额外扩展。编辑器中的挂载点语言扩展最终在 useCodeMirror.ts 组装EditorState.create时作为扩展数组的一员注入const state EditorState.create({ doc: initialContentRef.current, extensions: [ lineNumbers(), highlightActiveLine(), EditorView.lineWrapping, // ... 其他扩展 rawEditorLanguageExtensionsForPath(sourcePath), // ... ], })也就是说语言选择发生在编辑器实例创建的同一时刻创建时一次性确定之后切换文件会重建编辑器状态这正是 ADR-0140 所述at editor creation time的工程含义。实现三Markdown 路径与 frontmatter 高亮的隔离ADR-0140 明确要求Markdown-only frontmatter decorations and warnings stay scoped to Markdown files。这对应两层实现Markdown 语言与统一高亮样式src/extensions/markdownHighlight.ts 导出两个函数rawEditorSyntaxHighlighting()基于HighlightStyle.define的统一语法高亮把 Lezer 高亮标签tags.heading1、tags.strong、tags.link、tags.monospace、tags.keyword、tags.string、tags.number等映射到一组 CSS 变量--syntax-heading、--syntax-highlight-keyword、--syntax-highlight-string等。这套样式被highlighted()复用于所有非 Markdown 语言实现多语言、同一配色markdownLanguage()组装yamlFrontmatter({ content: markdown({ codeLanguages: markdownCodeLanguages }) })——用codemirror/lang-yaml的yamlFrontmatter扩展识别文档头部的 YAML frontmatter 块内部再用codemirror/lang-markdown解析正文并把.html/.htm注册为 Markdown 围栏代码块的子语言。frontmatter 装饰与 YAML 错误提示src/extensions/frontmatterHighlight.ts 是 ADR-0037 保留下来的自研ViewPlugin负责定位首行---与闭合---之间的 frontmatter 区间findFrontmatterEnd对分隔符、键、值分别打上cm-frontmatter-delimiter、cm-frontmatter-key、cm-frontmatter-value装饰类用yamlLanguage.parser对 frontmatter 内容做语法错误检测命中错误节点时追加波浪线 背景色的cm-frontmatter-error装饰通过frontmatterHighlightTheme()的EditorView.baseTheme定义这些类的视觉样式。在 rawEditorLanguage.ts 中frontmatterHighlightPlugin与frontmatterHighlightTheme只出现在case markdown分支而 RawEditorView.tsx 中showFrontmatterWarning rawEditorLanguageIdForPath(path) markdown又把YAML 错误提示条RawEditorYamlErrorBanner的显示条件限定为 Markdown 文件。双重隔离确保了打开.sql、.json等文件时绝不会出现 frontmatter 相关装饰或警告。决策权衡为什么选官方语言包而非 ShikiADR-0140 在 Options considered 中比较了三条路线方案结论关键考量使用官方 CodeMirror 语言包采纳保留 CodeMirror 单一编辑表面、获得官方维护的增量解析器、无需第二渲染管线复用 BlockNote 代码块中的 Shiki放弃Shiki 是静态高亮器视觉上更接近富文本代码块但需要并行维护一套 CodeMirror decoration 管线复杂度高所有 raw 文件继续用 Markdown 高亮放弃零新依赖但无法满足按扩展名高亮的诉求且持续把非 Markdown 文件当作 Markdown 处理选择官方语言包的最大收益在于增量解析器CodeMirror 语言包基于 Lezer 解析器框架编辑时只重解析受影响的文档区间天然契合交互式编辑场景而 Shiki 的静态 tokenize 模型若用于实时编辑必须依赖 decoration 管线做增量 diff成本远高于直接挂载语言包。运行时依赖与版本ADR-0140 引入的运行时依赖在 package.json 中均有真实记录codemirror/lang-html: ^6.4.11, codemirror/lang-javascript: ^6.2.5, codemirror/lang-json: ^6.0.2, codemirror/lang-python: ^6.2.1, codemirror/lang-sql: ^6.10.0, codemirror/lang-yaml: ^6.1.2其中codemirror/lang-javascript、codemirror/lang-json、codemirror/lang-python、codemirror/lang-sql四个包正是 ADR-0140 明确列举的新增依赖codemirror/lang-html与codemirror/lang-yaml则在此之前已服务于 Markdown 围栏代码块与 frontmatter 高亮参见 markdownHighlight.ts 与 frontmatterHighlight.ts。对应的解析器版本与传递依赖可在 pnpm-lock.yaml 中核对。测试验证映射表的可回归保证扩展名映射并非只靠手写src/utils/rawEditorLanguage.test.ts 用 vitest 的it.each参数化用例固化了全部映射行为正向用例example.md → markdown、example.markdown → markdown、query.sql → sql、config.yaml/config.yml → yaml、package.json/settings.jsonc → json、dashboard.html/dashboard.HTM → html验证大小写归一化、report.py/report.pyw → python兜底用例plain.txt与无扩展名文件均返回plain确保未知文本不会意外继承任何语言高亮。这些用例直接对应 rawEditorLanguage.ts 的映射表任何新增或误删扩展名映射都会在单测层立即暴露。围绕编辑器行为的组件级验证见 RawEditorView.behavior.test.tsx 等测试文件。如何扩展新的语言支持ADR-0140 在 Consequences 中给出了明确的扩展路径新增语言 修改映射表 使用官方 CodeMirror 语言包。具体到代码上需要两步在 rawEditorLanguage.ts 的LANGUAGE_BY_EXTENSION中登记新扩展名并视需要在RawEditorLanguageId联合类型中增加 ID在 rawEditorLanguage.ts 的markupLanguage()/scriptLanguage()分支中挂载对应的官方语言包并用highlighted()包装以复用统一配色。例如要支持 Rust先在映射表中加入[rs, rust]再在markupLanguage增加case rust: return highlighted(rust())依赖codemirror/lang-rust。由于高亮样式由rawEditorSyntaxHighlighting()统一提供新语言无需单独写配色视觉上与既有语言保持一致。影响与边界从 ADR-0140 的 Consequences 可以看到这次决策带来的整体影响行为改善.sql、.json、.py、.yaml、.js/.ts/.jsx/.tsx、.html等文本文件在原始编辑器中获得与自身文法匹配的高亮与 Markdown 笔记内围栏代码块的高亮体验对齐职责划分清晰src/utils/rawEditorLanguage.ts负责扩展名 → 语言 ID的纯函数映射可独立单测src/extensions/rawEditorLanguage.ts负责语言 ID → CodeMirror 扩展的组装两层分离让映射与编辑器细节互不耦合未识别的文本文件保持纯文本plain兜底意味着.txt、.log等文件不会被错误套用 Markdown 文法避免误导性的高亮frontmatter 能力严格限定在 Markdownfrontmatter 装饰、YAML 错误提示条RawEditorView.tsx只对fileKind: text中的 Markdown 文件生效。需要说明的边界是该能力只作用于fileKind: text的文本文件二进制文件如图片、PDF仍按 ADR-0041 的约定灰显且不可点击不在本文讨论的语法高亮范围内此外语言选择在编辑器创建时一次性确定若未来需要运行时动态切换语言则属于超出当前 ADR 的新议题。结语ADR-0140 用一张扩展名映射表 一层扩展组装函数把原始编辑器统一套 Markdown 高亮的粗放行为改造成了按扩展名精确匹配 CodeMirror 官方语言包的精细方案。它的工程价值不仅在于补上了非 Markdown 文本文件的高亮空白更在于确立了可复用的两层结构纯函数映射可测与扩展组装可组合并明确了新语言 新映射 官方语言包的扩展范式。对于在 Tolaria 中维护或扩展原始编辑器的人来说src/utils/rawEditorLanguage.ts 与 src/extensions/rawEditorLanguage.ts 就是这份决策的活文档。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考