
Tolaria 富文本代码块 Shiki 语言直注册方案基于 shikijs/langs 的懒加载语法扩展与别名规范化【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria导读Tolaria 是一款以文件优先、本地优先为设计原则的 Markdown 知识库桌面应用其富文本编辑体验基于 BlockNote 构建代码块高亮默认由blocknote/code-block集成的高亮器驱动。本文围绕 ADR 0134Direct Shiki language registrations展开讲解 Tolaria 如何在不替换 BlockNote 原有 schema 与解析链路的前提下通过按需lazy注册shikijs/langs中缺失的常见 Shiki 语法PowerShell、VBScript、Dart、Dockerfile、Terraform/HCL、TOML 等并配合别名规范化、语言推断与 WebKit 正则能力探测实现导入围栏fenced code block高亮、语言选择器状态正确、Markdown 导出稳定的完整闭环。读完本文你将掌握扩展语法目录与别名表的组织方式、懒加载 loader 的实现与 Go 内联语法注册的取舍、导入别名如ps1、vb如何被归一化为规范语言、以及 WebKit 环境下的降级安全边界。背景BlockNote 自带语法高亮的覆盖缺口Tolaria 的富文本编辑器围绕 BlockNote 组织代码块由blocknote/code-block提供package.json 中同时声明了shikijs/langs: 3.23.0。BlockNote 自带的语法高亮器覆盖了其编辑器菜单中常见的 Web 与系统语言但存在明确的盲区未覆盖的常见语法PowerShell、VBScript、Dart、Dockerfile、Terraform/HCL、TOML 等常见围栏无法高亮导入围栏的别名问题用户导入的 Markdown 中powershell、ps1、vb、vbscript等围栏名在 BlockNote 内置目录中找不到对应项导致高亮失效、语言选择器状态异常序列化稳定性围栏导入后必须能够序列化回稳定的 Markdown 围栏避免ps1被写成其它奇怪的名字。ADR 0134 记录于 2026-05-29状态为 active是 Tolaria 针对这一缺口的架构决策docs/adr/0134-direct-shiki-language-registrations.md。决策保留 BlockNote 集成直接懒加载 shikijs/langs 语法ADR 的核心决策是一句话Tolaria 保留 BlockNote 的 code-block 集成并为缺失的常见语言与别名添加直接的、惰性的shikijs/langs注册。决策时评估过的三个备选方案方案描述结论仅保留 BlockNote bundle实现最简单但 PowerShell/VBScript 等常见围栏依然不支持放弃按需注册选定的shikijs/langs语法保留 BlockNote 的 schema 与解析路径只增加用户真正需要的额外语法采纳用完整的自定义 Shiki bundle 替换 BlockNote 高亮器控制力最强但相对当前需求是更大的结构性改动放弃这一决策直接决定了后续三个模块的职责划分从源码结构看它们各司其职src/components/codeBlockOptions.ts 依然是 BlockNote 高亮器配置的唯一持有者ownersrc/utils/codeBlockLanguageCatalog.ts 负责维护受支持的额外语言标签与别名目录src/utils/codeBlockLanguage.ts 负责把导入的已知别名如ps1、vb归一化为规范的语言名。语言目录受支持额外语言的单一事实来源codeBlockLanguageCatalog.ts定义了CodeBlockLanguageCatalogEntry在CodeBlockLanguageOption基础上增加id并用EXTRA_CODE_BLOCK_LANGUAGES常量集中声明 19 种额外语言及其别名export const EXTRA_CODE_BLOCK_LANGUAGES [ { id: powershell, name: PowerShell, aliases: [powershell, ps, ps1] }, { id: vbscript, name: VBScript, aliases: [vbscript, vbs, vb, vba, visual-basic, visualbasic] }, { id: dart, name: Dart, aliases: [dart] }, { id: groovy, name: Groovy, aliases: [groovy] }, { id: matlab, name: MATLAB, aliases: [matlab] }, { id: perl, name: Perl, aliases: [perl, pl, pm] }, { id: elixir, name: Elixir, aliases: [elixir, ex, exs] }, { id: erlang, name: Erlang, aliases: [erlang, erl] }, { id: fsharp, name: F#, aliases: [fsharp, f#, fs] }, { id: clojure, name: Clojure, aliases: [clojure, clj] }, { id: asm, name: Assembly, aliases: [asm, assembly] }, { id: zig, name: Zig, aliases: [zig] }, { id: hcl, name: HCL, aliases: [hcl] }, { id: terraform, name: Terraform, aliases: [terraform, tf, tfvars] }, { id: dockerfile, name: Dockerfile, aliases: [dockerfile, docker] }, { id: batch, name: Batch, aliases: [batch, bat, cmd] }, { id: diff, name: Diff, aliases: [diff, patch] }, { id: ini, name: INI, aliases: [ini, properties] }, { id: toml, name: TOML, aliases: [toml] }, ] as const satisfies readonly CodeBlockLanguageCatalogEntry[]这一目录同时是三个关键能力的输入别名到规范语言的映射knownLanguageAliases()遍历KNOWN_CODE_BLOCK_LANGUAGES含内联注册的 Go把每个语言的 id 与全部别名统一压入KNOWN_LANGUAGE_ID_BY_ALIAS映射canonicalKnownCodeBlockLanguage(language)对入参做trim().toLowerCase()后查表命中则返回规范 id否则返回null。这正是ps1 → powershell、vb → vbscript归一化的底层依据。选择器选项构建codeBlockLanguageOptions()把目录转换为 BlockNotesupportedLanguages所需的{ name, aliases }结构。Go 语言的特殊内联注册见下节。懒加载实现createTolariaCodeBlockOptions 的组装逻辑codeBlockOptions.ts的核心是createTolariaCodeBlockOptions()它通过展开...codeBlockOptions保留 BlockNote 默认能力然后覆写三个关键字段export function createTolariaCodeBlockOptions(): PartialCodeBlockOptions { const options: PartialCodeBlockOptions { ...codeBlockOptions, createHighlighter: createTolariaCodeHighlighter, defaultLanguage: text, supportedLanguages: { ...codeBlockOptions.supportedLanguages, go: GO_LANGUAGE, ...EXTRA_SUPPORTED_LANGUAGES, }, } if (supportsShikiRegexFeatures()) return options delete options.createHighlighter return options }该函数在 src/components/editorSchema.tsx 通过createCodeBlockSpec(createTolariaCodeBlockOptions())注入 BlockNote schema成为编辑器代码块行为的总入口同时被 src/components/codeBlockLanguageControls.tsx、src/components/richEditorPaste.ts 与 src/components/richEditorCodeBlockShortcutExtension.ts 复用保证语言选择器、粘贴解析与快捷插入三处看到的语言集合完全一致。额外语言的懒加载 loader 表EXTRA_LANGUAGE_LOADERS是一个Mapstring, TolariaLanguageLoader每个条目用动态import()按需拉取shikijs/langs/name模块从而避免一次性加载所有语法带来的包体积与启动开销const EXTRA_LANGUAGE_LOADERS new Mapstring, TolariaLanguageLoader([ [powershell, async () optionalLanguageInputs(() import(shikijs/langs/powershell))], [vbscript, loadVbScriptLanguage], [dart, async () optionalLanguageInputs(() import(shikijs/langs/dart))], [groovy, async () optionalLanguageInputs(() import(shikijs/langs/groovy))], [matlab, async () optionalLanguageInputs(() import(shikijs/langs/matlab))], [perl, async () optionalLanguageInputs(() import(shikijs/langs/perl))], [elixir, async () optionalLanguageInputs(() import(shikijs/langs/elixir))], [erlang, async () optionalLanguageInputs(() import(shikijs/langs/erlang))], [fsharp, async () optionalLanguageInputs(() import(shikijs/langs/fsharp))], [clojure, async () optionalLanguageInputs(() import(shikijs/langs/clojure))], [asm, async () optionalLanguageInputs(() import(shikijs/langs/asm))], [zig, async () optionalLanguageInputs(() import(shikijs/langs/zig))], [hcl, async () optionalLanguageInputs(() import(shikijs/langs/hcl))], [terraform, async () optionalLanguageInputs(() import(shikijs/langs/terraform))], [dockerfile, async () optionalLanguageInputs(() import(shikijs/langs/dockerfile))], [batch, async () optionalLanguageInputs(() import(shikijs/langs/bat))], [diff, async () optionalLanguageInputs(() import(shikijs/langs/diff))], [ini, async () optionalLanguageInputs(() import(shikijs/langs/ini))], [toml, async () optionalLanguageInputs(() import(shikijs/langs/toml))], ])几个值得注意的实现细节容错加载optionalLanguageInputs()用try/catch包裹动态导入模块加载失败时返回空数组而不是抛错保证“可选语法缺失不拖垮整个高亮器”。模块形状归一languageModuleInputs()只信任模块的default导出为数组的情况shikijs/langs的 ESM 导出约定非对象或非数组一律返回空数组。Batch 的差异batch的 loader 拉取的是shikijs/langs/bat模块Shiki 内部语法名是bat但目录中对外暴露的 id 是batch从源码结构看这是 Shiki 语法命名与产品命名不一致时的显式桥接。VBScript 重命名注册VBScript 是唯一需要二次加工的语法。shikijs/langs/vb加载后loadVbScriptLanguage()通过renameLanguageRegistration()把注册名从vb重写为vbscript并覆盖displayName与别名表async function loadVbScriptLanguage(): PromiseTolariaLanguageInput[] { const language await optionalLanguageInputs(() import(shikijs/langs/vb)) return renameLanguageRegistration(language, vb, { name: vbscript, displayName: VBScript, aliases: [vb, vbs, vba, visual-basic, visualbasic], }) }这样 VBScript 在语言选择器中以独立条目出现且vb、vbs、vba、visual-basic、visualbasic全部归一到vbscript与目录声明保持一致。主题优先级与 Go 内联语法createTolariaCodeHighlighter()对 BlockNote 返回的高亮器做了两层包装主题优先getLoadedThemes()通过prioritizeTheme()把当前主题根据document.documentElement上的darkclass 或data-theme判断默认github-light/github-dark提到主题数组首位语言展开loadLanguage()先经expandLanguage()展开每个入参——Go 命中内联注册GO_LANGUAGE_REGISTRATION额外语言命中 loader 表其余保持原样最后扁平化后委托给底层highlighter.loadLanguage(...)。Go 选择内联而非懒加载是刻意的GO_LANGUAGE_REGISTRATION在 src/components/codeBlockOptions.ts 中以 TextMate 风格 pattern 直接内联了 Go 的注释、关键字、数字、字符串规则与codeBlockLanguageOptions([GO_CODE_BLOCK_LANGUAGE]).go组合成supportedLanguages.go。这样 Go 在任意环境包括不支持 Shiki 完整正则能力的 WebKit下都能作为可选项出现而dart、terraform等则完全依赖按需模块。别名规范化导入围栏从 ps1/vb 到 powershell/vbscriptcodeBlockLanguage.ts承载两个紧密相关的职责已知别名规范化与无标签代码块的语言推断。inferCodeBlockLanguages(blocks)递归遍历 block 树对每个codeBlock依次执行规范化canonicalKnownCodeBlockLanguage(rawLanguage)命中目录时若与原始值不同则用规范 id 覆写props.language。测试 src/utils/codeBlockLanguage.test.ts 明确验证ps1→powershell、vb→vbscript。推断仅当语言属于纯文本集合、none、plain、plaintext、text、txt时inferCodeBlockLanguage()才会根据内容启发式推断语言。显式语言保护用户显式选择的语言如python与未知语言如foolang都保持原样——测试分别验证了这两类情况codeBlockLanguage.test.ts、codeBlockLanguage.test.ts。推断器本身是一组有序的正则探测器优先级严格固定const LANGUAGE_DETECTORS: Array[string, LanguageDetector] [ [html, (source) /^\s*[/!A-Za-z][\s\S]*\s*$/u.test(source)], [python, (source) /^\s*(?:def|class)\s\w.*:\s*$/mu.test(source)], [python, hasPythonImport], [shellscript, (source) /^\s*(?:#!.*\b(?:bash|sh|zsh)\b|(?:pnpm|npm|yarn|git|cd|echo|export)\b)/mu.test(source)], [typescript, (source) /\b(?:interface|type|enum|implements|readonly|namespace|declare)\b/u.test(source)], [typescript, hasTypedCallableSignature], [typescript, (source) /:\s*(?:string|number|boolean|unknown|never|void|null|undefined|Record|[A-Z]\w*(?:\[\])?)\b/u.test(source)], [javascript, (source) /\b(?:import|export|const|let|var|function|return)\b|/u.test(source)], [sql, (source) /^\s*(?:SELECT|WITH|INSERT|UPDATE|DELETE)\b[\s\S]*\bFROM\b/iu.test(source)], [yaml, (source) /^\s*[\w-]\s*:\s*[\s\S]*$/u.test(source)], ]注意 JSON 检测被前置inferCodeBlockLanguage()先尝试JSON.parse只有成功才判为json确保{ id: Demo, count: 1 }不会落入 JavaScript 分支——这一点由测试detects JSON before JavaScript-like punctuation固化codeBlockLanguage.test.ts。这一规范化在导入链路上被 src/hooks/editorBlockResolution.ts 调用配合 src/components/editorSchema.codeBlockLanguages.test.ts 中基于真实 BlockNote editor 的往返验证it.each覆盖sql→sql、powershell→powershell、ps1→powershell、vbscript→vbscript、vb→vbscript、php→php断言导入后 block 的props.language是规范名且blocksToMarkdownLossy导出的围栏与规范名一致——即“导入何种别名导出何种规范围栏”。语言选择器与粘贴解析的联动扩展语言目录不仅服务于高亮还同时驱动 UI 与粘贴行为语言选择器CodeBlockLanguageControlssrc/components/codeBlockLanguageControls.tsx通过 MutationObserver 监听 BlockNote 原生select控件将LANGUAGE_OPTIONS由createTolariaCodeBlockOptions().supportedLanguages生成即 BlockNote 内置语言 Go 19 种额外语言渲染为自绘的 shadcnSelect下拉覆盖在原生控件之上用户选择后经updateBlock(blockId, { props: { language } })写回 block。粘贴解析richEditorPaste.ts读取同一份supportedLanguages决定粘贴进来的围栏文本是否可按已知语言处理src/components/richEditorPaste.ts。快捷插入richEditorCodeBlockShortcutExtension.ts同样复用该配置生成可插入的语言选项src/components/richEditorCodeBlockShortcutExtension.ts。三处共享同一配置源从架构上杜绝了“选择器能看到、粘贴却不认”之类的集合漂移。安全降级WebKit 正则能力探测与失败安全createTolariaCodeBlockOptions()末尾有一段关键保护逻辑if (supportsShikiRegexFeatures()) return options delete options.createHighlighter return optionsShiki 的 TextMate 语法依赖现代正则能力命名捕获组、lookbehind、vflag 等。supportsShikiRegexFeatures()src/utils/regexCapabilities.ts通过真实编译一组探测正则来判断运行环境是否具备这些能力一旦探测失败如旧版 WebKitcreateHighlighter被整体移除编辑器退回 BlockNote 无高亮但完全可用的路径代码块编辑、序列化不受影响。这一行为在 src/components/editorSchema.webkit.test.ts 中被系统性测试主题优先亮色模式getLoadedThemes()[0]为github-light暗色模式为github-darkeditorSchema.webkit.test.ts目录完整supportedLanguages中的 Go、PowerShell、VBScript、Dart、HCL、Terraform、Dockerfile 等条目及其别名与目录声明一致editorSchema.webkit.test.ts懒加载成功loadLanguage(go)与全部额外语言含ps1、vb、php均能成功加载并进入getLoadedLanguages()editorSchema.webkit.test.ts模块容错用vi.doMock(shikijs/langs/powershell, () ({ namedOnly: [] }))模拟畸形模块loadLanguage(powershell)依然 resolve不抛异常editorSchema.webkit.test.ts能力降级在模拟缺失预编译正则 flag 与缺失 lookbehind 的 WebKit 下createTolariaCodeBlockOptions()不包含createHighlighter现代 WebKit 下则保留editorSchema.webkit.test.ts。代价与后续演进空间ADR 的 Consequences 部分给出了明确的代价与边界结合源码可以进一步印证语言菜单变大supportedLanguages扩展后选择器选项随之增长从源码结构看选项集合由目录常量集中驱动未来若要精简只需收缩EXTRA_CODE_BLOCK_LANGUAGES一处。未知别名失败安全目录未收录的别名如foolang不会报错围栏保留为显式语言名、按纯文本处理测试leaves unsupported explicit code block languages unchanged即为该行为的回归保障。未来的定制化触发点ADR 明确写道——如果 Tolaria 后续需要“生成的语法 bundle”“导出时高亮”或“显著更小的菜单”本 ADR 应被一个自定义 Shiki 打包决策取代。当前的懒加载单语法模块架构就是为这一演进预留的接口所有额外语言已经集中在 loader 表与目录常量两处替换为生成式 bundle 时改动面可控。总结ADR 0134 用“保留 BlockNote、直接懒加载 shikijs/langs”这一最小侵入方案解决了 Tolaria 富文本代码块在 PowerShell、VBScript、Dart、Dockerfile、Terraform/HCL、TOML 等常见语言上的覆盖缺口。三层职责划分清晰可循codeBlockLanguageCatalog.ts定义“有哪些语言与别名”、codeBlockLanguage.ts负责“导入别名归一化与语言推断”、codeBlockOptions.ts负责“高亮器组装与懒加载调度”。配合 WebKit 正则能力探测的安全降级与覆盖齐全的测试矩阵这一机制在提升编辑体验的同时保持了 Tolaria 一贯的本地优先、文件可读、Git 可 diff 的核心约束。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考