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:
424
notes/architecture.md
Normal file
424
notes/architecture.md
Normal 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
|
||||
之前: 
|
||||
之后: 
|
||||
```
|
||||
|
||||
**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
|
||||
Reference in New Issue
Block a user