ID: NMP-BUG-001 标题: 在引入图片画廊功能后,引用块(Blockquote)在预览和复制内容中意外丢失 报告日期: 2025年6月19日 状态: 已定位根源 严重等级: 严重 (Blocker)
在为实现图片并排显示功能,对 src/markdown/parser.ts 文件中的 parse 方法进行重构后,插件出现了一个严重的渲染问题:所有的 Markdown 引用块(包括标准的 > 引用和 [!NOTE] 形式的 Callout)在插件的预览窗格中都无法显示,并且在使用“复制”功能时,这部分内容也会从最终的 HTML 中丢失。
- 在
src/markdown/parser.ts的MarkedParser类中,实现一个新的async parse方法。 - 在此方法中,将
marked.js的解析过程从单步调用await this.marked.parse(content)拆分为三步: a.let tokens = this.marked.lexer(content);b.tokens = this.processImageGalleries(tokens);(对 token 流进行自定义处理) c.let html = this.marked.parser(tokens); - 在 Obsidian 中加载此版本的插件。
- 创建一个包含标准引用块的 Markdown 文件,例如:
> 这是一段测试引用的文字。 一些其他文字。 - 打开插件的预览窗格。
预览窗格应正常渲染引用块的样式和内容。
预览窗格中仅显示“一些其他文字”,引用块部分完全丢失。
问题的根源在于对 marked.js 解析流程的误解。
marked.js 的完整解析流程包含三个主要阶段:Lexer(词法分析,将字符串转为Tokens)、walkTokens(遍历并预处理Tokens,执行异步钩子)和 Parser(将Tokens转换为HTML)。
-
当使用
marked.parse(string)进行一体化调用时,这三个阶段会依次完整执行。项目中Blockquote扩展的逻辑依赖于walkTokens钩子来异步预渲染其内容,并将结果存入token.html属性。 -
当为了插入自定义的
processImageGalleries逻辑而将流程手动拆分为lexer()和parser(tokens)时,直接调用parser(tokens)会跳过walkTokens阶段。 -
由于
walkTokens阶段被跳过,Blockquote扩展的异步渲染逻辑从未被触发。因此,当parser阶段执行到blockquotetoken 时,其token.html属性为undefined。 -
Blockquote扩展的renderer设计为直接返回token.html的值,因此它返回了undefined,导致该元素没有生成任何 HTML 输出,从而在最终的视图中“丢失”。
核心思路:既然必须拆分 lexer 和 parser,就需要手动将 walkTokens 的功能模拟并插入到流程中。
-
保持
src/markdown/blockquote.ts的walkTokens实现不变。该文件中的异步预渲染逻辑是正确的,问题在于它没有被调用。 -
修改
src/markdown/parser.ts中的parse方法,在lexer和parser调用之间,手动执行所有已注册扩展的walkTokens钩子。// src/markdown/parser.ts async parse(content: string) { if (!this.marked) await this.buildMarked(); await this.prepare(); // 1. Lexer let tokens = this.marked.lexer(content); // 2. 自定义Token处理 tokens = this.processImageGalleries(tokens); // 3. 手动执行 walkTokens 钩子 (关键修复) const walkers = []; for (const ext of this.extensions) { const markedExt = ext.markedExtension(); if (markedExt.walkTokens) { walkers.push(markedExt.walkTokens.bind(ext)); } } if (walkers.length > 0) { for (const token of tokens) { for (const walker of walkers) { await walker(token); } } } // 4. Parser (现在可以安全调用) let html = this.marked.parser(tokens); // 5. 后处理 html = await this.postprocess(html); return html; }