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

12 KiB
Raw Blame History

VitePress Exporter 执行流程

插件概述

VitePress Exporter 是一个 Obsidian 插件,用于将当前笔记导出到 VitePress 文档系统,包括资源文件管理和 Canvas 渲染功能。

完整执行流程图

flowchart TD
    Start([用户触发导出]) --> CheckActiveFile{检查当前文件}
    CheckActiveFile -->|没有活动文件| ShowError1[显示错误提示]
    CheckActiveFile -->|不是 md 文件| ShowError2[显示错误提示]
    CheckActiveFile -->|是 md 文件| ReadFile[读取文件内容<br/>使用 binary 模式]

    ReadFile --> ProcessAssets[处理资源文件<br/>processAssets]
    ProcessAssets --> GetCache[获取文件元数据缓存<br/>metadataCache.getCache]

    GetCache --> CheckCache{缓存是否存在}
    CheckCache -->|否| ReturnContent[返回原内容]
    CheckCache -->|是| InitializeSets[初始化集合<br/>assetsToCopy, canvasFiles]

    InitializeSets --> ScanEmbeds[扫描嵌入文件<br/>cache.embeds]
    ScanEmbeds --> CheckEmbed{有嵌入文件?}
    CheckEmbed -->|否| ScanLinks
    CheckEmbed -->|是| ResolveEmbed[解析嵌入路径<br/>getFirstLinkpathDest]

    ResolveEmbed --> CheckCanvas{是 canvas 文件?}
    CheckCanvas -->|是| AddToCanvasSet[添加到 canvasFiles]
    CheckCanvas -->|否| AddToAssetsSet[添加到 assetsToCopy]
    AddToCanvasSet --> ScanEmbeds
    AddToAssetsSet --> ScanEmbeds

    ScanEmbeds --> ScanLinks[扫描链接文件<br/>cache.links]
    ScanLinks --> CheckLink{有链接文件?}
    CheckLink -->|否| ProcessCanvasFiles
    CheckLink -->|是| ResolveLink[解析链接路径<br/>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 文件<br/>activeLeaf.openFile]

    OpenFile --> GetCanvas[获取 Canvas 对象<br/>activeLeaf.view.canvas]
    GetCanvas --> CheckCanvasObj{Canvas 对象存在?}

    CheckCanvasObj -->|否| NextCanvas
    CheckCanvasObj -->|是| ExportInternalAPI[exportCanvasWithInternalAPI]

    ExportInternalAPI --> GetBBox[获取视口边界框<br/>getViewportBBox]
    GetBBox --> SetZoom[设置 zoom = 0<br/>100% 缩放]

    SetZoom --> RequestFrame[请求重绘<br/>requestFrame]
    RequestFrame --> WaitRender[等待渲染<br/>100ms + rAF]

    WaitRender --> TakeScreenshot[调用 takeScreenshot<br/>使用绝对路径]
    TakeScreenshot --> WaitWrite[等待文件写入<br/>100ms]

    WaitWrite --> ReadTemp[读取临时文件<br/>readBinary]
    ReadTemp --> CheckBuffer{读取成功?}

    CheckBuffer -->|否| LogError1[记录错误]
    CheckBuffer -->|是| RemoveTemp[删除临时文件]

    RemoveTemp --> WriteFinal[写入最终位置<br/>writeBinary]
    WriteFinal --> UpdateCanvasLinks[更新 Canvas 链接<br/>updateCanvasLinksInContent]

    UpdateCanvasLinks --> NextCanvas{还有 Canvas?}
    LogError1 --> NextCanvas
    NextCanvas -->|是| GetCanvasFile
    NextCanvas -->|否| ProcessAssetFiles

    subgraph 资源复制流程
        ProcessAssetFiles[处理资源文件] --> CheckAssets{有资源文件?}
        CheckAssets -->|否| WriteDocs
        CheckAssets -->|是| GetAssetFile[获取资源文件对象]

        GetAssetFile --> ReadAsset[读取资源文件<br/>readBinary binary 模式]
        ReadAsset --> MkdirAssets[创建 assets 目录<br/>mkdir]

        MkdirAssets --> WriteAsset[写入到 docs/assets<br/>writeBinary binary 模式]
        WriteAsset --> UpdateLinks[更新资源链接<br/>updateLinksInContent]

        UpdateLinks --> NextAsset{还有资源?}
        NextAsset -->|是| GetAssetFile
        NextAsset -->|否| WriteDocs
    end

    WriteDocs[写入到 docs 目录<br/>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 格式:

之前: ![](path/to/image.png)
之后: ![](assets/image.png)

Wikilink 格式:

之前: ![[path/to/image.png]]
之后: ![[assets/image.png]]

Canvas 特殊处理:

之前: ![[diagram.canvas]]
之后: <Viewer src="assets/diagram.png" />

路径匹配:

  • 支持多种格式(绝对、相对、URL 编码)
  • 使用正则表达式匹配和替换
  • 转换为相对路径

5. 文件写入

Binary 模式:

  • 使用 TextEncoder.encode() 转换内容
  • 调用 vault.adapter.writeBinary() 写入
  • 保留原始格式

关键方法说明

exportCurrentNote()

导出当前活动笔记

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)

执行导出逻辑

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)

处理资源文件

async processAssets(content: string, sourceFile: TFile): Promise<string> {
    const cache = metadataCache.getCache(sourceFile.path);
    if (!cache) return content;

    const assetsToCopy = new Set<string>();
    const canvasFiles = new Set<string>();

    // 收集嵌入文件
    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 为图片

async captureCanvas(canvasFile: TFile): Promise<Uint8Array | null> {
    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
    • 增量导出(只处理修改的文件)