Files
odox/notes/implementation.md

401 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 核心功能
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:渲染为图片并更新链接为 `<Viewer>` 组件
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 链接转换为 `<Viewer>` 组件
**转换规则**:
```markdown
// 默认模板:![]({url})
![](测试用.canvas) → ![](/测试用.canvas.png)
![[测试用.canvas]] → ![](/测试用.canvas.png)
// 自定义模板:<Viewer src="{url}" />
![](测试用.canvas) → <Viewer src="/测试用.canvas.png" />
![[测试用.canvas]] → <Viewer src="/测试用.canvas.png" />
```
## 配置选项
### 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. **导出历史**:记录导出历史,支持撤销和回滚操作