# 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 格式**:``
- **转换后**:``
#### 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) - 日志系统说明