feat: Implement Canvas rendering system and VitePress exporter plugin
Some checks failed
Node.js build / build (20.x) (push) Failing after 18m32s
Node.js build / build (22.x) (push) Has been cancelled

- 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:
2026-04-20 22:23:23 +08:00
parent e54a8a3b05
commit 41db190fee
27 changed files with 5613 additions and 4912 deletions

424
notes/architecture.md Normal file
View File

@@ -0,0 +1,424 @@
# VitePress Exporter 架构设计
## 概述
VitePress Exporter 是一个 Obsidian 插件,用于将笔记和资源导出到 VitePress 格式。核心功能包括:
- 导出 Markdown 文件到 `docs/` 目录
- 复制图片等资源到 `docs/assets/` 目录
- 将 Obsidian Canvas 渲染为图片
## 技术栈
- **语言**: TypeScript
- **构建工具**: esbuild
- **包管理器**: pnpm
- **Obsidian API**: Plugin, Workspace, TFile, Vault, MetadataCache
## 核心架构
### 1. 导出流程
```
用户触发导出
↓
exportCurrentNote()
↓
exportNote(file)
↓
processAssets(content, file)
├─→ 扫描嵌入和链接(使用 MetadataCache)
├─→ 处理 Canvas 文件 (captureCanvas)
│ ├─→ 打开 Canvas 文件
│ ├─→ 获取 Canvas 对象
│ └─→ exportCanvasWithInternalAPI
│ ├─→ 设置 zoom = 0 (100%)
│ ├─→ 调用 takeScreenshot
│ └─→ 保存图片
└─→ 复制资源文件
├─→ 使用 binary 模式读取
└─→ 写入到 docs/assets/
↓
updateLinksInContent() / updateCanvasLinksInContent()
↓
写入 docs/ 目录(使用 binary 模式)
```
### 2. 文件结构
```
vitepress-exporter/
├── src/
│ ├── main.ts # 主插件源代码
│ └── settings.ts # 设置相关代码
├── manifest.json # 插件清单
├── package.json # 项目配置
├── esbuild.config.mjs # 构建配置
├── tsconfig.json # TypeScript 配置
└── notes/ # 开发文档
├── architecture.md # 本文档
├── canvas-rendering.md # Canvas 渲染详细说明
└── flowchart.md # 流程图
```
### 3. 关键类和方法
#### VitePressExporterPlugin
主插件类,继承自 `Plugin`:
```typescript
class VitePressExporterPlugin extends Plugin {
settings: VitePressExporterSettings;
// 生命周期
async onload() // 插件加载,注册命令和菜单
onunload() // 插件卸载
// 设置管理
async loadSettings() // 加载设置
async saveSettings() // 保存设置
// 导出功能
async exportCurrentNote() // 导出当前笔记
async exportNote(file) // 导出单个笔记
// 资源处理
async processAssets(content, file) // 处理资源文件和 Canvas
// Canvas 渲染
async captureCanvas(canvasFile) // 捕获 Canvas 为图片
async exportCanvasWithInternalAPI(canvas, canvasFile) // 使用内部 API
// 链接更新
updateLinksInContent(content, sourceFile, assetPath, newFileName) // 更新资源链接
updateCanvasLinksInContent(content, canvasFile, canvasPath, imageFileName) // 更新 Canvas 链接
}
```
## Canvas 渲染架构
### 核心决策
**使用 Obsidian Canvas 内部 API**,而不是自己实现渲染逻辑:
- ✅ `canvas.takeScreenshot()` - 直接截图
- ✅ `canvas.getViewportBBox()` - 获取边界框
- ✅ `canvas.zoom` - 控制缩放(对数刻度)
- ✅ `canvas.requestFrame()` - 触发重绘
**为什么不使用第三方库**:
- ❌ `html-to-image` - 需要滑动窗口,代码复杂
- ❌ `dom-to-image-more` - 维护不活跃
- ✅ Obsidian 内部 API - 兼容性好,代码简洁
### Zoom 对数刻度
**关键发现**:`canvas.zoom` 使用对数刻度(base 2)
```typescript
zoom = 0 → scale = 2^0 = 1.0 (100%)
zoom = 1 → scale = 2^1 = 2.0 (200%)
zoom = -1 → scale = 2^-1 = 0.5 (50%)
```
**重要**:设置 `zoom = 0` 获得准确的 100% 缩放,不要设置 `zoom = 1.0`!
### 文件路径处理
`takeScreenshot` 需要绝对文件系统路径:
```typescript
// 1. 获取 vault 的完整路径
const vaultBasePath = vault.adapter.basePath;
// 2. 构建临时文件的绝对路径
const tempFullPath = `${vaultBasePath}/temp_canvas_${Date.now()}.png`;
// 3. 调用 takeScreenshot(需要绝对路径)
await canvas.takeScreenshot(tempFullPath, false);
// 4. 使用相对路径读取(vault API)
const imageBuffer = await vault.adapter.readBinary(tempFileName);
// 5. 删除临时文件
await vault.adapter.remove(tempFileName);
// 6. 写入到正确位置(相对路径)
await vault.adapter.writeBinary(finalPath, imageBuffer);
```
## 资源处理
### 资源类型
1. **嵌入资源** (`![[image.png]]`)
- 图片、PDF 等非 Markdown 文件
- 自动复制到 `docs/assets/`
- 使用 binary 模式保留原始内容
2. **Canvas 文件** (`![[diagram.canvas]]`)
- 使用内部 API 渲染为 PNG 图片
- 替换为 `<Viewer src="assets/diagram.png" />`
3. **普通链接** (`[[other-file]]`)
- Markdown 文件不复制
- 其他资源文件会被复制
### 资源扫描机制
使用 Obsidian 的 `MetadataCache` API:
```typescript
const cache = metadataCache.getCache(sourceFile.path);
// 嵌入文件
if (cache.embeds) {
for (const embed of cache.embeds) {
const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path);
// 检查是否是 Canvas 或其他资源
}
}
// 普通链接
if (cache.links) {
for (const link of cache.links) {
const resolvedLink = metadataCache.getFirstLinkpathDest(link.link, sourceFile.path);
// 只复制非 Markdown 文件
}
}
```
### 路径更新
资源链接会自动更新为相对于 `docs/` 的路径:
**Markdown 格式**:
```markdown
之前: ![](images/screenshot.png)
之后: ![](assets/screenshot.png)
```
**Wikilink 格式**:
```markdown
之前: ![[images/screenshot.png]]
之后: ![[assets/screenshot.png]]
```
**Canvas 特殊处理**:
```markdown
之前: ![[diagram.canvas]]
之后: <Viewer src="assets/diagram.png" />
```
### 路径解析策略
支持多种路径格式的智能匹配:
```typescript
const possiblePaths = [
assetPath, // 完整路径
assetPath.substring(sourceDir.length + 1), // 相对路径
encodeURI(assetPath), // URL 编码
encodeURI(assetPath.substring(sourceDir.length + 1)),
sourceFile.name, // 文件名
encodeURI(sourceFile.name)
];
```
## 配置
### 插件设置
```typescript
interface VitePressExporterSettings {
docsPath: string; // 默认: 'docs'
assetsPath: string; // 默认: 'docs/assets'
}
```
### Obsidian 配置
插件通过 Obsidian 的设置界面配置:
1. 设置 → 社区插件 → VitePress Exporter → 选项
2. 配置 `Docs path` 和 `Assets path`
3. 保存设置
## 文件读写
### Binary 模式
所有文件操作都使用 binary 模式,以保留原始格式:
```typescript
// 读取文件
const contentBuffer = await vault.readBinary(file);
const content = new TextDecoder().decode(contentBuffer);
// 写入文件
const updatedBuffer = new TextEncoder().encode(updatedContent);
await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer);
```
**优势**:
- 保留行尾空格
- 保留特殊字符
- 不修改原始格式
## 错误处理
### Canvas 渲染失败
当 Canvas 渲染失败时,会在控制台输出错误信息,但不会中断整个导出流程:
```typescript
try {
const renderedImage = await this.captureCanvas(canvasFile);
if (renderedImage) {
// 成功处理
}
} catch (error) {
console.error(`Failed to render canvas ${canvasPath}:`, error);
}
// 继续处理其他文件
```
### 资源复制失败
单个资源文件复制失败不会影响其他文件:
```typescript
try {
// 复制资源
} catch (error) {
console.error(`Failed to copy asset ${assetPath}:`, error);
}
// 继续处理下一个资源
```
## 性能考虑
### 优化策略
1. **使用 MetadataCache**
- 快速获取文件的嵌入和链接信息
- 避免手动解析 Markdown
2. **批量处理**
- 一次性收集所有需要处理的文件
- 批量复制和更新
3. **异步操作**
- 所有文件操作都是异步的
- 不阻塞 UI 线程
4. **Binary 模式**
- 减少字符串转换开销
- 直接操作二进制数据
### 预期性能
- 小笔记(< 10 个资源):< 1 秒
- 中等笔记(10-50 个资源):< 3 秒
- 大笔记(> 50 个资源或多个 Canvas):< 10 秒
## 触发方式
插件提供三种导出触发方式:
1. **命令面板**
- `Export current note to VitePress`
2. **文件菜单**
- 右键点击文件 → `Export to VitePress`
3. **编辑器菜单**
- 右键点击编辑器 → `Export to VitePress`
## 未来扩展
### 可能的功能
1. **批量导出**
- 导出整个文件夹
- 导出所有笔记
2. **高级配置**
- 自定义图片格式(PNG/JPEG/WebP)
- 图片质量控制
- 自定义链接替换规则
3. **VitePress 集成**
- 自动生成 frontmatter
- 自动创建索引页面
- 集成 VitePress 构建流程
4. **更多资源类型**
- 支持视频嵌入
- 支持音频文件
- 支持其他特殊格式
## 维护指南
### 修改导出逻辑
主要导出逻辑在 `exportNote()` 方法中:
```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 updatedBuffer = new TextEncoder().encode(updatedContent);
await vault.adapter.writeBinary(docsPath, updatedBuffer.buffer);
}
```
### 添加新的资源类型
在 `processAssets()` 方法中添加:
```typescript
if (cache.embeds) {
for (const embed of cache.embeds) {
const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path);
if (resolvedLink) {
if (resolvedLink.extension === 'canvas') {
// Canvas 处理
} else if (resolvedLink.extension === 'newtype') {
// 处理新类型
} else {
// 普通资源
}
}
}
}
```
### 调试技巧
1. **启用详细日志**
```typescript
console.log('Processing assets for file:', sourceFile.path);
console.log('Cache:', cache);
console.log('Assets to copy:', Array.from(assetsToCopy));
```
2. **使用 Obsidian 开发者工具**
- `Ctrl+Shift+I` 打开控制台
- 查看日志输出
3. **测试单个功能**
- 创建最小测试用例
- 逐步验证每个步骤
## 相关文档
- [Canvas 渲染详细说明](./canvas-rendering.md) - Canvas 渲染的实现细节
- [执行流程图](./flowchart.md) - 完整的导出流程图
- [Obsidian API 文档](https://github.com/obsidianmd/obsidian-api) - 官方 API