# Joplin 插件 - 导出功能说明 本文档详细说明 Joplin Typora 风格编辑器插件的导出功能。关于插件的构建、调试及发布操作,可参考 GENERATOR_DOC.md。 ## 功能概述 该插件支持将 Joplin 笔记导出为 Markdown 格式,兼容 Typora 等 Markdown 编辑器。主要特性包括: - **YAML Frontmatter 处理**:自动生成或更新元数据(标题、作者、创建时间、更新时间) - **资源链接转换**:将 Joplin 内部资源链接转换为相对路径 - **两种导出模式**:扁平结构和层次结构 - **批量导出支持**:支持导出单个或多个笔记 ## 导出流程 插件通过 `joplin.interop.registerExportModule()` 注册导出模块,导出过程按以下顺序触发: ``` 用户选择导出操作 ↓ 1. onInit - 初始化导出环境 ↓ 2. onProcessItem - 处理笔记和文件夹 ↓ 3. onProcessResource - 处理资源文件 ↓ 4. onClose - 保存文件并清理缓存 ``` 详细触发机制请参考 [触发机制.md](触发机制.md)。 ## 一、导出路径样式 插件支持两种导出路径样式,可在插件设置中配置: ### 1. 扁平结构(Flat) 所有文件和资源放在同一目录下,适用于简单的笔记组织。 ``` export/ ├── 笔记1.md ├── 笔记2.md ├── 笔记3.md └── assets/ ├── image1.png ├── image2.png └── document.pdf ``` **特点**: - 所有笔记文件位于导出目录根目录 - 所有资源统一存储在 `assets` 目录 - 资源引用路径:`./assets/文件名` ### 2. 层次结构(Hierarchical) 按照 Joplin 的文件夹结构组织文件,保持原有的层级关系。 ``` export/ ├── 工作笔记/ │ ├── 项目A/ │ │ ├── 需求文档.md │ │ └── assets/ │ │ └── diagram.png │ └── 项目B/ │ ├── 会议记录.md │ └── assets/ │ └── whiteboard.jpg └── 个人笔记/ ├── 读书笔记.md └── assets/ └── bookcover.png ``` **特点**: - 笔记按照 Joplin 文件夹结构组织 - 每个文件夹有独立的 `assets` 目录 - 资源引用路径:`./assets/文件名`(相对于笔记所在文件夹) - 支持从叶节点向上追溯到根节点构建完整路径 ## 二、YAML Frontmatter 处理 ### 核心字段 导出的 YAML Frontmatter 必须包含以下 4 个核心字段: | 字段 | 说明 | 来源 | 格式 | |------|------|------|------| | `title` | 笔记标题 | `note.title` | 字符串 | | `author` | 作者 | `note.author` | 字符串(默认"未知作者") | | `created` | 创建时间 | `note.created_time` | ISO 8601 格式 | | `updated` | 更新时间 | `note.updated_time` | ISO 8601 格式 | ### 处理规则 #### 1. 已有 YAML Frontmatter 如果笔记正文顶部已包含 YAML Frontmatter(以 `---` 包裹),插件会: - **更新核心字段值**:用 Joplin 的元数据替换 4 个核心字段的值 - **保留自定义字段**:所有非核心字段保持不变 - **保持原有顺序**:核心字段和自定义字段的顺序不变 - **补全缺失字段**:如果缺少某个核心字段,按 `title → author → created → updated` 的顺序追加 **示例**: 原始 YAML: ```yaml --- custom_field: some_value title: 旧标题 author: 张三 --- 笔记正文内容... ``` 处理后: ```yaml --- custom_field: some_value title: 新标题 author: 张三 created: 2024-01-15T10:30:00.000Z updated: 2024-01-20T15:45:00.000Z --- 笔记正文内容... ``` #### 2. 无 YAML Frontmatter 如果笔记没有 YAML Frontmatter,插件会自动生成标准的 Frontmatter: ```yaml --- title: 笔记标题 author: 作者名称 created: 2024-01-15T10:30:00.000Z updated: 2024-01-20T15:45:00.000Z --- 笔记正文内容... ``` ## 三、资源处理 ### 资源链接转换 插件支持两种图片引用格式,均会进行资源链接转换: #### 3.1 Markdown 标准格式 Joplin 内部使用 `:/资源ID` 格式的资源链接,插件会将其转换为相对路径: - **Joplin 格式**:`![图片描述](:/a1b2c3d4e5f6)` - **转换后**:`![图片描述](./assets/image.png)` #### 3.2 HTML 标签格式 Joplin 内部也支持 HTML 格式的图片引用,插件会将其转换为相对路径: - **Joplin 格式**:`图片描述` - **转换后**:`图片描述` **转换规则**: - 将 `src` 属性从 Joplin 内部格式 `:/资源ID` 转换为相对路径 `./assets/文件名` - 保留所有其他属性(`alt`、`width`、`height`、`class`、`style` 等) - 层次结构模式下,相对路径基于笔记所在文件夹 **支持的 HTML 属性**: - `src` - 图片源地址(必须转换) - `alt` - 替代文本(保留原值) - `width` - 宽度(保留原值) - `height` - 高度(保留原值) - `class` - CSS 类名(保留原值) - `style` - 内联样式(保留原值) - `id` - 元素标识(保留原值) - `title` - 标题提示(保留原值) - 其他自定义属性(保留原值) **支持的 HTML 标签格式**: - 自闭合格式:`` - 非自闭合格式:`` - 单引号属性:`...` - 双引号属性:`...` - 无引号属性(部分情况):`...` **转换示例**: 示例 1 - 基本转换: ``` 输入:产品截图 输出:产品截图 ``` 示例 2 - 带多个属性: ``` 输入:架构图 输出:架构图 ``` 示例 3 - 层次结构模式: ``` 输入:流程图 输出:流程图 ``` **混合格式处理**: - 当笔记中同时存在 Markdown 格式和 HTML 格式的图片引用时,插件会分别处理 - 两种格式的图片都会正确转换为相对路径 - 保持原有的格式结构不变 ### 资源存储策略 ### 资源存储策略 #### 扁平结构模式 - 所有资源复制到导出目录下的 `assets` 目录 - 使用资源原始文件名(如果有)或 `资源ID.扩展名` - 所有笔记共享同一个 `assets` 目录 #### 层次结构模式 - 资源复制到笔记所在文件夹的 `assets` 目录 - 如果一个资源被多个笔记引用,会在每个笔记对应的 `assets` 目录中复制一份 - 确保每个笔记都有独立的资源副本 ### 资源映射机制 插件使用 `resourceMap` 存储资源 ID 到相对路径的映射: ```typescript // 扁平结构:直接使用资源 ID resourceMap.set(resourceId, "./assets/image.png"); // 层次结构:使用"笔记ID_资源ID"作为键,避免冲突 resourceMap.set(`${noteId}_${resourceId}`, "./assets/image.png"); ``` ## 四、导出选项 插件提供以下导出选项: ### 1. 导出路径样式 - **扁平结构**:所有文件在同一目录 - **层次结构**:按文件夹结构组织 ### 2. 保存合并内容 - **启用**:将所有笔记合并为一个 Markdown 文件 - **禁用**:每个笔记保存为独立的 Markdown 文件 ## 五、文件名处理 插件会自动清理文件名中的非法字符: - 替换字符:`< > : " / \ | ? *` → `_` - 保留原始文件名中的其他字符 - 笔记文件名:`{笔记标题}.md` - 资源文件名:优先使用原始名称,否则使用 `资源ID.扩展名` ## 六、技术实现细节 ### 全局缓存 插件使用全局缓存存储导出过程中的临时数据: ```typescript exportGlobalCache = { exportPathStyle: ExportPathStyle.Flat, // 导出路径样式 saveMergedContent: false, // 是否保存合并内容 content: "", // 累加的笔记内容(用于合并导出) notes: Map, // 笔记信息映射 resourceMap: Map, // 资源路径映射 folderMap: Map, // 文件夹信息映射(层次结构) noteFolderMap: Map, // 笔记-文件夹关联映射 } ``` ### 层次结构优化 在层次结构模式下,插件会在 `onInit` 阶段预加载所有文件夹信息,避免后续重复查询: ```typescript // onInit 时预加载 const allFolders = await joplin.data.get(["folders"]); for (const folder of allFolders.items) { exportGlobalCache.folderMap.set(folder.id, folder); } ``` ### 路径构建算法 从叶节点向上追溯到根节点,构建完整路径: ```typescript function buildFolderPath(folderId: string, destDir: string): string { const folderPathParts: string[] = []; let currentFolderId = folderId; while (currentFolderId) { const folder = exportGlobalCache.folderMap.get(currentFolderId); folderPathParts.unshift(sanitizeFileName(folder.title)); currentFolderId = folder.parent_id; } return path.join(destDir, ...folderPathParts); } ``` ## 七、注意事项 1. **资源关联查询**:在层次结构模式下,需要查询资源与笔记的关联关系来确定资源存储位置 2. **循环引用检测**:路径构建时检测循环引用,防止无限循环 3. **缓存清理**:每次导出完成后清理缓存,避免多次导出数据污染 4. **异步处理**:所有导出操作都是异步的,确保文件操作完成后再继续 5. **错误处理**:完善的错误处理和日志记录,便于排查问题 ## 八、示例 ### 导出单个笔记(扁平结构) 输入: ``` 笔记标题:产品需求文档 内容:包含 3 张图片 ``` 输出: ``` export/ ├── 产品需求文档.md └── assets/ ├── mockup.png ├── flowchart.png └── screenshot.png ``` ### 导出多个笔记(层次结构) 输入: ``` 工作笔记/ ├── 项目A/ │ ├── 需求文档.md │ └── 设计稿.md └── 项目B/ └── 会议记录.md ``` 输出: ``` export/ └── 工作笔记/ ├── 项目A/ │ ├── 需求文档.md │ ├── 设计稿.md │ └── assets/ │ ├── mockup.png │ └── diagram.png └── 项目B/ ├── 会议记录.md └── assets/ └── whiteboard.jpg ``` ## 参考资料 - [触发机制.md](触发机制.md) - 导出模块事件触发机制 - [插件设置.md](插件设置.md) - 插件配置说明 - [日志系统.md](日志系统.md) - 日志系统说明