Tolaria 的 Markdown 持久化数学公式:基于占位符往返与 KaTeX 的笔记公式渲染方案
发布时间:2026/9/13 19:06:46来源:尧图网络
Tolaria 的 Markdown 持久化数学公式基于占位符往返与 KaTeX 的笔记公式渲染方案【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria在 Tolaria一个以 Markdown 文件为唯一事实来源的桌面笔记应用中数学公式支持是可读性与文件持久性之间的一次典型工程权衡用户期望像$Emc^2$、$$ ... $$这样的 LaTeX 语法在富文本编辑器里直接渲染同时保存后的.md文件必须保留原始的纯文本分隔符不能被编辑器私有格式改写。本文基于架构决策记录 0082 展开完整覆盖该决策的背景、备选方案与后果并结合 mathMarkdown.ts、editorSchema.tsx 等源码讲解占位符编码、公式合法性启发式、KaTeX 安全渲染与输入交互的完整实现链路。读完后你将掌握如何在 BlockNote/ProseMirror 编辑器中为自定义内容设计可往返的 Markdown 桥接层以及如何用测试固化往返契约。背景为什么公式不能直接交给编辑器处理Tolaria 的笔记是持久化的 Markdown 文件主编辑器使用 BlockNoteraw 模式使用 CodeMirror。来自技术笔记工具的用户期望行内公式$Emc^2$与展示公式$$ ... $$在笔记中直接渲染但又不希望笔记因此变成只有应用自己能读懂的私有格式。ADR 0082 的 Context 部分指出了两个具体约束本地编辑器包中的 BlockNote 目前没有第一方的数学公式 blockTiptap 官方虽然提供了基于 KaTeX 节点的 Mathematics 扩展但 Tolaria 的保存路径依赖 BlockNote 的 Markdown 解析器和blocksToMarkdownLossy()序列化器——直接塞入不透明的 ProseMirror 数学节点而没有配套的 Tolaria 序列化器会导致原始 Markdown 源码丢失或被改写。因此决策非常明确公式支持通过由编辑器流水线editor pipeline拥有的Markdown 占位符往返实现——把公式源码先换成临时占位符再交给 Markdown 解析渲染层用 Tolaria 自定义 schema 节点承载保存/进入 raw 模式前再还原为原始分隔符。备选方案四种路线的取舍ADR 中完整记录了四个候选方案及否决理由这是理解当前架构的钥匙方案评估结论Tolaria 自有的占位符往返 KaTeX 渲染与既有的 wikilink 架构一致保留纯文本源码不依赖 BlockNote 对非默认 ProseMirror 数学节点的支持采纳在 BlockNote 中直接用 Tiptap Mathematics 扩展官方出品、KaTeX 支撑有吸引力但无法单独解决 Tolaria 的 BlockNote Markdown 序列化契约否决仅在 raw 模式支持公式保留源码但牺牲了富编辑器中应有的阅读体验否决把公式存为自定义 JSON/frontmatter 元数据未来可做更丰富的结构化编辑但违反 Markdown 优先的持久性要求否决值得注意的是与既有 wikilink 架构一致并非随口一提——Tolaria 的 richEditorMarkdown.ts 中wikilink 的预处理/注入preProcessWikilinks/injectWikilinks与公式的preProcessMathMarkdown/injectMathInBlocks位于同一条流水线里见下文往返流水线在保存路径中的位置公式模块实际上是这套自研 Markdown 编解码器模式的又一次复用。核心实现一占位符编码与往返函数ADR 的 Consequences 第一条指出src/utils/mathMarkdown.ts是笔记公式的唯一权威解析/序列化桥。该模块对外导出五组 API分别对应往返的不同阶段// src/utils/mathMarkdown.ts export const MATH_INLINE_TYPE mathInline export const MATH_BLOCK_TYPE mathBlock const INLINE_TOKEN_PREFIX TOLARIA_MATH_INLINE: const BLOCK_TOKEN_PREFIX TOLARIA_MATH_BLOCK: const TOKEN_SUFFIX export function preProcessMathMarkdown({ markdown }: MarkdownSource): string // 源码 - 占位符 export function injectMathInBlocks(blocks: unknown[]): unknown[] // 占位符 - schema 节点 export function restoreMathInBlocks(blocks: unknown[]): unknown[] // 节点还原回文本 export function serializeMathAwareBlocks(editor: MarkdownSerializer, blocks: unknown[]): string export function renderMathToHtml({ latex, displayMode }: MathRenderRequest): string占位符的设计要点在于LaTeX 载荷的编码方式。源码中每个字符被转换为十六进制 code point 并以连字符连接mathMarkdown.tsfunction encodeLatex({ latex }: LatexPayload): string { return Array.from(latex, (char) char.codePointAt(0)?.toString(16) ?? ).join(-) } function decodeLatex({ encoded }: EncodedPayload): string { if (/^[0-9a-f-]$/iu.test(encoded)) { try { return encoded.split(-).map((part) String.fromCodePoint(Number.parseInt(part, 16))).join() } catch { return encoded } } try { return decodeURIComponent(encoded) } catch { return encoded } } function mathToken({ prefix, latex }: TokenRequest): string { return ${prefix}${encodeLatex({ latex })}${TOKEN_SUFFIX} }这样做有明确收益LaTeX 源码中的~、^、_、{}等字符在 Markdown 里都有歧义含义如~触发删除线、_触发斜体一旦编码成[0-9a-f-]字符集占位符在后续的 Markdown 解析中就是**惰性inert**的不会触发任何格式转换。测试文件 mathMarkdown.test.ts 专门固化了这条契约it(keeps math placeholder payloads inert for Markdown parsing, () { const preprocessed preProcessMathMarkdown({ markdown: Spacing math $x y ~ z$ stays math. }) expect(preprocessed).toContain(TOLARIA_MATH_INLINE:) expect(preprocessed).not.toContain(~) })即$x y ~ z$被替换为TOLARIA_MATH_INLINE:hex之后~不再裸露于 Markdown 流中。核心实现二公式识别的边界处理preProcessMathMarkdown是整个流水线的入口mathMarkdown.ts其扫描逻辑按行进行并有三条硬规则代码围栏内不动与~~~围栏开启后行内容原样透传直到围栏闭合行内代码不动逐字符扫描时反引号会切换inCodeSpan状态代码段内的$不会被识别为公式转义美元符不动isEscaped通过统计$前连续反斜杠数量的奇偶性来判断该$是否被转义被转义者不构成公式定界符。对行内公式$...$定界符必须满足isSingleDollar前后都不是$避免与$$冲突闭合$后紧跟字母或数字时也不成立防止把a$1$5这类货币文本误判为公式。对展示公式则支持两种形态均由readDisplayMath分派单行形式$$x^2$$正则^\$\$(.)\$\$$匹配整行跨行形式独占一行的$$起始向后查找下一个独占$$行中间各行以\n连接为 LaTeX 载荷readMultilineDisplayMathmathMarkdown.ts。金融文本防护启发式识别行内公式时最棘手的场景是业务笔记中大量的金额文本例如$24.59M、($884M)。模块中的isValidInlineLatex在非空、首尾无空格之外叠加了两道过滤function isValidInlineLatex({ latex }: LatexPayload): boolean { return Boolean(latex.trim()) !/^\s|\s$/.test(latex) !looksLikeFinancialProse({ latex }) looksLikeIntentionalMath({ latex }) }looksLikeIntentionalMath的判定优先级单个变量名x、y直接算数以疑似货币金额开头则直接排除含2x、3m_1这类系数-变量语法则算数随后要求存在数学符号-*/^_{}~之一或 LaTeX 命令\frac等最后若存在三个以上字母的纯英文单词则排除。looksLikeFinancialProse扫描数字 K/M/B/T/% 后缀 逗号/句号/右括号/空白的金额前缀且金额后跟随散文词时判定为金融文本而非公式。这套启发式由测试精确钉住mathMarkdown.test.tsit(does not treat financial prose between dollar amounts as inline math, () { const markdown FY2025 revenue grew 178.5% YoY to $24.59M, and gross margins improved dramatically from 63.0% to 82.6%. The company has a solid cash position ($884M) and a strategic focus. expect(preProcessMathMarkdown({ markdown })).toBe(markdown) }) it(keeps finance suffix amounts literal even when they are compact, () { const markdown Keep $2k$, $2M$, and $2%$ as finance prose. expect(preProcessMathMarkdown({ markdown })).toBe(markdown) })同时$22$、$x_i$、$2x$、$\frac{a}{b}$仍被正确识别为行内公式mathMarkdown.test.ts。这说明该启发式是保真优先、宁缺毋滥的取向误识别为公式的代价改写业务文本高于漏识别公式按纯文本展示。核心实现三BlockNote schema 节点与 KaTeX 安全渲染injectMathInBlocks负责把 BlockNote 解析出的 blocks 中的占位符文本转换为自定义节点行内占位符TOLARIA_MATH_INLINE:...被切分文本项替换为{ type: mathInline, props: { latex } }内联内容项expandInlineMath当某个 block 的唯一内容是TOLARIA_MATH_BLOCK:...文本时整个 block 被改写为{ type: mathBlock, props: { latex } }buildMathBlockmathMarkdown.ts表格单元格内容也会被递归处理transformTableContent所以| Formula |\n| --- |\n| $ab$ |这类表格内公式同样可往返mathMarkdown.test.ts。这两类节点在 BlockNote schema 中注册editorSchema.tsxexport const MathInline createReactInlineContentSpec( { type: MATH_INLINE_TYPE, propSchema: { latex: { default: } }, content: none, }, { render: (props) ( MathRender latex{props.inlineContent.props.latex} displayMode{false} / ), }, ) const MathBlock createReactBlockSpec( { type: MATH_BLOCK_TYPE, propSchema: { latex: { default: } }, content: none, }, { render: (props) MathBlockEditor block{props.block} editor{props.editor} /, }, )并随wikilink一起挂入 schemainlineContentSpecs增加mathInlineblockSpecs增加mathBlockeditorSchema.tsx。由于content: none公式节点是自包含的叶子节点LaTeX 源码只存在于props.latex一个属性中——这正是保存路径只需还原属性、无需遍历子节点的结构基础。渲染统一走MathRender组件editorSchema.tsxfunction MathRender({ latex, displayMode }: { latex: string; displayMode: boolean }) { const source displayMode ? $$\n${latex}\n$$ : $${latex}$ return ( SafeHtmlSpan aria-label{Math: ${latex}} className{displayMode ? math math--block : math math--inline} >export function renderMathToHtml({ latex, displayMode }: MathRenderRequest): string { try { return katex.renderToString(latex, { displayMode, throwOnError: false, trust: false, }) } catch { return escapeHtml({ text: latex }) } }即throwOnError: false与trust: false格式错误或不可信的公式保持可见而不是抛出异常破坏整篇笔记trust: false禁用\htmlClass等可执行 HTML 的宏能力即便 KaTeX 意外抛错也会降级为转义后的纯文本。渲染结果通过SafeHtmlSpanSafeMarkup.tsx受控注入且元素携带data-latex、title与aria-label——前者是后续交互扩展回查 LaTeX 的定位锚点后两者保证可访问性。KaTeX 依赖声明于 package.json 的katex: ^0.16.28。核心实现四富编辑器中的公式编辑交互公式节点在富编辑器中并非只读。editorSchema.tsx 的MathBlockEditor为mathBlock提供了双击进入源码编辑的体验双击渲染出的公式 → 替换为Textarea内容为props.latexCmd/Ctrl Enter提交编辑updateBlock(blockId, { props: { latex } })Escape取消并还原草稿提交时通过updateMathBlockLatexSafely包裹updateBlock若捕获到过期 block 引用错误则走统一的变换错误恢复通道reportRecoveredEditorTransformError避免编辑器进入不可恢复状态。另一方面mathInputExtension.ts 负责让用户直接键入公式输入时实时转换createMathInputTransform监听beforeinput当用户敲下空格或换行时调用readCompletedInlineMathAtEndmathMarkdown.ts检查光标前行文本是否恰好以一段完整的$...$结尾是则把该段文本用tr.replaceWith替换为mathInline节点并保留用户刚输入的尾随字符反方向转换光标选中mathInline节点后按Enter/F2handleMathKeyDown或对渲染出的行内公式双击handleRenderedMathDoubleClick都会把节点替换回字面量$latex$文本并把选区定位到 LaTeX 载荷内部——用户随后就能像在 plain 文本中一样直接修改公式源码再次触发上面的输入转换。这一渲染态 ↔ 源码态的双向切换还会上报math_source_edit_reopened遥测事件携带 keyboard/pointer 激活方式。往返流水线在保存路径中的位置公式编解码只是 Tolaria 更大的durable markdown编排中的一个环节。richEditorMarkdown.ts 展示了完整的读取方向顺序export function preProcessRichEditorMarkdown(markdown, vaultPath?, notePath?) { const withLiteralBackslashes preserveLiteralBackslashes(markdown) const withDurableBlocks preProcessDurableEditorMarkdown({ markdown: withLiteralBackslashes }) const withEmptyChecklists preProcessEmptyChecklistItems(withDurableBlocks) const withBlankQuotes preProcessBlankBlockquoteParagraphs(withEmptyChecklists) const withBlankParagraphs preProcessBlankParagraphs(withBlankQuotes) const withBareImages normalizeBareImageUrls(withBlankParagraphs) const withImages vaultPath ? resolveImageUrls(withBareImages, vaultPath, notePath) : withBareImages const withLinkedCode preProcessLinkedCodeMarkdown(withImages) const withWikilinks preProcessWikilinks(withLinkedCode) const withMath preProcessMathMarkdown({ markdown: withWikilinks }) return preProcessSingleTildeStrikethrough({ markdown: withMath }) }保存方向则由 editorDurableMarkdown.ts 编排serializeDurableEditorBlocks依次外包 file attachment、HTML/mermaid/tldraw 围栏、callout 与公式等序列化层。其中公式层的serializeCalloutAndMathAwareBlockseditorDurableMarkdown.ts与serializeMathAwareBlocksmathMarkdown.ts的策略一致遇到mathBlock就先把此前积累的普通 blocks 用restoreMathInBlocksblocksToMarkdownLossy刷出为 Markdown 片段然后直接输出$$\n{latex}\n$$字面量最后以空行连接各片段。行内公式则在restoreInlineMath中还原为$latex$文本后再交给 BlockNote 序列化器mathMarkdown.ts。这条链路与 wikilink、callout、mermaid、tldraw 等模块共同构成每个自定义内容都有成对的 preProcess/inject/serialize 函数的模式公式模块是 ABSTRACTIONS.md 中所描述抽象的落地实例之一。效果与约束结合 ADR 0082 的 Consequences 与上述源码可以归纳出该方案的运行时行为与边界富编辑器中看到 KaTeX 渲染结果raw 模式CodeMirror中看到的是原始$...$/$$...$$字面量——raw 模式是编辑精确公式源码最直接的途径Obsidian 风格导入的笔记保持可读因为落盘格式就是标准美元定界符文件在 Tolaria 之外Obsidian、VS Code、Pandoc 等同样可理解未来扩展点在同一个源码契约上公式编辑辅助功能可以构建在同一 Markdown 存储契约之上无需改动存储模型再评估条件也被写进 ADR只有当能够证明 Tiptap Mathematics 直接集成不引入自定义 lossy 行为、能完整保存 Tolaria 的 Markdown 保存路径时才会重新考虑直连方案。识别侧的已知取舍金融启发式$2k$、$884M)等优先保护业务文本代价是某些不常规的行内公式写法会按纯文本展示——这是由 mathMarkdown.test.ts 中多条回归测试显式固化的产品级行为而非 bug。小结占位符往返模式的可复用要点Tolaria 的公式方案对任何以 Markdown 文件为持久化载体、以 Block 编辑器为前端的项目都有直接参考价值其可复用的工程要点是编码占位符必须对下游解析器惰性——用无歧义字符集十六进制连字符承载载荷避免触发下游 Markdown 语法识别规则要有明确的否定测试——对易误判的领域文本此处为金额编写回归用例把不识别作为契约的一部分渲染层与存储层解耦——KaTeX 只负责把props.latex变成 HTMLthrowOnError: falsetrust: false 异常降级保证任何坏公式都不会击穿文档序列化时自定义节点旁路 lossy 序列化器——自定义 block 直接输出字面量 Markdown普通 blocks 才走blocksToMarkdownLossy保证往返字节级可控交互双向可逆——键入$...$自动生成节点、选中节点按 Enter/F2 或双击退回源码编辑体验闭环。所有关键证据路径汇总决策记录 docs/adr/0082-markdown-durable-math-notes.md解析/序列化桥 src/utils/mathMarkdown.ts 及其测试 src/utils/mathMarkdown.test.tsschema 与渲染组件 src/components/editorSchema.tsx输入交互扩展 src/components/mathInputExtension.ts 及测试 src/components/mathInputExtension.test.ts读写流水线 src/utils/richEditorMarkdown.ts 与 src/utils/editorDurableMarkdown.ts。【免费下载链接】tolariaDesktop app to manage markdown knowledge bases项目地址: https://gitcode.com/GitHub_Trending/to/tolaria创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网