## 核心功能 1. **Markdown 文件导出**:将 `.md` 文件导出到静态文档站点目录,自动处理资源文件 2. **Canvas 文件导出**:将 `.canvas` 文件渲染为高清 PNG 图片并生成对应的 `.canvas.md` 文件 3. **资源文件管理**:自动复制和处理图片、Canvas 等资源文件到统一目录 4. **路径规则配置**:支持多种目录结构和命名规则,灵活配置输出路径 5. **透明背景支持**:Canvas 导出时可选择透明背景,移除 Obsidian 的点状背景图案 ## 主要流程 ### 整体架构流程 ```mermaid graph TB Start[用户触发导出] --> CheckType{检查文件类型} CheckType -->|Canvas 文件| CanvasFlow[Canvas 导出流程] CheckType -->|Markdown 文件| MarkdownFlow[Markdown 导出流程] CanvasFlow --> RenderCanvas[渲染 Canvas 为图片] RenderCanvas --> SaveImage[保存图片到 assetsPath] SaveImage --> GenerateCanvasMD[生成 .canvas.md 文件] GenerateCanvasMD --> WriteCanvas[写入文件] WriteCanvas --> End[完成] MarkdownFlow --> ReadContent[读取文件内容] ReadContent --> ProcessAssets[扫描并处理资源] ProcessAssets --> CollectLinks[收集链接和嵌入] CollectLinks --> Separate{分类资源} Separate -->|Canvas 文件| RenderCanvas Separate -->|普通资源| CopyAssets[复制资源到 assetsPath] CopyAssets --> UpdateLinks[更新内容中的链接] RenderCanvas --> UpdateCanvasLinks[更新 Canvas 链接为 Viewer 组件] UpdateCanvasLinks --> CalculatePath[计算输出路径] UpdateLinks --> CalculatePath CalculatePath --> ApplyRules{应用路径规则} ApplyRules --> PreserveFolder[preserveFolderStructure?] PreserveFolder -->|是| KeepStructure[保留文件夹结构] PreserveFolder -->|否| Flatten[扁平化] KeepStructure --> NestNote{nestNoteFolder?} Flatten --> NestNote NestNote -->|是| CreateIndex[创建 index.md] NestNote -->|否| UseFilename[使用原文件名] CreateIndex --> WriteMD[写入 Markdown 文件] UseFilename --> WriteMD WriteMD --> End ``` ### 资源处理流程 ```mermaid graph TB Start[processAssets 开始] --> GetCache[获取文件缓存] GetCache --> CheckCache{缓存存在?} CheckCache -->|否| ReturnContent[返回原内容] CheckCache -->|是| CollectStart[收集资源] CollectStart --> ScanEmbeds[扫描 embeds] ScanEmbeds --> CheckEmbed{是 Canvas?} CheckEmbed -->|是| AddCanvas[添加到 canvasFiles] CheckEmbed -->|否| AddAsset[添加到 assetsToCopy] AddCanvas --> ScanLinks[扫描 links] AddAsset --> ScanLinks ScanLinks --> CheckLink{是本地资源?} CheckLink -->|是| AddAsset CheckLink -->|否| ProcessCanvas ProcessCanvas[处理 Canvas 文件] --> RenderEach[遍历每个 Canvas] RenderEach --> CallRender[调用 renderCanvas] CallRender --> SavePNG[保存为 PNG] SavePNG --> UpdateCanvasLink[更新为 Viewer 组件] UpdateCanvasLink --> CopyAssets[复制普通资源] CopyAssets --> CopyEach[遍历每个资源] CopyEach --> ReadBinary[Binary 模式读取] ReadBinary --> WriteAsset[写入 assetsPath] WriteAsset --> UpdateAssetLink[更新资源链接] UpdateAssetLink --> ReturnContent[返回更新后的内容] ``` ### Canvas 渲染流程 ```mermaid graph TB Start[renderCanvas 开始] --> OpenFile[打开 Canvas 文件] OpenFile --> GetCanvas[获取 Canvas 对象] GetCanvas --> CheckCanvas{Canvas 存在?} CheckCanvas -->|否| ReturnNull[返回 null] CheckCanvas -->|是| CalcSystem[计算系统参数] CalcSystem --> GetZoom[获取缩放级别] GetZoom --> CalcPixel[计算设备像素比] CalcPixel --> CalcScale[计算缩放校正系数] CalcScale --> GetNodes[获取所有节点] GetNodes --> CalcBBox[计算边界框] CalcBBox --> CalcSize[计算宽度和高度] CalcSize --> CalcLimit{计算缩放限制} CalcLimit --> CheckLimit{超过 16284px?} CheckLimit -->|是| UseLimit[使用限制值] CheckLimit -->|否| UseZoom[使用原始缩放] UseLimit --> PrepareCanvas[准备 Canvas 环境] UseZoom --> PrepareCanvas PrepareCanvas --> DeselectAll[取消选择] DeselectAll --> AddScreenshotting[添加截图样式] AddScreenshotting --> CheckTransparent{透明背景?} CheckTransparent -->|是| AddTransparentClass[添加透明样式类] CheckTransparent -->|否| HideElements[隐藏页面元素] AddTransparentClass --> HideElements HideElements --> AppendToBody[附加到 body] AppendToBody --> SetPosition[设置位置] SetPosition --> CalcViewport[计算视口] CalcViewport --> SetCanvasZoom[设置 Canvas 缩放] SetCanvasZoom --> RequestFrame[请求帧] RequestFrame --> WaitRender[等待渲染完成] WaitRender --> CalcTiles[计算分块数量] CalcTiles --> CreateFinalCanvas[创建最终 Canvas] CreateFinalCanvas --> RenderLoop[渲染循环] RenderLoop --> SetViewport[设置视口位置] SetViewport --> RequestTileFrame[请求分块帧] RequestTileFrame --> WaitTile[等待渲染] WaitTile --> CapturePage[捕获页面] CapturePage --> ConvertToPNG[转换为 PNG] ConvertToPNG --> DrawToFinal[绘制到最终 Canvas] DrawToFinal --> CheckNext{还有分块?} CheckNext -->|是| SetViewport CheckNext -->|否| ConvertFinal[转换最终 Canvas] ConvertFinal --> ToBlob[转换为 Blob] ToBlob --> ToArrayBuffer[转换为 ArrayBuffer] ToArrayBuffer -> ToUint8Array[转换为 Uint8Array] ToUint8Array --> ReturnResult[返回结果] ReturnResult --> Cleanup[清理环境] Cleanup --> CheckTransparentCleanup{透明背景?} CheckTransparentCleanup -->|是| RestoreHiddenElements[恢复隐藏元素] CheckTransparentCleanup -->|否| RestoreStyles[恢复样式] RestoreHiddenElements --> RemoveBodyClasses[移除 body 类] RemoveBodyClasses --> RestoreStyles RestoreStyles --> ResetCanvas[重置 Canvas] ResetCanvas --> End[结束] ReturnNull --> End ``` ## 核心方法说明 ### 1. exportCurrentNote() **位置**:main.ts:75-89 **功能**:导出当前活动的文件 - 获取活动文件 - 验证文件类型(md 或 canvas) - 调用 exportNote() ### 2. exportNote() **位置**:main.ts:92-135 **功能**:导出单个文件的主入口 - 判断文件类型并分发到不同的处理逻辑 - Markdown 文件:读取内容 → 处理资源 → 计算路径 → 写入文件 - Canvas 文件:调用 exportCanvasFile() ### 3. exportCanvasFile() **位置**:main.ts:138-192 **功能**:专门处理 Canvas 文件导出 1. 渲染 Canvas 为 PNG 图片(支持透明背景) 2. 保存图片到 assetsPath(扁平化存储) 3. 计算 .canvas.md 文件路径 4. 使用配置的嵌入模板生成 Markdown 内容 5. 写入文件 **路径规则示例**: ```typescript // 保留文件夹 + 嵌套 notes/测试用.canvas → docs/notes/测试用.canvas/index.md // 仅嵌套 notes/测试用.canvas → docs/测试用.canvas/index.md // 保留文件夹 notes/测试用.canvas → docs/notes/测试用.canvas.md // 默认 notes/测试用.canvas → docs/测试用.canvas.md ``` ### 4. processAssets() **位置**:main.ts:195-308 **功能**:扫描并处理 Markdown 文件中的资源 **处理流程**: 1. 使用 metadataCache 获取文件的 embeds 和 links 2. 分类资源:Canvas 文件 vs 普通资源 3. 处理 Canvas:渲染为图片并更新链接为 `` 组件 4. 复制普通资源到 assetsPath 5. 更新内容中的所有资源链接 **链接格式支持**: - Markdown 格式:`![](path)` - Wikilink 格式:`![[path]]` ### 5. renderCanvas() **位置**:main.ts:368-556 **功能**:使用 Obsidian 内部 API 将 Canvas 渲染为 PNG 图片 **技术要点**: - 使用 Electron 的 `webContents.capturePage()` API - 分块渲染:处理大型 Canvas(避免超过 16284px 限制) - 缩放校正:考虑系统缩放和设备像素比 - 像素级精确:使用 binary 模式保留原始格式 - 透明背景:通过临时 CSS 类和 DOM 操作实现透明背景导出 - 智能隐藏:导出时临时隐藏页面其他元素,确保只捕获 Canvas 内容 **分块渲染算法**: ```typescript // 计算分块数量 const cols = Math.ceil((bbox.maxX - bbox.minX) / tileWidth); const rows = Math.ceil((bbox.maxY - bbox.minY) / tileHeight); // 逐个渲染分块并拼接 for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { // 设置视口位置 canvas.x = canvas.tx = startX + col * tileWidth; canvas.y = canvas.ty = startY + row * tileHeight; // 捕获并绘制 const capturedData = await webContents.capturePage(...); ctx.drawImage(img, col * O * s, row * F * s); } } ``` ### 6. updateLinksInContent() **位置**:main.ts:311-364 **功能**:更新内容中的资源链接 **智能匹配**: 尝试多种可能的路径格式: - 完整路径:`notes/image.png` - 相对路径:`image.png` - URL 编码:`notes%2Fimage.png` - 文件名:`image.png` **统一输出**: 所有资源链接转换为根路径:`/filename.png` ### 7. updateCanvasLinksInContent() **位置**:main.ts:593-648 **功能**:将 Canvas 链接转换为 `` 组件 **转换规则**: ```markdown // 默认模板:![]({url}) ![](测试用.canvas) → ![](/测试用.canvas.png) ![[测试用.canvas]] → ![](/测试用.canvas.png) // 自定义模板: ![](测试用.canvas) → ![[测试用.canvas]] → ``` ## 配置选项 ### settings.ts ```typescript interface odoxSettings { docsPath: string; // 文档站点目录路径,默认 'docs' assetsPath: string; // 资源目录路径,默认 'docs/public' nestNoteFolder: boolean; // 是否为每个笔记创建文件夹,默认 true preserveFolderStructure: boolean; // 是否保留原始文件夹结构,默认 true transparentCanvas: boolean; // 是否使用透明背景导出 Canvas,默认 true canvasEmbedTemplate: string; // Canvas 嵌入模板,默认 '![]({url})' } ``` ### 路径规则矩阵 | preserveFolderStructure | nestNoteFolder | Markdown 输入 | Markdown 输出 | Canvas 输入 | Canvas 输出 | |---------|---------|------|------|------|------| | true | true | notes/test.md | docs/notes/test/index.md | notes/test.canvas | docs/notes/test.canvas/index.md | | true | false | notes/test.md | docs/notes/test.md | notes/test.canvas | docs/notes/test.md | | false | true | notes/test.md | docs/test/index.md | notes/test.canvas | docs/test.canvas/index.md | | false | false | notes/test.md | docs/test.md | notes/test.canvas | docs/test.canvas.md | ## 技术实现要点 ### 1. Binary 模式文件操作 使用 binary 模式保留文件原始格式: ```typescript // 读取 const contentBuffer = await vault.readBinary(file); const content = new TextDecoder().decode(contentBuffer); // 写入 const buffer = new TextEncoder().encode(content); await vault.adapter.writeBinary(path, buffer.buffer); ``` ### 2. Obsidian 元数据缓存 使用 `metadataCache` 解析链接: ```typescript const cache = metadataCache.getCache(sourceFile.path); const resolvedLink = metadataCache.getFirstLinkpathDest(link, sourceFile.path); ``` ### 3. Electron API 集成 使用 Obsidian 内部的 Electron API: ```typescript const electron = window.electron?.remote; const webContents = electron?.getCurrentWebContents(); const capturedData = await webContents.capturePage({ x, y, width, height }); ``` ### 4. 链接解析兼容性 支持多种链接格式: - 绝对路径和相对路径 - URL 编码和未编码 - Markdown 标准格式和 Obsidian Wikilink ### 5. Canvas 渲染优化 - **分块渲染**:避免单个 Canvas 过大,超过 16284px 限制 - **异步处理**:使用 `requestAnimationFrame` 确保渲染完成 - **缩放校正**:考虑系统 DPI 设置,确保高清输出 - **内存管理**:及时释放临时对象(URL.revokeObjectURL) - **透明背景**:通过 CSS 类和 DOM 操作实现透明背景导出 - **智能隐藏**:导出时临时隐藏页面其他元素,确保只捕获 Canvas 内容 ## 文件结构 ``` .obsidian/plugins/odox/ ├── src/ │ ├── main.ts # 主插件逻辑(约 680 行) │ └── settings.ts # 设置定义和 UI ├── notes/ # 文档和说明 │ └── implementation.md # 实现细节文档 ├── manifest.json # 插件清单 ├── package.json # 依赖管理 └── main.js # 编译后的插件文件 ``` ## 依赖项 - **Obsidian API**:插件框架 - **Electron**:Canvas 渲染(桌面版) - **TypeScript**:类型安全 ## 使用场景 1. **从 Obsidian 迁移到静态站点**:将个人知识库转换为 VitePress、Docusaurus 等静态网站 2. **技术文档发布**:在 Obsidian 中编写,发布到文档站点 3. **Canvas 可视化**:将 Obsidian Canvas 导出为高清 PNG 图片 4. **自动化工作流**:结合 CI/CD 实现自动部署文档站点 ## 技术优势 1. **高质量渲染**:使用 Obsidian 原生渲染引擎,确保导出内容与编辑器中一致 2. **智能资源管理**:自动识别、复制和更新资源文件引用 3. **灵活配置**:支持多种目录结构和命名规则,适应不同的项目需求 4. **透明背景支持**:Canvas 导出时可移除背景图案,适合嵌入各种主题的网站 5. **分块渲染技术**:处理大型 Canvas 文件,避免尺寸限制 ## 未来改进方向 1. **增量导出**:只导出修改过的文件,提高大项目的导出效率 2. **批量导出**:支持一次性导出整个文件夹或整个项目 3. **配置预设**:保存常用的导出配置,快速切换不同项目的导出规则 4. **错误恢复**:更完善的错误处理和回滚机制,导出失败时恢复原状态 5. **性能优化**:并行处理多个文件,利用多核 CPU 加速导出过程 6. **进度显示**:为批量导出操作提供进度条和详细的状态信息 7. **导出历史**:记录导出历史,支持撤销和回滚操作