Files
odox/notes/flowchart.md
nan 41db190fee
Some checks failed
Node.js build / build (20.x) (push) Failing after 18m32s
Node.js build / build (22.x) (push) Has been cancelled
feat: Implement Canvas rendering system and VitePress exporter plugin
- 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`
2026-04-20 22:23:23 +08:00

412 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.
# 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
之前: ![](path/to/image.png)
之后: ![](assets/image.png)
```
**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
- 增量导出(只处理修改的文件)