Files
odox/notes/implementation.md
nan f0ca3776f0
All checks were successful
Node.js build / build (20.x) (push) Successful in 21s
Node.js build / build (22.x) (push) Successful in 20s
feat: Implement Canvas file export functionality and enhance Markdown export process
- Added support for exporting `.canvas` files by rendering them to images and generating corresponding `.canvas.md` files.
- Updated the export logic to handle both Markdown and Canvas files, including file type checks and notifications.
- Improved resource management by ensuring assets are copied correctly and links are updated in the exported content.
- Refactored settings to change default export paths from 'docs' to 'wiki' for better organization.
- Created detailed implementation notes and flowcharts to document the export processes and core functionalities.
2026-04-21 22:06:37 +08:00

376 lines
12 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` 文件导出到 VitePress 的 docs 目录,自动处理资源文件
2. **Canvas 文件导出**:将 `.canvas` 文件渲染为图片并生成对应的 `.canvas.md` 文件
3. **资源文件管理**:自动复制和处理图片、Canvas 等资源文件
4. **路径规则配置**:支持多种目录结构和命名规则
## 主要流程
### 整体架构流程
```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 --> 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 --> 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. 生成包含 `<Viewer>` 组件的 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 模式保留原始格式
**分块渲染算法**:
```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
![](测试用.canvas) → <Viewer src="/测试用.canvas.png" />
![[测试用.canvas]] → <Viewer src="/测试用.canvas.png" />
```
## 配置选项
### settings.ts
```typescript
interface vitepressExporterSettings {
docsPath: string; // VitePress docs 目录路径,默认 'docs'
assetsPath: string; // 资源目录路径,默认 'docs/public'
nestNoteFolder: boolean; // 是否为每个笔记创建文件夹,默认 true
preserveFolderStructure: boolean; // 是否保留原始文件夹结构,默认 true
}
```
### 路径规则矩阵
| preserveFolderStructure | nestNoteFolder | 输入 | 输出 |
|---------|---------|------|------|
| true | true | notes/test.md | docs/notes/test/index.md |
| true | false | notes/test.md | docs/notes/test.md |
| false | true | notes/test.md | docs/test/index.md |
| false | false | notes/test.md | docs/test.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 过大
- **异步处理**:使用 `requestAnimationFrame` 确保渲染完成
- **缩放校正**:考虑系统 DPI 设置
- **内存管理**:及时释放临时对象(URL.revokeObjectURL)
## 文件结构
```
.obsidian/plugins/vitepress-exporter/
├── src/
│ ├── main.ts # 主插件逻辑(650 行)
│ └── settings.ts # 设置定义和 UI
├── notes/ # 文档和说明
├── manifest.json # 插件清单
└── package.json # 依赖管理
```
## 依赖项
- **Obsidian API**:插件框架
- **Electron**:Canvas 渲染(桌面版)
- **TypeScript**:类型安全
## 使用场景
1. **从 Obsidian 迁移到 VitePress**:将个人知识库转换为静态网站
2. **技术文档发布**:在 Obsidian 中编写,发布到 VitePress
3. **Canvas 可视化**:将 Obsidian Canvas 导出为交互式图片
4. **自动化工作流**:结合 CI/CD 实现自动部署
## 局限性
1. **平台依赖**:Canvas 渲染仅支持桌面版 Obsidian(需要 Electron)
2. **VitePress 特定**:生成的 `<Viewer>` 组件需要在 VitePress 中配置
3. **链接转换**:复杂的链接语法可能需要手动调整
4. **大型 Canvas**:分块渲染可能耗时较长
## 未来改进方向
1. **增量导出**:只导出修改过的文件
2. **批量导出**:支持一次性导出整个文件夹
3. **配置预设**:保存常用的导出配置
4. **错误恢复**:更完善的错误处理和回滚机制
5. **性能优化**:并行处理多个文件