diff --git a/manifest.json b/manifest.json
index 57db413..18b397f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -4,7 +4,7 @@
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "Export Obsidian notes to VitePress with asset management",
- "author": "Your Name",
- "authorUrl": "https://github.com/yourusername",
+ "author": "nan",
+ "authorUrl": "https://github.com/icrdr",
"isDesktopOnly": false
}
diff --git a/notes/architecture.md b/notes/architecture.md
deleted file mode 100644
index f2d9837..0000000
--- a/notes/architecture.md
+++ /dev/null
@@ -1,424 +0,0 @@
-# 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 图片
- - 替换为 ``
-
-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]]
-之后:
-```
-
-### 路径解析策略
-
-支持多种路径格式的智能匹配:
-
-```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
diff --git a/notes/flowchart.md b/notes/flowchart.md
deleted file mode 100644
index 7dffd0c..0000000
--- a/notes/flowchart.md
+++ /dev/null
@@ -1,411 +0,0 @@
-# VitePress Exporter 执行流程
-
-## 插件概述
-
-VitePress Exporter 是一个 Obsidian 插件,用于将当前笔记导出到 VitePress 文档系统,包括资源文件管理和 Canvas 渲染功能。
-
-## 完整执行流程图
-
-```mermaid
-flowchart TD
- Start([用户触发导出]) --> CheckActiveFile{检查当前文件}
- CheckActiveFile -->|没有活动文件| ShowError1[显示错误提示]
- CheckActiveFile -->|不是 md 文件| ShowError2[显示错误提示]
- CheckActiveFile -->|是 md 文件| ReadFile[读取文件内容
使用 binary 模式]
-
- ReadFile --> ProcessAssets[处理资源文件
processAssets]
- ProcessAssets --> GetCache[获取文件元数据缓存
metadataCache.getCache]
-
- GetCache --> CheckCache{缓存是否存在}
- CheckCache -->|否| ReturnContent[返回原内容]
- CheckCache -->|是| InitializeSets[初始化集合
assetsToCopy, canvasFiles]
-
- InitializeSets --> ScanEmbeds[扫描嵌入文件
cache.embeds]
- ScanEmbeds --> CheckEmbed{有嵌入文件?}
- CheckEmbed -->|否| ScanLinks
- CheckEmbed -->|是| ResolveEmbed[解析嵌入路径
getFirstLinkpathDest]
-
- ResolveEmbed --> CheckCanvas{是 canvas 文件?}
- CheckCanvas -->|是| AddToCanvasSet[添加到 canvasFiles]
- CheckCanvas -->|否| AddToAssetsSet[添加到 assetsToCopy]
- AddToCanvasSet --> ScanEmbeds
- AddToAssetsSet --> ScanEmbeds
-
- ScanEmbeds --> ScanLinks[扫描链接文件
cache.links]
- ScanLinks --> CheckLink{有链接文件?}
- CheckLink -->|否| ProcessCanvasFiles
- CheckLink -->|是| ResolveLink[解析链接路径
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 文件
activeLeaf.openFile]
-
- OpenFile --> GetCanvas[获取 Canvas 对象
activeLeaf.view.canvas]
- GetCanvas --> CheckCanvasObj{Canvas 对象存在?}
-
- CheckCanvasObj -->|否| NextCanvas
- CheckCanvasObj -->|是| ExportInternalAPI[exportCanvasWithInternalAPI]
-
- ExportInternalAPI --> GetBBox[获取视口边界框
getViewportBBox]
- GetBBox --> SetZoom[设置 zoom = 0
100% 缩放]
-
- SetZoom --> RequestFrame[请求重绘
requestFrame]
- RequestFrame --> WaitRender[等待渲染
100ms + rAF]
-
- WaitRender --> TakeScreenshot[调用 takeScreenshot
使用绝对路径]
- TakeScreenshot --> WaitWrite[等待文件写入
100ms]
-
- WaitWrite --> ReadTemp[读取临时文件
readBinary]
- ReadTemp --> CheckBuffer{读取成功?}
-
- CheckBuffer -->|否| LogError1[记录错误]
- CheckBuffer -->|是| RemoveTemp[删除临时文件]
-
- RemoveTemp --> WriteFinal[写入最终位置
writeBinary]
- WriteFinal --> UpdateCanvasLinks[更新 Canvas 链接
updateCanvasLinksInContent]
-
- UpdateCanvasLinks --> NextCanvas{还有 Canvas?}
- LogError1 --> NextCanvas
- NextCanvas -->|是| GetCanvasFile
- NextCanvas -->|否| ProcessAssetFiles
-
- subgraph 资源复制流程
- ProcessAssetFiles[处理资源文件] --> CheckAssets{有资源文件?}
- CheckAssets -->|否| WriteDocs
- CheckAssets -->|是| GetAssetFile[获取资源文件对象]
-
- GetAssetFile --> ReadAsset[读取资源文件
readBinary binary 模式]
- ReadAsset --> MkdirAssets[创建 assets 目录
mkdir]
-
- MkdirAssets --> WriteAsset[写入到 docs/assets
writeBinary binary 模式]
- WriteAsset --> UpdateLinks[更新资源链接
updateLinksInContent]
-
- UpdateLinks --> NextAsset{还有资源?}
- NextAsset -->|是| GetAssetFile
- NextAsset -->|否| WriteDocs
- end
-
- WriteDocs[写入到 docs 目录
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]]
-之后:
-```
-
-**路径匹配**:
-- 支持多种格式(绝对、相对、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 {
- const cache = metadataCache.getCache(sourceFile.path);
- if (!cache) return content;
-
- const assetsToCopy = new Set();
- const canvasFiles = new Set();
-
- // 收集嵌入文件
- 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 {
- 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
- - 增量导出(只处理修改的文件)
diff --git a/notes/implementation.md b/notes/implementation.md
new file mode 100644
index 0000000..e72c1dd
--- /dev/null
+++ b/notes/implementation.md
@@ -0,0 +1,375 @@
+## 核心功能
+
+1. **Markdown 文件导出**:将 `.md` 文件导出到 VitePress 的 docs 目录,自动处理资源文件
+2. **Canvas 文件导出**:将 `.canvas` 文件渲染为图片并生成对应的 `.canvas.md` 文件
+3. **资源文件管理**:自动复制和处理图片、Canvas 等资源文件
+4. **路径规则配置**:支持多种目录结构和命名规则
+
+## 主要流程
+
+### 整体架构流程
+
+```mermaid
+graph TB
+ Start[用户触发导出] --> CheckType{检查文件类型}
+ CheckType -->|Canvas 文件| CanvasFlow[Canvas 导出流程]
+ CheckType -->|Markdown 文件| MarkdownFlow[Markdown 导出流程]
+
+ CanvasFlow --> RenderCanvas[渲染 Canvas 为图片]
+ RenderCanvas --> SaveImage[保存图片到 assetsPath]
+ SaveImage --> GenerateCanvasMD[生成 .canvas.md 文件]
+ GenerateCanvasMD --> WriteCanvas[写入文件]
+ WriteCanvas --> End[完成]
+
+ MarkdownFlow --> ReadContent[读取文件内容]
+ ReadContent --> ProcessAssets[扫描并处理资源]
+ ProcessAssets --> CollectLinks[收集链接和嵌入]
+ CollectLinks --> Separate{分类资源}
+
+ Separate -->|Canvas 文件| RenderCanvas
+ Separate -->|普通资源| CopyAssets[复制资源到 assetsPath]
+
+ CopyAssets --> UpdateLinks[更新内容中的链接]
+ RenderCanvas --> UpdateCanvasLinks[更新 Canvas 链接为 Viewer 组件]
+
+ UpdateCanvasLinks --> CalculatePath[计算输出路径]
+ UpdateLinks --> CalculatePath
+
+ CalculatePath --> ApplyRules{应用路径规则}
+ ApplyRules --> PreserveFolder[preserveFolderStructure?]
+ PreserveFolder -->|是| KeepStructure[保留文件夹结构]
+ PreserveFolder -->|否| Flatten[扁平化]
+
+ KeepStructure --> NestNote{nestNoteFolder?}
+ Flatten --> NestNote
+
+ NestNote -->|是| CreateIndex[创建 index.md]
+ NestNote -->|否| UseFilename[使用原文件名]
+
+ CreateIndex --> WriteMD[写入 Markdown 文件]
+ UseFilename --> WriteMD
+
+ WriteMD --> End
+```
+
+### 资源处理流程
+
+```mermaid
+graph TB
+ Start[processAssets 开始] --> GetCache[获取文件缓存]
+ GetCache --> CheckCache{缓存存在?}
+ CheckCache -->|否| ReturnContent[返回原内容]
+ CheckCache -->|是| CollectStart[收集资源]
+
+ CollectStart --> ScanEmbeds[扫描 embeds]
+ ScanEmbeds --> CheckEmbed{是 Canvas?}
+ CheckEmbed -->|是| AddCanvas[添加到 canvasFiles]
+ CheckEmbed -->|否| AddAsset[添加到 assetsToCopy]
+
+ AddCanvas --> ScanLinks[扫描 links]
+ AddAsset --> ScanLinks
+
+ ScanLinks --> CheckLink{是本地资源?}
+ CheckLink -->|是| AddAsset
+ CheckLink -->|否| ProcessCanvas
+
+ ProcessCanvas[处理 Canvas 文件] --> RenderEach[遍历每个 Canvas]
+ RenderEach --> CallRender[调用 renderCanvas]
+ CallRender --> SavePNG[保存为 PNG]
+ SavePNG --> UpdateCanvasLink[更新为 Viewer 组件]
+
+ UpdateCanvasLink --> CopyAssets[复制普通资源]
+ CopyAssets --> CopyEach[遍历每个资源]
+ CopyEach --> ReadBinary[Binary 模式读取]
+ ReadBinary --> WriteAsset[写入 assetsPath]
+ WriteAsset --> UpdateAssetLink[更新资源链接]
+
+ UpdateAssetLink --> ReturnContent[返回更新后的内容]
+```
+
+### Canvas 渲染流程
+
+```mermaid
+graph TB
+ Start[renderCanvas 开始] --> OpenFile[打开 Canvas 文件]
+ OpenFile --> GetCanvas[获取 Canvas 对象]
+ GetCanvas --> CheckCanvas{Canvas 存在?}
+ CheckCanvas -->|否| ReturnNull[返回 null]
+ CheckCanvas -->|是| CalcSystem[计算系统参数]
+
+ CalcSystem --> GetZoom[获取缩放级别]
+ GetZoom --> CalcPixel[计算设备像素比]
+ CalcPixel --> CalcScale[计算缩放校正系数]
+
+ CalcScale --> GetNodes[获取所有节点]
+ GetNodes --> CalcBBox[计算边界框]
+ CalcBBox --> CalcSize[计算宽度和高度]
+
+ CalcSize --> CalcLimit{计算缩放限制}
+ CalcLimit --> CheckLimit{超过 16284px?}
+ CheckLimit -->|是| UseLimit[使用限制值]
+ CheckLimit -->|否| UseZoom[使用原始缩放]
+
+ UseLimit --> PrepareCanvas[准备 Canvas 环境]
+ UseZoom --> PrepareCanvas
+
+ PrepareCanvas --> DeselectAll[取消选择]
+ DeselectAll --> AddScreenshotting[添加截图样式]
+ AddScreenshotting --> AppendToBody[附加到 body]
+
+ AppendToBody --> SetPosition[设置位置]
+ SetPosition --> CalcViewport[计算视口]
+ CalcViewport --> SetCanvasZoom[设置 Canvas 缩放]
+ SetCanvasZoom --> RequestFrame[请求帧]
+
+ RequestFrame --> WaitRender[等待渲染完成]
+ WaitRender --> CalcTiles[计算分块数量]
+ CalcTiles --> CreateFinalCanvas[创建最终 Canvas]
+
+ CreateFinalCanvas --> RenderLoop[渲染循环]
+ RenderLoop --> SetViewport[设置视口位置]
+ SetViewport --> RequestTileFrame[请求分块帧]
+ RequestTileFrame --> WaitTile[等待渲染]
+ WaitTile --> CapturePage[捕获页面]
+ CapturePage --> ConvertToPNG[转换为 PNG]
+ ConvertToPNG --> DrawToFinal[绘制到最终 Canvas]
+ DrawToFinal --> CheckNext{还有分块?}
+ CheckNext -->|是| SetViewport
+ CheckNext -->|否| ConvertFinal[转换最终 Canvas]
+
+ ConvertFinal --> ToBlob[转换为 Blob]
+ ToBlob --> ToArrayBuffer[转换为 ArrayBuffer]
+ ToArrayBuffer -> ToUint8Array[转换为 Uint8Array]
+ ToUint8Array --> ReturnResult[返回结果]
+
+ ReturnResult --> Cleanup[清理环境]
+ Cleanup --> RestoreStyles[恢复样式]
+ RestoreStyles --> ResetCanvas[重置 Canvas]
+ ResetCanvas --> End[结束]
+
+ ReturnNull --> End
+```
+
+## 核心方法说明
+
+### 1. exportCurrentNote()
+**位置**:main.ts:75-89
+
+**功能**:导出当前活动的文件
+- 获取活动文件
+- 验证文件类型(md 或 canvas)
+- 调用 exportNote()
+
+### 2. exportNote()
+**位置**:main.ts:92-135
+
+**功能**:导出单个文件的主入口
+- 判断文件类型并分发到不同的处理逻辑
+- Markdown 文件:读取内容 → 处理资源 → 计算路径 → 写入文件
+- Canvas 文件:调用 exportCanvasFile()
+
+### 3. exportCanvasFile()
+**位置**:main.ts:138-192
+
+**功能**:专门处理 Canvas 文件导出
+1. 渲染 Canvas 为 PNG 图片
+2. 保存图片到 assetsPath(扁平化)
+3. 计算 .canvas.md 文件路径
+4. 生成包含 `` 组件的 Markdown 内容
+5. 写入文件
+
+**路径规则示例**:
+```typescript
+// 保留文件夹 + 嵌套
+notes/测试用.canvas → docs/notes/测试用.canvas/index.md
+
+// 仅嵌套
+notes/测试用.canvas → docs/测试用.canvas/index.md
+
+// 保留文件夹
+notes/测试用.canvas → docs/notes/测试用.canvas.md
+
+// 默认
+notes/测试用.canvas → docs/测试用.canvas.md
+```
+
+### 4. processAssets()
+**位置**:main.ts:195-308
+
+**功能**:扫描并处理 Markdown 文件中的资源
+
+**处理流程**:
+1. 使用 metadataCache 获取文件的 embeds 和 links
+2. 分类资源:Canvas 文件 vs 普通资源
+3. 处理 Canvas:渲染为图片并更新链接为 `` 组件
+4. 复制普通资源到 assetsPath
+5. 更新内容中的所有资源链接
+
+**链接格式支持**:
+- Markdown 格式:``
+- Wikilink 格式:`![[path]]`
+
+### 5. renderCanvas()
+**位置**:main.ts:368-556
+
+**功能**:使用 Obsidian 内部 API 将 Canvas 渲染为 PNG 图片
+
+**技术要点**:
+- 使用 Electron 的 `webContents.capturePage()` API
+- 分块渲染:处理大型 Canvas(避免超过 16284px 限制)
+- 缩放校正:考虑系统缩放和设备像素比
+- 像素级精确:使用 binary 模式保留原始格式
+
+**分块渲染算法**:
+```typescript
+// 计算分块数量
+const cols = Math.ceil((bbox.maxX - bbox.minX) / tileWidth);
+const rows = Math.ceil((bbox.maxY - bbox.minY) / tileHeight);
+
+// 逐个渲染分块并拼接
+for (let row = 0; row < rows; row++) {
+ for (let col = 0; col < cols; col++) {
+ // 设置视口位置
+ canvas.x = canvas.tx = startX + col * tileWidth;
+ canvas.y = canvas.ty = startY + row * tileHeight;
+
+ // 捕获并绘制
+ const capturedData = await webContents.capturePage(...);
+ ctx.drawImage(img, col * O * s, row * F * s);
+ }
+}
+```
+
+### 6. updateLinksInContent()
+**位置**:main.ts:311-364
+
+**功能**:更新内容中的资源链接
+
+**智能匹配**:
+尝试多种可能的路径格式:
+- 完整路径:`notes/image.png`
+- 相对路径:`image.png`
+- URL 编码:`notes%2Fimage.png`
+- 文件名:`image.png`
+
+**统一输出**:
+所有资源链接转换为根路径:`/filename.png`
+
+### 7. updateCanvasLinksInContent()
+**位置**:main.ts:593-648
+
+**功能**:将 Canvas 链接转换为 `` 组件
+
+**转换规则**:
+```markdown
+ →
+![[测试用.canvas]] →
+```
+
+## 配置选项
+
+### settings.ts
+
+```typescript
+interface vitepressExporterSettings {
+ docsPath: string; // VitePress docs 目录路径,默认 'docs'
+ assetsPath: string; // 资源目录路径,默认 'docs/public'
+ nestNoteFolder: boolean; // 是否为每个笔记创建文件夹,默认 true
+ preserveFolderStructure: boolean; // 是否保留原始文件夹结构,默认 true
+}
+```
+
+### 路径规则矩阵
+
+| preserveFolderStructure | nestNoteFolder | 输入 | 输出 |
+|---------|---------|------|------|
+| true | true | notes/test.md | docs/notes/test/index.md |
+| true | false | notes/test.md | docs/notes/test.md |
+| false | true | notes/test.md | docs/test/index.md |
+| false | false | notes/test.md | docs/test.md |
+
+## 技术实现要点
+
+### 1. Binary 模式文件操作
+
+使用 binary 模式保留文件原始格式:
+```typescript
+// 读取
+const contentBuffer = await vault.readBinary(file);
+const content = new TextDecoder().decode(contentBuffer);
+
+// 写入
+const buffer = new TextEncoder().encode(content);
+await vault.adapter.writeBinary(path, buffer.buffer);
+```
+
+### 2. Obsidian 元数据缓存
+
+使用 `metadataCache` 解析链接:
+```typescript
+const cache = metadataCache.getCache(sourceFile.path);
+const resolvedLink = metadataCache.getFirstLinkpathDest(link, sourceFile.path);
+```
+
+### 3. Electron API 集成
+
+使用 Obsidian 内部的 Electron API:
+```typescript
+const electron = window.electron?.remote;
+const webContents = electron?.getCurrentWebContents();
+const capturedData = await webContents.capturePage({ x, y, width, height });
+```
+
+### 4. 链接解析兼容性
+
+支持多种链接格式:
+- 绝对路径和相对路径
+- URL 编码和未编码
+- Markdown 标准格式和 Obsidian Wikilink
+
+### 5. Canvas 渲染优化
+
+- **分块渲染**:避免单个 Canvas 过大
+- **异步处理**:使用 `requestAnimationFrame` 确保渲染完成
+- **缩放校正**:考虑系统 DPI 设置
+- **内存管理**:及时释放临时对象(URL.revokeObjectURL)
+
+## 文件结构
+
+```
+.obsidian/plugins/vitepress-exporter/
+├── src/
+│ ├── main.ts # 主插件逻辑(650 行)
+│ └── settings.ts # 设置定义和 UI
+├── notes/ # 文档和说明
+├── manifest.json # 插件清单
+└── package.json # 依赖管理
+```
+
+## 依赖项
+
+- **Obsidian API**:插件框架
+- **Electron**:Canvas 渲染(桌面版)
+- **TypeScript**:类型安全
+
+## 使用场景
+
+1. **从 Obsidian 迁移到 VitePress**:将个人知识库转换为静态网站
+2. **技术文档发布**:在 Obsidian 中编写,发布到 VitePress
+3. **Canvas 可视化**:将 Obsidian Canvas 导出为交互式图片
+4. **自动化工作流**:结合 CI/CD 实现自动部署
+
+## 局限性
+
+1. **平台依赖**:Canvas 渲染仅支持桌面版 Obsidian(需要 Electron)
+2. **VitePress 特定**:生成的 `` 组件需要在 VitePress 中配置
+3. **链接转换**:复杂的链接语法可能需要手动调整
+4. **大型 Canvas**:分块渲染可能耗时较长
+
+## 未来改进方向
+
+1. **增量导出**:只导出修改过的文件
+2. **批量导出**:支持一次性导出整个文件夹
+3. **配置预设**:保存常用的导出配置
+4. **错误恢复**:更完善的错误处理和回滚机制
+5. **性能优化**:并行处理多个文件
diff --git a/notes/render-canvas.md b/notes/render-canvas.md
deleted file mode 100644
index c188b09..0000000
--- a/notes/render-canvas.md
+++ /dev/null
@@ -1,397 +0,0 @@
-# Canvas 渲染系统实现
-
-## 技术栈
-
-- **渲染方式**: Obsidian Canvas 内部 API (`takeScreenshot`)
-- **缩放控制**: `canvas.zoom` 属性(对数刻度)
-- **语言**: TypeScript
-
-## 核心实现
-
-### 1. Canvas 捕获流程
-
-```typescript
-async renderCanvas(canvasFile: TFile): Promise {
- // 1. 获取活动 leaf
- const activeLeaf = this.app.workspace.getLeaf();
- if (!activeLeaf) return null;
-
- // 2. 打开 Canvas 文件
- await activeLeaf.openFile(canvasFile);
-
- // 3. 获取 Canvas 对象
- const canvas = activeLeaf.view.canvas;
- if (!canvas) return null;
-
- // 4. 使用内部 API 导出
- return await this.exportCanvasWithInternalAPI(canvas, canvasFile);
-}
-```
-
-### 2. 使用 Canvas 内部 API
-
-```typescript
-async exportCanvasWithInternalAPI(
- canvas: any,
- canvasFile: TFile
-): Promise {
- try {
- // 1. 获取边界框
- const bbox = canvas.getViewportBBox();
- const contentWidth = bbox.maxX - bbox.minX;
- const contentHeight = bbox.maxY - bbox.minY;
-
- // 2. 设置 zoom = 0 (100% 缩放)
- const oldZoom = canvas.zoom;
- canvas.zoom = 0; // 对数刻度:2^0 = 1.0 (100%)
-
- // 3. 请求重绘
- canvas.requestFrame();
- await new Promise(resolve => setTimeout(resolve, 100));
- await new Promise(resolve => requestAnimationFrame(resolve));
-
- // 4. 生成文件路径
- const tempFileName = `temp_canvas_${Date.now()}.png`;
- const finalFileName = canvasFile.name.replace(".canvas", ".png");
- const finalPath = `${this.settings.assetsPath}/${finalFileName}`;
-
- // 5. 获取 vault 绝对路径
- const vaultBasePath = vault.adapter.basePath;
- const tempFullPath = `${vaultBasePath}/${tempFileName}`;
-
- // 6. 调用 takeScreenshot(需要绝对路径)
- await canvas.takeScreenshot(tempFullPath, false);
-
- // 7. 等待文件写入完成
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // 8. 读取临时文件(使用相对路径)
- const imageBuffer = await vault.adapter.readBinary(tempFileName);
-
- if (!imageBuffer || imageBuffer.byteLength === 0) {
- console.error("Failed to read temporary file");
- return null;
- }
-
- // 9. 删除临时文件
- await vault.adapter.remove(tempFileName);
-
- // 10. 写入到最终位置
- await vault.adapter.writeBinary(finalPath, imageBuffer);
-
- // 11. 返回 Uint8Array
- return new Uint8Array(imageBuffer);
-
- } catch (error) {
- console.error("exportCanvasWithInternalAPI failed:", error);
- return null;
- }
-}
-```
-
-### 3. Canvas Zoom 对数刻度
-
-**关键发现**:`canvas.zoom` 使用对数刻度(base 2)
-
-| zoom 值 | 缩放比例 | 说明 |
-|---------|----------|------|
-| 0 | 100% (2^0) | 原始尺寸,我们要设置的值 |
-| 1 | 200% (2^1) | 放大 2 倍 |
-| -1 | 50% (2^-1) | 缩小一半 |
-| -1.76 | ~30% | Obsidian 默认缩放 |
-
-**重要**:不要设置 `zoom = 1.0`,这会让 Canvas 放大到 200%!应该设置 `zoom = 0`。
-
-### 4. 链接更新
-
-Canvas 引用会被转换为 Viewer 组件:
-
-```typescript
-updateCanvasLinksInContent(content: string, canvasFile: TFile, canvasPath: string, imageFileName: string): string {
- // 支持多种路径格式
- const possiblePaths = [
- canvasPath, // 完整路径
- canvasPath.substring(sourceDir.length + 1), // 相对路径
- encodeURI(canvasPath), // URL 编码
- canvasFile.name, // 文件名
- canvasFile.name.replace('.canvas', ''), // 不带扩展名
- ];
-
- // 替换 Markdown 格式
- const markdownRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
- content = content.replace(markdownRegex, (match, alt, path) => {
- if (path匹配) {
- const relativePath = this.settings.assetsPath.replace('docs/', '');
- return ``;
- }
- return match;
- });
-
- // 替换 Wikilink 格式
- const wikilinkRegex = /!\[\[([^\]]+)\]\]/g;
- content = content.replace(wikilinkRegex, (match, link) => {
- if (link匹配) {
- const relativePath = this.settings.assetsPath.replace('docs/', '');
- return ``;
- }
- return match;
- });
-
- return content;
-}
-```
-
-## 渲染流程
-
-```
-1. 打开 Canvas 文件
- └─> activeLeaf.openFile(canvasFile)
-
-2. 获取 Canvas 对象
- └─> activeLeaf.view.canvas
-
-3. 调用 exportCanvasWithInternalAPI
- ├─> 获取视口边界框
- ├─> 设置 zoom = 0 (100%)
- ├─> 请求重绘
- ├─> 等待 100ms + requestAnimationFrame
- ├─> 调用 takeScreenshot(绝对路径)
- ├─> 读取临时文件
- ├─> 删除临时文件
- └─> 写入到 docs/assets/
-
-4. 更新内容中的链接
- └─> 转换为
-```
-
-## 在资源处理中的集成
-
-Canvas 渲染是资源处理流程的一部分:
-
-```typescript
-async processAssets(content: string, sourceFile: TFile): Promise {
- const cache = metadataCache.getCache(sourceFile.path);
- const canvasFiles = new Set();
-
- // 1. 收集 Canvas 文件
- if (cache.embeds) {
- for (const embed of cache.embeds) {
- const resolvedLink = metadataCache.getFirstLinkpathDest(embed.link, sourceFile.path);
- if (resolvedLink?.extension === 'canvas') {
- canvasFiles.add(resolvedLink.path);
- }
- }
- }
-
- // 2. 渲染每个 Canvas
- for (const canvasPath of canvasFiles) {
- try {
- const canvasFile = vault.getAbstractFileByPath(canvasPath);
- if (canvasFile instanceof TFile) {
- const renderedImage = await this.captureCanvas(canvasFile);
- if (renderedImage) {
- const imageFileName = canvasFile.name.replace('.canvas', '.png');
- const imagePath = `${this.settings.assetsPath}/${imageFileName}`;
-
- // 确保 assets 目录存在
- await vault.adapter.mkdir(this.settings.assetsPath);
-
- // 写入渲染的图片
- await vault.adapter.writeBinary(imagePath, renderedImage.buffer);
-
- // 更新内容中的 canvas 引用
- content = this.updateCanvasLinksInContent(content, canvasFile, canvasPath, imageFileName);
- }
- }
- } catch (error) {
- console.error(`Failed to render canvas ${canvasPath}:`, error);
- }
- }
-
- return content;
-}
-```
-
-## 优势
-
-1. **完全使用 Obsidian 内部 API**
- - 不依赖第三方库(html-to-image)
- - 兼容性好,Obsidian 升级时应该能正常工作
- - 代码简洁,维护方便
-
-2. **正确的缩放控制**
- - 使用对数刻度的 `zoom` 属性
- - 设置 `zoom = 0` 获得准确的 100% 缩放
- - 每个元素都是原始尺寸
-
-3. **路径处理灵活**
- - 支持多种路径格式(绝对、相对、URL 编码)
- - 自动转换为正确的相对路径
- - 支持 Markdown 和 Wikilink 两种格式
-
-4. **错误隔离**
- - 单个 Canvas 渲染失败不影响其他文件
- - 在控制台输出详细错误信息
- - 继续处理后续资源
-
-## 测试方法
-
-### 1. 基本功能测试
-
-创建测试 Canvas 文件:
-- 小尺寸: 800x600
-- 大尺寸: 4000x3000
-- 包含多个节点和连接
-
-### 2. 验证步骤
-
-1. **创建测试笔记**,包含 Canvas 引用
- ```markdown
- # 测试笔记
-
- 这是一个 Canvas:
- ![[test.canvas]]
- ```
-
-2. **执行导出命令**
-
-3. **检查控制台输出**:
- ```
- Processing assets for file: test.md
- Canvas files to render: ['test.canvas']
- Successfully rendered canvas: test.canvas -> docs/assets/test.png
- ```
-
-4. **检查输出文件**:
- - 图片位置:`docs/assets/test.png`
- - 图片尺寸应该等于 Canvas 的实际内容尺寸
- - 文字清晰,没有缩水
- - 导出后的笔记内容被更新
-
-### 3. 路径格式测试
-
-测试各种路径格式的 Canvas 引用:
-
-```markdown
-![[test.canvas]] # 文件名
-![[folder/test.canvas]] # 相对路径
- # Markdown 格式
- # Markdown 相对路径
-![[test.canvas|别名]] # 带别名
- # URL 编码
-```
-
-所有格式都应该被正确识别和转换。
-
-## API 参考
-
-### Canvas 内部方法
-
-| 方法 | 参数 | 返回值 | 说明 |
-|------|------|--------|------|
-| `getViewportBBox()` | - | `{minX, minY, maxX, maxY}` | 获取视口边界框 |
-| `takeScreenshot(path, privacy)` | `path: string`, `privacy: boolean` | `Promise` | 截图并保存到文件 |
-| `requestFrame()` | - | - | 请求重绘 |
-| `zoom` 属性 | - | `number` | 对数刻度缩放(0 = 100%) |
-
-### Workspace API
-
-| 方法 | 参数 | 返回值 | 说明 |
-|------|------|--------|------|
-| `getLeaf()` | - | `WorkspaceLeaf` | 获取活动 leaf |
-| `openFile(file)` | `TFile` | `Promise` | 打开文件 |
-
-### Vault API
-
-| 方法 | 参数 | 返回值 | 说明 |
-|------|------|--------|------|
-| `adapter.readBinary(path)` | `string` | `Promise` | 读取二进制文件 |
-| `adapter.writeBinary(path, data)` | `string, ArrayBuffer` | `Promise` | 写入二进制文件 |
-| `adapter.mkdir(path)` | `string` | `Promise` | 创建目录 |
-| `adapter.remove(path)` | `string` | `Promise` | 删除文件 |
-
-### 重要常量
-
-- `zoom = 0` → 100% 缩放
-- `zoom = 1` → 200% 缩放
-- `zoom = -1` → 50% 缩放
-- `zoom = Math.log2(scale)` → zoom 和缩放的转换公式
-
-## 已知问题
-
-1. **临时文件处理**
- - `takeScreenshot` 需要**绝对文件系统路径**
- - 必须先保存到临时文件,然后读取并移动
- - 临时文件会自动清理
-
-2. **路径获取**
- - 使用 `vault.adapter.basePath` 获取 vault 的完整路径
- - 这是 Obsidian 的内部 API,可能在未来版本中变化
-
-3. **同步问题**
- - 需要等待 `takeScreenshot` 完成
- - 使用 `setTimeout` 和 `requestAnimationFrame` 确保渲染完成
- - 时间可能需要根据实际情况调整
-
-## 故障排除
-
-### Canvas 渲染失败
-
-**问题**:Canvas 没有被渲染成图片
-
-**可能原因**:
-1. Canvas 文件路径不正确
-2. Canvas 对象获取失败
-3. `takeScreenshot` 调用失败
-4. 临时文件读写权限问题
-
-**解决方法**:
-- 检查控制台日志
-- 确认 Canvas 文件存在且可打开
-- 检查文件系统权限
-
-### 图片质量差
-
-**问题**:渲染的图片模糊或尺寸不正确
-
-**可能原因**:
-- zoom 值设置不正确
-- Canvas 节点被折叠
-
-**解决方法**:
-- 确认 `zoom = 0`(100%)
-- 检查 Canvas 是否有折叠的节点
-- 增加等待时间确保完全渲染
-
-### 链接没有更新
-
-**问题**:导出后 Canvas 引用没有被替换
-
-**可能原因**:
-- 路径格式不支持
-- 正则表达式不匹配
-
-**解决方法**:
-- 检查链接格式(Markdown vs Wikilink)
-- 查看控制台日志确认路径匹配
-- 添加更多路径格式支持
-
-## 未来改进
-
-1. **进度反馈**
- - 添加渲染进度提示
- - 显示 Canvas 尺寸和预计时间
-
-2. **错误处理**
- - 更详细的错误信息
- - 降级方案(如果 takeScreenshot 不可用)
-
-3. **性能优化**
- - 缓存已渲染的 Canvas
- - 避免重复截图
- - 并行处理多个 Canvas
-
-4. **更多格式**
- - 支持 JPEG、WebP 等格式
- - 可配置的图片质量
- - 可配置的分辨率
diff --git a/src/main.ts b/src/main.ts
index f7f9fa4..2fd2062 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -23,7 +23,7 @@ export default class vitepressExporterPlugin extends Plugin {
this.registerEvent(
// @ts-ignore
this.app.workspace.on("file-menu", (menu: Menu, file: TFile) => {
- if (file.extension === "md") {
+ if (file.extension === "md" || file.extension === "canvas") {
menu.addItem((item: MenuItem) => {
item
@@ -80,8 +80,8 @@ export default class vitepressExporterPlugin extends Plugin {
return;
}
- if (activeFile.extension !== 'md') {
- new Notice('Current file is not a Markdown file');
+ if (activeFile.extension !== 'md' && activeFile.extension !== 'canvas') {
+ new Notice('Current file is not a Markdown or canvas file');
return;
}
@@ -90,6 +90,11 @@ export default class vitepressExporterPlugin extends Plugin {
// 导出单个笔记
async exportNote(file: TFile) {
+ if (file.extension === 'canvas') {
+ await this.exportCanvasFile(file);
+ return;
+ }
+
const { vault } = this.app;
// 使用 binary 模式读取文件以保留原始格式(包括行尾空格)
@@ -129,6 +134,63 @@ export default class vitepressExporterPlugin extends Plugin {
console.warn(`Exported ${file.name} to ${docsPath}`);
}
+ // 导出 canvas 文件
+ async exportCanvasFile(file: TFile) {
+ const { vault } = this.app;
+
+ // 1. 渲染 canvas 为图片
+ const renderedImage = await this.renderCanvas(file);
+ if (!renderedImage) {
+ new Notice(`Failed to render canvas: ${file.name}`);
+ return;
+ }
+
+ // 2. 保存图片到 assetsPath (扁平化存储)
+ const imageFileName = file.name + '.png';
+ const imagePath = `${this.settings.assetsPath}/${imageFileName}`;
+
+ // 确保目标目录存在
+ await vault.adapter.mkdir(this.settings.assetsPath);
+
+ // 写入渲染的图片
+ await vault.adapter.writeBinary(imagePath, renderedImage.buffer);
+
+ console.debug(`Successfully rendered canvas: ${file.path} -> ${imagePath}`);
+
+ // 3. 计算 .canvas.md 文件的输出路径(复用现有逻辑)
+ const fileNameWithoutExt = file.name.replace('.canvas', '');
+ const filePathWithoutExt = file.path.replace('.canvas', '');
+
+ let docsPath: string;
+ if (this.settings.preserveFolderStructure && this.settings.nestNoteFolder) {
+ // 保留文件夹 + 嵌套: notes/some-note.canvas > docs/notes/some-note.canvas/index.md
+ docsPath = `${this.settings.docsPath}/${filePathWithoutExt}.canvas/index.md`;
+ } else if (this.settings.preserveFolderStructure) {
+ // 保留文件夹结构: notes/some-note.canvas > docs/notes/some-note.canvas.md
+ docsPath = `${this.settings.docsPath}/${filePathWithoutExt}.md`;
+ } else if (this.settings.nestNoteFolder) {
+ // 嵌套文件夹: notes/some-note.canvas > docs/some-note.canvas/index.md
+ docsPath = `${this.settings.docsPath}/${fileNameWithoutExt}.canvas/index.md`;
+ } else {
+ // 默认: notes/some-note.canvas > docs/some-note.canvas.md
+ docsPath = `${this.settings.docsPath}/${fileNameWithoutExt}.canvas.md`;
+ }
+
+ // 4. 确保 .canvas.md 目标目录存在
+ const targetDir = docsPath.substring(0, docsPath.lastIndexOf('/'));
+ await vault.adapter.mkdir(targetDir);
+
+ // 5. 生成 .canvas.md 内容
+ const content = ``;
+
+ // 6. 写入 .canvas.md 文件
+ const contentBuffer = new TextEncoder().encode(content);
+ await vault.adapter.writeBinary(docsPath, contentBuffer.buffer);
+
+ console.warn(`Exported ${file.name} to ${docsPath}`);
+ new Notice(`Exported ${file.name} to ${docsPath}`);
+ }
+
// 处理资源文件
async processAssets(content: string, sourceFile: TFile): Promise {
const { vault, metadataCache } = this.app;
@@ -188,7 +250,7 @@ export default class vitepressExporterPlugin extends Plugin {
if (canvasFile instanceof TFile) {
const renderedImage = await this.renderCanvas(canvasFile);
if (renderedImage) {
- const imageFileName = canvasFile.name.replace('.canvas', '.png');
+ const imageFileName = canvasFile.name + '.png';
// 图片保存到 docs/assets/
const imagePath = `${this.settings.assetsPath}/${imageFileName}`;
diff --git a/src/settings.ts b/src/settings.ts
index 3183b7d..4a0622b 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -9,8 +9,8 @@ export interface vitepressExporterSettings {
}
export const DEFAULT_SETTINGS: vitepressExporterSettings = {
- docsPath: 'docs',
- assetsPath: 'docs/public',
+ docsPath: 'wiki',
+ assetsPath: 'wiki/public',
nestNoteFolder: true,
preserveFolderStructure: true
}