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`
This commit is contained in:
411
notes/flowchart.md
Normal file
411
notes/flowchart.md
Normal file
@@ -0,0 +1,411 @@
|
||||
# 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
|
||||
- 增量导出(只处理修改的文件)
|
||||
Reference in New Issue
Block a user