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.
This commit is contained in:
375
notes/implementation.md
Normal file
375
notes/implementation.md
Normal file
@@ -0,0 +1,375 @@
|
||||
## 核心功能
|
||||
|
||||
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 格式:``
|
||||
- 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
|
||||
 → <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. **性能优化**:并行处理多个文件
|
||||
Reference in New Issue
Block a user