Files
odox/notes/implementation.md
nan f0ca3776f0
All checks were successful
Node.js build / build (20.x) (push) Successful in 21s
Node.js build / build (22.x) (push) Successful in 20s
feat: Implement Canvas file export functionality and enhance Markdown export process
- Added support for exporting `.canvas` files by rendering them to images and generating corresponding `.canvas.md` files.
- Updated the export logic to handle both Markdown and Canvas files, including file type checks and notifications.
- Improved resource management by ensuring assets are copied correctly and links are updated in the exported content.
- Refactored settings to change default export paths from 'docs' to 'wiki' for better organization.
- Created detailed implementation notes and flowcharts to document the export processes and core functionalities.
2026-04-21 22:06:37 +08:00

12 KiB
Raw Blame History

核心功能

  1. Markdown 文件导出:将 .md 文件导出到 VitePress 的 docs 目录,自动处理资源文件
  2. Canvas 文件导出:将 .canvas 文件渲染为图片并生成对应的 .canvas.md 文件
  3. 资源文件管理:自动复制和处理图片、Canvas 等资源文件
  4. 路径规则配置:支持多种目录结构和命名规则

主要流程

整体架构流程

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

资源处理流程

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 渲染流程

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. 生成包含 <Viewer> 组件的 Markdown 内容
  5. 写入文件

路径规则示例:

// 保留文件夹 + 嵌套
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:渲染为图片并更新链接为 <Viewer> 组件
  4. 复制普通资源到 assetsPath
  5. 更新内容中的所有资源链接

链接格式支持:

  • Markdown 格式:![](path)
  • Wikilink 格式:![[path]]

5. renderCanvas()

位置:main.ts:368-556

功能:使用 Obsidian 内部 API 将 Canvas 渲染为 PNG 图片

技术要点:

  • 使用 Electron 的 webContents.capturePage() API
  • 分块渲染:处理大型 Canvas(避免超过 16284px 限制)
  • 缩放校正:考虑系统缩放和设备像素比
  • 像素级精确:使用 binary 模式保留原始格式

分块渲染算法:

// 计算分块数量
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 链接转换为 <Viewer> 组件

转换规则:

![](测试用.canvas)  →  <Viewer src="/测试用.canvas.png" />
![[测试用.canvas]]  →  <Viewer src="/测试用.canvas.png" />

配置选项

settings.ts

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 模式保留文件原始格式:

// 读取
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 解析链接:

const cache = metadataCache.getCache(sourceFile.path);
const resolvedLink = metadataCache.getFirstLinkpathDest(link, sourceFile.path);

3. Electron API 集成

使用 Obsidian 内部的 Electron API:

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 特定:生成的 <Viewer> 组件需要在 VitePress 中配置
  3. 链接转换:复杂的链接语法可能需要手动调整
  4. 大型 Canvas:分块渲染可能耗时较长

未来改进方向

  1. 增量导出:只导出修改过的文件
  2. 批量导出:支持一次性导出整个文件夹
  3. 配置预设:保存常用的导出配置
  4. 错误恢复:更完善的错误处理和回滚机制
  5. 性能优化:并行处理多个文件