Files
odox/notes/architecture.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

425 lines
11 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 格式。核心功能包括:
- 导出 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