# VitePress Exporter 架构设计 ## 概述 VitePress Exporter 是一个 Obsidian 插件,用于将笔记和资源导出到 VitePress 格式。核心功能包括: - 导出 Markdown 文件到 `docs/` 目录 - 复制图片等资源到 `docs/assets/` 目录 - 将 Obsidian Canvas 渲染为图片 ## 技术栈 - **语言**: TypeScript - **构建工具**: esbuild - **包管理器**: pnpm - **Obsidian API**: Plugin, Workspace, TFile, Vault, MetadataCache ## 核心架构 ### 1. 导出流程 ``` 用户触发导出 ↓ exportCurrentNote() ↓ exportNote(file) ↓ processAssets(content, file) ├─→ 扫描嵌入和链接(使用 MetadataCache) ├─→ 处理 Canvas 文件 (captureCanvas) │ ├─→ 打开 Canvas 文件 │ ├─→ 获取 Canvas 对象 │ └─→ exportCanvasWithInternalAPI │ ├─→ 设置 zoom = 0 (100%) │ ├─→ 调用 takeScreenshot │ └─→ 保存图片 └─→ 复制资源文件 ├─→ 使用 binary 模式读取 └─→ 写入到 docs/assets/ ↓ updateLinksInContent() / updateCanvasLinksInContent() ↓ 写入 docs/ 目录(使用 binary 模式) ``` ### 2. 文件结构 ``` vitepress-exporter/ ├── src/ │ ├── main.ts # 主插件源代码 │ └── settings.ts # 设置相关代码 ├── manifest.json # 插件清单 ├── package.json # 项目配置 ├── esbuild.config.mjs # 构建配置 ├── tsconfig.json # TypeScript 配置 └── notes/ # 开发文档 ├── architecture.md # 本文档 ├── canvas-rendering.md # Canvas 渲染详细说明 └── flowchart.md # 流程图 ``` ### 3. 关键类和方法 #### VitePressExporterPlugin 主插件类,继承自 `Plugin`: ```typescript class VitePressExporterPlugin extends Plugin { settings: VitePressExporterSettings; // 生命周期 async onload() // 插件加载,注册命令和菜单 onunload() // 插件卸载 // 设置管理 async loadSettings() // 加载设置 async saveSettings() // 保存设置 // 导出功能 async exportCurrentNote() // 导出当前笔记 async exportNote(file) // 导出单个笔记 // 资源处理 async processAssets(content, file) // 处理资源文件和 Canvas // Canvas 渲染 async captureCanvas(canvasFile) // 捕获 Canvas 为图片 async exportCanvasWithInternalAPI(canvas, canvasFile) // 使用内部 API // 链接更新 updateLinksInContent(content, sourceFile, assetPath, newFileName) // 更新资源链接 updateCanvasLinksInContent(content, canvasFile, canvasPath, imageFileName) // 更新 Canvas 链接 } ``` ## Canvas 渲染架构 ### 核心决策 **使用 Obsidian Canvas 内部 API**,而不是自己实现渲染逻辑: - ✅ `canvas.takeScreenshot()` - 直接截图 - ✅ `canvas.getViewportBBox()` - 获取边界框 - ✅ `canvas.zoom` - 控制缩放(对数刻度) - ✅ `canvas.requestFrame()` - 触发重绘 **为什么不使用第三方库**: - ❌ `html-to-image` - 需要滑动窗口,代码复杂 - ❌ `dom-to-image-more` - 维护不活跃 - ✅ Obsidian 内部 API - 兼容性好,代码简洁 ### Zoom 对数刻度 **关键发现**:`canvas.zoom` 使用对数刻度(base 2) ```typescript zoom = 0 → scale = 2^0 = 1.0 (100%) zoom = 1 → scale = 2^1 = 2.0 (200%) zoom = -1 → scale = 2^-1 = 0.5 (50%) ``` **重要**:设置 `zoom = 0` 获得准确的 100% 缩放,不要设置 `zoom = 1.0`! ### 文件路径处理 `takeScreenshot` 需要绝对文件系统路径: ```typescript // 1. 获取 vault 的完整路径 const vaultBasePath = vault.adapter.basePath; // 2. 构建临时文件的绝对路径 const tempFullPath = `${vaultBasePath}/temp_canvas_${Date.now()}.png`; // 3. 调用 takeScreenshot(需要绝对路径) await canvas.takeScreenshot(tempFullPath, false); // 4. 使用相对路径读取(vault API) const imageBuffer = await vault.adapter.readBinary(tempFileName); // 5. 删除临时文件 await vault.adapter.remove(tempFileName); // 6. 写入到正确位置(相对路径) await vault.adapter.writeBinary(finalPath, imageBuffer); ``` ## 资源处理 ### 资源类型 1. **嵌入资源** (`![[image.png]]`) - 图片、PDF 等非 Markdown 文件 - 自动复制到 `docs/assets/` - 使用 binary 模式保留原始内容 2. **Canvas 文件** (`![[diagram.canvas]]`) - 使用内部 API 渲染为 PNG 图片 - 替换为 `` 3. **普通链接** (`[[other-file]]`) - Markdown 文件不复制 - 其他资源文件会被复制 ### 资源扫描机制 使用 Obsidian 的 `MetadataCache` API: ```typescript const cache = metadataCache.getCache(sourceFile.path); // 嵌入文件 if (cache.embeds) { for (const embed of cache.embeds) { const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path); // 检查是否是 Canvas 或其他资源 } } // 普通链接 if (cache.links) { for (const link of cache.links) { const resolvedLink = metadataCache.getFirstLinkpathDest(link.link, sourceFile.path); // 只复制非 Markdown 文件 } } ``` ### 路径更新 资源链接会自动更新为相对于 `docs/` 的路径: **Markdown 格式**: ```markdown 之前: ![](images/screenshot.png) 之后: ![](assets/screenshot.png) ``` **Wikilink 格式**: ```markdown 之前: ![[images/screenshot.png]] 之后: ![[assets/screenshot.png]] ``` **Canvas 特殊处理**: ```markdown 之前: ![[diagram.canvas]] 之后: ``` ### 路径解析策略 支持多种路径格式的智能匹配: ```typescript const possiblePaths = [ assetPath, // 完整路径 assetPath.substring(sourceDir.length + 1), // 相对路径 encodeURI(assetPath), // URL 编码 encodeURI(assetPath.substring(sourceDir.length + 1)), sourceFile.name, // 文件名 encodeURI(sourceFile.name) ]; ``` ## 配置 ### 插件设置 ```typescript interface VitePressExporterSettings { docsPath: string; // 默认: 'docs' assetsPath: string; // 默认: 'docs/assets' } ``` ### Obsidian 配置 插件通过 Obsidian 的设置界面配置: 1. 设置 → 社区插件 → VitePress Exporter → 选项 2. 配置 `Docs path` 和 `Assets path` 3. 保存设置 ## 文件读写 ### Binary 模式 所有文件操作都使用 binary 模式,以保留原始格式: ```typescript // 读取文件 const contentBuffer = await vault.readBinary(file); const content = new TextDecoder().decode(contentBuffer); // 写入文件 const updatedBuffer = new TextEncoder().encode(updatedContent); await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer); ``` **优势**: - 保留行尾空格 - 保留特殊字符 - 不修改原始格式 ## 错误处理 ### Canvas 渲染失败 当 Canvas 渲染失败时,会在控制台输出错误信息,但不会中断整个导出流程: ```typescript try { const renderedImage = await this.captureCanvas(canvasFile); if (renderedImage) { // 成功处理 } } catch (error) { console.error(`Failed to render canvas ${canvasPath}:`, error); } // 继续处理其他文件 ``` ### 资源复制失败 单个资源文件复制失败不会影响其他文件: ```typescript try { // 复制资源 } catch (error) { console.error(`Failed to copy asset ${assetPath}:`, error); } // 继续处理下一个资源 ``` ## 性能考虑 ### 优化策略 1. **使用 MetadataCache** - 快速获取文件的嵌入和链接信息 - 避免手动解析 Markdown 2. **批量处理** - 一次性收集所有需要处理的文件 - 批量复制和更新 3. **异步操作** - 所有文件操作都是异步的 - 不阻塞 UI 线程 4. **Binary 模式** - 减少字符串转换开销 - 直接操作二进制数据 ### 预期性能 - 小笔记(< 10 个资源):< 1 秒 - 中等笔记(10-50 个资源):< 3 秒 - 大笔记(> 50 个资源或多个 Canvas):< 10 秒 ## 触发方式 插件提供三种导出触发方式: 1. **命令面板** - `Export current note to VitePress` 2. **文件菜单** - 右键点击文件 → `Export to VitePress` 3. **编辑器菜单** - 右键点击编辑器 → `Export to VitePress` ## 未来扩展 ### 可能的功能 1. **批量导出** - 导出整个文件夹 - 导出所有笔记 2. **高级配置** - 自定义图片格式(PNG/JPEG/WebP) - 图片质量控制 - 自定义链接替换规则 3. **VitePress 集成** - 自动生成 frontmatter - 自动创建索引页面 - 集成 VitePress 构建流程 4. **更多资源类型** - 支持视频嵌入 - 支持音频文件 - 支持其他特殊格式 ## 维护指南 ### 修改导出逻辑 主要导出逻辑在 `exportNote()` 方法中: ```typescript async exportNote(file: TFile) { // 1. 读取文件(binary 模式) const contentBuffer = await vault.readBinary(file); const content = new TextDecoder().decode(contentBuffer); // 2. 处理资源 const updatedContent = await this.processAssets(content, file); // 3. 写入文件(binary 模式) const updatedBuffer = new TextEncoder().encode(updatedContent); await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer); } ``` ### 添加新的资源类型 在 `processAssets()` 方法中添加: ```typescript if (cache.embeds) { for (const embed of cache.embeds) { const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path); if (resolvedLink) { if (resolvedLink.extension === 'canvas') { // Canvas 处理 } else if (resolvedLink.extension === 'newtype') { // 处理新类型 } else { // 普通资源 } } } } ``` ### 调试技巧 1. **启用详细日志** ```typescript console.log('Processing assets for file:', sourceFile.path); console.log('Cache:', cache); console.log('Assets to copy:', Array.from(assetsToCopy)); ``` 2. **使用 Obsidian 开发者工具** - `Ctrl+Shift+I` 打开控制台 - 查看日志输出 3. **测试单个功能** - 创建最小测试用例 - 逐步验证每个步骤 ## 相关文档 - [Canvas 渲染详细说明](./canvas-rendering.md) - Canvas 渲染的实现细节 - [执行流程图](./flowchart.md) - 完整的导出流程图 - [Obsidian API 文档](https://github.com/obsidianmd/obsidian-api) - 官方 API