Files
odox/notes/implementation.md

14 KiB
Raw Permalink Blame History

核心功能

  1. Markdown 文件导出:将 .md 文件导出到静态文档站点目录,自动处理资源文件
  2. Canvas 文件导出:将 .canvas 文件渲染为高清 PNG 图片并生成对应的 .canvas.md 文件
  3. 资源文件管理:自动复制和处理图片、Canvas 等资源文件到统一目录
  4. 路径规则配置:支持多种目录结构和命名规则,灵活配置输出路径
  5. 透明背景支持:Canvas 导出时可选择透明背景,移除 Obsidian 的点状背景图案

主要流程

整体架构流程

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 --> CheckTransparent{透明背景?}
    CheckTransparent -->|是| AddTransparentClass[添加透明样式类]
    CheckTransparent -->|否| HideElements[隐藏页面元素]
    AddTransparentClass --> HideElements
    HideElements --> 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 --> CheckTransparentCleanup{透明背景?}
    CheckTransparentCleanup -->|是| RestoreHiddenElements[恢复隐藏元素]
    CheckTransparentCleanup -->|否| RestoreStyles[恢复样式]
    RestoreHiddenElements --> RemoveBodyClasses[移除 body 类]
    RemoveBodyClasses --> 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. 写入文件

路径规则示例:

// 保留文件夹 + 嵌套
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 模式保留原始格式
  • 透明背景:通过临时 CSS 类和 DOM 操作实现透明背景导出
  • 智能隐藏:导出时临时隐藏页面其他元素,确保只捕获 Canvas 内容

分块渲染算法:

// 计算分块数量
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> 组件

转换规则:

// 默认模板:![]({url})
![](测试用.canvas)  →  ![](/测试用.canvas.png)
![[测试用.canvas]]  →  ![](/测试用.canvas.png)

// 自定义模板:<Viewer src="{url}" />
![](测试用.canvas)  →  <Viewer src="/测试用.canvas.png" />
![[测试用.canvas]]  →  <Viewer src="/测试用.canvas.png" />

配置选项

settings.ts

interface odoxSettings {
    docsPath: string;                  // 文档站点目录路径,默认 'docs'
    assetsPath: string;                // 资源目录路径,默认 'docs/public'
    nestNoteFolder: boolean;           // 是否为每个笔记创建文件夹,默认 true
    preserveFolderStructure: boolean;  // 是否保留原始文件夹结构,默认 true
    transparentCanvas: boolean;        // 是否使用透明背景导出 Canvas,默认 true
    canvasEmbedTemplate: string;       // Canvas 嵌入模板,默认 '![]({url})'
}

路径规则矩阵

preserveFolderStructure nestNoteFolder Markdown 输入 Markdown 输出 Canvas 输入 Canvas 输出
true true notes/test.md docs/notes/test/index.md notes/test.canvas docs/notes/test.canvas/index.md
true false notes/test.md docs/notes/test.md notes/test.canvas docs/notes/test.md
false true notes/test.md docs/test/index.md notes/test.canvas docs/test.canvas/index.md
false false notes/test.md docs/test.md notes/test.canvas docs/test.canvas.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 过大,超过 16284px 限制
  • 异步处理:使用 requestAnimationFrame 确保渲染完成
  • 缩放校正:考虑系统 DPI 设置,确保高清输出
  • 内存管理:及时释放临时对象(URL.revokeObjectURL)
  • 透明背景:通过 CSS 类和 DOM 操作实现透明背景导出
  • 智能隐藏:导出时临时隐藏页面其他元素,确保只捕获 Canvas 内容

文件结构

.obsidian/plugins/odox/
├── src/
│   ├── main.ts          # 主插件逻辑(约 680 行)
│   └── settings.ts      # 设置定义和 UI
├── notes/               # 文档和说明
│   └── implementation.md # 实现细节文档
├── manifest.json        # 插件清单
├── package.json         # 依赖管理
└── main.js              # 编译后的插件文件

依赖项

  • Obsidian API:插件框架
  • Electron:Canvas 渲染(桌面版)
  • TypeScript:类型安全

使用场景

  1. 从 Obsidian 迁移到静态站点:将个人知识库转换为 VitePress、Docusaurus 等静态网站
  2. 技术文档发布:在 Obsidian 中编写,发布到文档站点
  3. Canvas 可视化:将 Obsidian Canvas 导出为高清 PNG 图片
  4. 自动化工作流:结合 CI/CD 实现自动部署文档站点

技术优势

  1. 高质量渲染:使用 Obsidian 原生渲染引擎,确保导出内容与编辑器中一致
  2. 智能资源管理:自动识别、复制和更新资源文件引用
  3. 灵活配置:支持多种目录结构和命名规则,适应不同的项目需求
  4. 透明背景支持:Canvas 导出时可移除背景图案,适合嵌入各种主题的网站
  5. 分块渲染技术:处理大型 Canvas 文件,避免尺寸限制

未来改进方向

  1. 增量导出:只导出修改过的文件,提高大项目的导出效率
  2. 批量导出:支持一次性导出整个文件夹或整个项目
  3. 配置预设:保存常用的导出配置,快速切换不同项目的导出规则
  4. 错误恢复:更完善的错误处理和回滚机制,导出失败时恢复原状态
  5. 性能优化:并行处理多个文件,利用多核 CPU 加速导出过程
  6. 进度显示:为批量导出操作提供进度条和详细的状态信息
  7. 导出历史:记录导出历史,支持撤销和回滚操作