- Add Canvas rendering implementation in `notes/render-canvas.md` - Create VitePress exporter plugin in `src/main.ts` with commands for exporting notes - Implement settings management for the plugin in `src/settings.ts` - Add CSS file for potential styling needs - Create version bump script in `version-bump.mjs` for managing versioning - Initialize version tracking in `versions.json`
412 lines
12 KiB
Markdown
412 lines
12 KiB
Markdown
# VitePress Exporter 执行流程
|
||
|
||
## 插件概述
|
||
|
||
VitePress Exporter 是一个 Obsidian 插件,用于将当前笔记导出到 VitePress 文档系统,包括资源文件管理和 Canvas 渲染功能。
|
||
|
||
## 完整执行流程图
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
Start([用户触发导出]) --> CheckActiveFile{检查当前文件}
|
||
CheckActiveFile -->|没有活动文件| ShowError1[显示错误提示]
|
||
CheckActiveFile -->|不是 md 文件| ShowError2[显示错误提示]
|
||
CheckActiveFile -->|是 md 文件| ReadFile[读取文件内容<br/>使用 binary 模式]
|
||
|
||
ReadFile --> ProcessAssets[处理资源文件<br/>processAssets]
|
||
ProcessAssets --> GetCache[获取文件元数据缓存<br/>metadataCache.getCache]
|
||
|
||
GetCache --> CheckCache{缓存是否存在}
|
||
CheckCache -->|否| ReturnContent[返回原内容]
|
||
CheckCache -->|是| InitializeSets[初始化集合<br/>assetsToCopy, canvasFiles]
|
||
|
||
InitializeSets --> ScanEmbeds[扫描嵌入文件<br/>cache.embeds]
|
||
ScanEmbeds --> CheckEmbed{有嵌入文件?}
|
||
CheckEmbed -->|否| ScanLinks
|
||
CheckEmbed -->|是| ResolveEmbed[解析嵌入路径<br/>getFirstLinkpathDest]
|
||
|
||
ResolveEmbed --> CheckCanvas{是 canvas 文件?}
|
||
CheckCanvas -->|是| AddToCanvasSet[添加到 canvasFiles]
|
||
CheckCanvas -->|否| AddToAssetsSet[添加到 assetsToCopy]
|
||
AddToCanvasSet --> ScanEmbeds
|
||
AddToAssetsSet --> ScanEmbeds
|
||
|
||
ScanEmbeds --> ScanLinks[扫描链接文件<br/>cache.links]
|
||
ScanLinks --> CheckLink{有链接文件?}
|
||
CheckLink -->|否| ProcessCanvasFiles
|
||
CheckLink -->|是| ResolveLink[解析链接路径<br/>getFirstLinkpathDest]
|
||
|
||
ResolveLink --> CheckMD{是 md 文件?}
|
||
CheckMD -->|是| SkipLink[跳过]
|
||
CheckMD -->|否| AddToAssetsSet
|
||
SkipLink --> ScanLinks
|
||
AddToAssetsSet --> ScanLinks
|
||
|
||
ScanLinks --> ProcessCanvasFiles[处理 Canvas 文件]
|
||
ProcessCanvasFiles --> CheckCanvasEmpty{有 Canvas 文件?}
|
||
|
||
CheckCanvasEmpty -->|否| ProcessAssetFiles
|
||
CheckCanvasEmpty -->|是| GetCanvasFile[获取 Canvas 文件对象]
|
||
|
||
GetCanvasFile --> CaptureCanvas[captureCanvas]
|
||
CaptureCanvas --> OpenFile[打开 Canvas 文件<br/>activeLeaf.openFile]
|
||
|
||
OpenFile --> GetCanvas[获取 Canvas 对象<br/>activeLeaf.view.canvas]
|
||
GetCanvas --> CheckCanvasObj{Canvas 对象存在?}
|
||
|
||
CheckCanvasObj -->|否| NextCanvas
|
||
CheckCanvasObj -->|是| ExportInternalAPI[exportCanvasWithInternalAPI]
|
||
|
||
ExportInternalAPI --> GetBBox[获取视口边界框<br/>getViewportBBox]
|
||
GetBBox --> SetZoom[设置 zoom = 0<br/>100% 缩放]
|
||
|
||
SetZoom --> RequestFrame[请求重绘<br/>requestFrame]
|
||
RequestFrame --> WaitRender[等待渲染<br/>100ms + rAF]
|
||
|
||
WaitRender --> TakeScreenshot[调用 takeScreenshot<br/>使用绝对路径]
|
||
TakeScreenshot --> WaitWrite[等待文件写入<br/>100ms]
|
||
|
||
WaitWrite --> ReadTemp[读取临时文件<br/>readBinary]
|
||
ReadTemp --> CheckBuffer{读取成功?}
|
||
|
||
CheckBuffer -->|否| LogError1[记录错误]
|
||
CheckBuffer -->|是| RemoveTemp[删除临时文件]
|
||
|
||
RemoveTemp --> WriteFinal[写入最终位置<br/>writeBinary]
|
||
WriteFinal --> UpdateCanvasLinks[更新 Canvas 链接<br/>updateCanvasLinksInContent]
|
||
|
||
UpdateCanvasLinks --> NextCanvas{还有 Canvas?}
|
||
LogError1 --> NextCanvas
|
||
NextCanvas -->|是| GetCanvasFile
|
||
NextCanvas -->|否| ProcessAssetFiles
|
||
|
||
subgraph 资源复制流程
|
||
ProcessAssetFiles[处理资源文件] --> CheckAssets{有资源文件?}
|
||
CheckAssets -->|否| WriteDocs
|
||
CheckAssets -->|是| GetAssetFile[获取资源文件对象]
|
||
|
||
GetAssetFile --> ReadAsset[读取资源文件<br/>readBinary binary 模式]
|
||
ReadAsset --> MkdirAssets[创建 assets 目录<br/>mkdir]
|
||
|
||
MkdirAssets --> WriteAsset[写入到 docs/assets<br/>writeBinary binary 模式]
|
||
WriteAsset --> UpdateLinks[更新资源链接<br/>updateLinksInContent]
|
||
|
||
UpdateLinks --> NextAsset{还有资源?}
|
||
NextAsset -->|是| GetAssetFile
|
||
NextAsset -->|否| WriteDocs
|
||
end
|
||
|
||
WriteDocs[写入到 docs 目录<br/>writeBinary binary 模式] --> Success[显示成功提示]
|
||
Success --> End([结束])
|
||
|
||
ShowError1 --> End
|
||
ShowError2 --> End
|
||
ReturnContent --> WriteDocs
|
||
|
||
style Start fill:#e1f5e1
|
||
style End fill:#ffe1e1
|
||
style Success fill:#e1f5e1
|
||
style ShowError1 fill:#ffe1e1
|
||
style ShowError2 fill:#ffe1e1
|
||
style ProcessCanvasFiles fill:#fff4e1
|
||
style ProcessAssetFiles fill:#e1f0ff
|
||
style ExportInternalAPI fill:#ffe1ff
|
||
```
|
||
|
||
## 核心功能模块
|
||
|
||
### 1. 导出流程
|
||
|
||
**触发方式**:
|
||
- 命令面板:`Export current note to VitePress`
|
||
- 文件右键菜单:`Export to VitePress`
|
||
- 编辑器右键菜单:`Export to VitePress`
|
||
|
||
**文件验证**:
|
||
- 检查活动文件是否存在
|
||
- 验证文件扩展名是否为 `md`
|
||
|
||
**内容读取**:
|
||
- 使用 `vault.readBinary()` 读取文件
|
||
- 保留原始格式(包括行尾空格)
|
||
|
||
### 2. 资源处理
|
||
|
||
**扫描机制**:
|
||
- 使用 `metadataCache.getCache()` 获取文件元数据
|
||
- 扫描 `cache.embeds` 获取嵌入文件
|
||
- 扫描 `cache.links` 获取链接文件
|
||
|
||
**文件分类**:
|
||
- Canvas 文件:扩展名为 `.canvas`
|
||
- 普通资源:非 Markdown 文件
|
||
- Markdown 文件:不复制
|
||
|
||
**路径解析**:
|
||
- 使用 `metadataCache.getFirstLinkpathDest()` 解析路径
|
||
- 支持相对路径和绝对路径
|
||
|
||
### 3. Canvas 渲染
|
||
|
||
**打开文件**:
|
||
- 使用 `workspace.getLeaf()` 获取 leaf
|
||
- 调用 `activeLeaf.openFile()` 打开 Canvas
|
||
- 从 `activeLeaf.view.canvas` 获取 Canvas 对象
|
||
|
||
**渲染过程**:
|
||
- 获取视口边界框 `getViewportBBox()`
|
||
- 设置 `zoom = 0`(100% 缩放)
|
||
- 请求重绘 `requestFrame()`
|
||
- 等待渲染完成(100ms + requestAnimationFrame)
|
||
- 调用 `takeScreenshot(绝对路径)` 截图
|
||
|
||
**文件处理**:
|
||
- 保存到临时文件(绝对路径)
|
||
- 读取临时文件(相对路径)
|
||
- 删除临时文件
|
||
- 写入到最终位置
|
||
|
||
### 4. 链接更新
|
||
|
||
**Markdown 格式**:
|
||
```markdown
|
||
之前: 
|
||
之后: 
|
||
```
|
||
|
||
**Wikilink 格式**:
|
||
```markdown
|
||
之前: ![[path/to/image.png]]
|
||
之后: ![[assets/image.png]]
|
||
```
|
||
|
||
**Canvas 特殊处理**:
|
||
```markdown
|
||
之前: ![[diagram.canvas]]
|
||
之后: <Viewer src="assets/diagram.png" />
|
||
```
|
||
|
||
**路径匹配**:
|
||
- 支持多种格式(绝对、相对、URL 编码)
|
||
- 使用正则表达式匹配和替换
|
||
- 转换为相对路径
|
||
|
||
### 5. 文件写入
|
||
|
||
**Binary 模式**:
|
||
- 使用 `TextEncoder.encode()` 转换内容
|
||
- 调用 `vault.adapter.writeBinary()` 写入
|
||
- 保留原始格式
|
||
|
||
## 关键方法说明
|
||
|
||
### exportCurrentNote()
|
||
导出当前活动笔记
|
||
|
||
```typescript
|
||
async exportCurrentNote() {
|
||
const activeFile = this.app.workspace.getActiveFile();
|
||
if (!activeFile) {
|
||
new Notice('No active file found');
|
||
return;
|
||
}
|
||
if (activeFile.extension !== 'md') {
|
||
new Notice('Current file is not a markdown file');
|
||
return;
|
||
}
|
||
await this.exportNote(activeFile);
|
||
}
|
||
```
|
||
|
||
### exportNote(file)
|
||
执行导出逻辑
|
||
|
||
```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 docsPath = `${this.settings.docsPath}/${file.name}`;
|
||
const updatedBuffer = new TextEncoder().encode(updatedContent);
|
||
await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer);
|
||
}
|
||
```
|
||
|
||
### processAssets(content, file)
|
||
处理资源文件
|
||
|
||
```typescript
|
||
async processAssets(content: string, sourceFile: TFile): Promise<string> {
|
||
const cache = metadataCache.getCache(sourceFile.path);
|
||
if (!cache) return content;
|
||
|
||
const assetsToCopy = new Set<string>();
|
||
const canvasFiles = new Set<string>();
|
||
|
||
// 收集嵌入文件
|
||
if (cache.embeds) {
|
||
for (const embed of cache.embeds) {
|
||
const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path);
|
||
if (resolvedLink) {
|
||
if (resolvedLink.extension === 'canvas') {
|
||
canvasFiles.add(resolvedLink.path);
|
||
} else {
|
||
assetsToCopy.add(resolvedLink.path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收集链接文件
|
||
if (cache.links) {
|
||
for (const link of cache.links) {
|
||
const resolvedLink = metadataCache.getFirstLinkpathDest(link.link, sourceFile.path);
|
||
if (resolvedLink && resolvedLink.extension !== 'md') {
|
||
assetsToCopy.add(resolvedLink.path);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理 Canvas 文件
|
||
for (const canvasPath of canvasFiles) {
|
||
const renderedImage = await this.captureCanvas(canvasFile);
|
||
if (renderedImage) {
|
||
// 写入并更新链接
|
||
}
|
||
}
|
||
|
||
// 复制资源文件
|
||
for (const assetPath of assetsToCopy) {
|
||
const assetContent = await vault.readBinary(sourceFile);
|
||
await vault.adapter.writeBinary(destPath, assetContent);
|
||
content = this.updateLinksInContent(content, ...);
|
||
}
|
||
|
||
return content;
|
||
}
|
||
```
|
||
|
||
### captureCanvas(canvasFile)
|
||
捕获 Canvas 为图片
|
||
|
||
```typescript
|
||
async captureCanvas(canvasFile: TFile): Promise<Uint8Array | null> {
|
||
const activeLeaf = this.app.workspace.getLeaf();
|
||
await activeLeaf.openFile(canvasFile);
|
||
|
||
const canvas = activeLeaf.view.canvas;
|
||
if (!canvas) return null;
|
||
|
||
return await this.exportCanvasWithInternalAPI(canvas, canvasFile);
|
||
}
|
||
```
|
||
|
||
## 数据流图
|
||
|
||
```
|
||
用户触发
|
||
↓
|
||
exportCurrentNote()
|
||
↓
|
||
exportNote(file)
|
||
↓
|
||
readBinary(file) → content
|
||
↓
|
||
processAssets(content, file)
|
||
├─→ metadataCache.getCache() → cache
|
||
├─→ 收集文件 → canvasFiles, assetsToCopy
|
||
├─→ 处理 Canvas
|
||
│ ├─→ captureCanvas()
|
||
│ ├─→ takeScreenshot()
|
||
│ └─→ updateCanvasLinksInContent()
|
||
└─→ 复制资源
|
||
├─→ readBinary()
|
||
├─→ writeBinary()
|
||
└─→ updateLinksInContent()
|
||
↓
|
||
updatedContent
|
||
↓
|
||
TextEncoder.encode() → buffer
|
||
↓
|
||
writeBinary(docsPath, buffer)
|
||
↓
|
||
完成
|
||
```
|
||
|
||
## 错误处理
|
||
|
||
### 文件验证失败
|
||
- 没有活动文件 → 显示 Notice
|
||
- 文件不是 Markdown → 显示 Notice
|
||
|
||
### Canvas 渲染失败
|
||
- 捕获异常并记录日志
|
||
- 继续处理其他文件
|
||
- 不中断整个导出流程
|
||
|
||
### 资源复制失败
|
||
- 捕获异常并记录日志
|
||
- 继续处理下一个资源
|
||
- 不影响其他文件
|
||
|
||
## 性能特点
|
||
|
||
### 优化策略
|
||
|
||
1. **使用 MetadataCache**
|
||
- 快速获取文件关系
|
||
- 避免手动解析
|
||
|
||
2. **Binary 模式**
|
||
- 减少字符串转换
|
||
- 保留原始格式
|
||
|
||
3. **异步操作**
|
||
- 所有文件操作异步
|
||
- 不阻塞 UI
|
||
|
||
4. **错误隔离**
|
||
- 单个文件失败不影响其他
|
||
- 继续处理后续文件
|
||
|
||
### 时间复杂度
|
||
|
||
- 文件读取:O(n)
|
||
- 资源扫描:O(e + l) 其中 e=嵌入数,l=链接数
|
||
- Canvas 渲染:O(c) 其中 c=Canvas 数量
|
||
- 资源复制:O(a) 其中 a=资源数量
|
||
|
||
总体:O(n + e + l + c + a)
|
||
|
||
## 配置选项
|
||
|
||
用户可以在插件设置中配置:
|
||
|
||
- **docsPath**: 导出文档目录(默认: `docs`)
|
||
- **assetsPath**: 资源文件目录(默认: `docs/assets`)
|
||
|
||
## 未来改进方向
|
||
|
||
1. **批量导出**
|
||
- 导出整个文件夹
|
||
- 导出多个选中的文件
|
||
|
||
2. **进度显示**
|
||
- 显示导出进度条
|
||
- 显示当前处理的文件
|
||
|
||
3. **更多配置**
|
||
- 自定义图片格式
|
||
- 自定义链接转换规则
|
||
- 排除特定文件类型
|
||
|
||
4. **性能优化**
|
||
- 并行处理资源
|
||
- 缓存已渲染的 Canvas
|
||
- 增量导出(只处理修改的文件)
|