- 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`
12 KiB
12 KiB
VitePress Exporter 执行流程
插件概述
VitePress Exporter 是一个 Obsidian 插件,用于将当前笔记导出到 VitePress 文档系统,包括资源文件管理和 Canvas 渲染功能。
完整执行流程图
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 格式:
之前: 
之后: 
Wikilink 格式:
之前: ![[path/to/image.png]]
之后: ![[assets/image.png]]
Canvas 特殊处理:
之前: ![[diagram.canvas]]
之后: <Viewer src="assets/diagram.png" />
路径匹配:
- 支持多种格式(绝对、相对、URL 编码)
- 使用正则表达式匹配和替换
- 转换为相对路径
5. 文件写入
Binary 模式:
- 使用
TextEncoder.encode()转换内容 - 调用
vault.adapter.writeBinary()写入 - 保留原始格式
关键方法说明
exportCurrentNote()
导出当前活动笔记
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)
执行导出逻辑
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)
处理资源文件
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 为图片
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 渲染失败
- 捕获异常并记录日志
- 继续处理其他文件
- 不中断整个导出流程
资源复制失败
- 捕获异常并记录日志
- 继续处理下一个资源
- 不影响其他文件
性能特点
优化策略
-
使用 MetadataCache
- 快速获取文件关系
- 避免手动解析
-
Binary 模式
- 减少字符串转换
- 保留原始格式
-
异步操作
- 所有文件操作异步
- 不阻塞 UI
-
错误隔离
- 单个文件失败不影响其他
- 继续处理后续文件
时间复杂度
- 文件读取: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)
未来改进方向
-
批量导出
- 导出整个文件夹
- 导出多个选中的文件
-
进度显示
- 显示导出进度条
- 显示当前处理的文件
-
更多配置
- 自定义图片格式
- 自定义链接转换规则
- 排除特定文件类型
-
性能优化
- 并行处理资源
- 缓存已渲染的 Canvas
- 增量导出(只处理修改的文件)