update at 2025-09-22 16:49:33
This commit is contained in:
131
architecture.md
Normal file
131
architecture.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Architecture Overview
|
||||||
|
|
||||||
|
> 本文档从整体视角拆分自 `detaildesign.md`,聚焦架构分层、核心模块职责与交互关系。补充细粒度时序与图片处理细节请参见:
|
||||||
|
> - `image-pipeline.md`
|
||||||
|
> - `render-service-blueprint.md`
|
||||||
|
> - `diagrams.md`
|
||||||
|
|
||||||
|
## 1. 分层结构
|
||||||
|
```
|
||||||
|
UI / Interaction (NotePreview)
|
||||||
|
├─ Toolbar (复制/上传/发草稿/图片集/导出/批量)
|
||||||
|
└─ 状态维护 (当前笔记、主题、代码高亮、公众号 AppId)
|
||||||
|
|
||||||
|
Rendering & Composition (ArticleRender + MarkedParser + Extensions)
|
||||||
|
├─ Markdown 预处理 (frontmatter 剥离 / 可选图片语法转换)
|
||||||
|
├─ 语法扩展 (LocalFile: wikilink 图片、文件嵌入、Excalidraw、SVG)
|
||||||
|
├─ 延迟元素缓存 (Mermaid / Excalidraw)
|
||||||
|
└─ 样式注入 (主题 + 代码高亮 + 自定义 CSS)
|
||||||
|
|
||||||
|
Assets & Settings Layer
|
||||||
|
├─ AssetsManager (themes, highlights, customCSS)
|
||||||
|
└─ NMPSettings (frontmatter key 映射 / 功能开关 / 微信配置)
|
||||||
|
|
||||||
|
Resource & Media Layer
|
||||||
|
├─ LocalImageManager (图片登记 / 上传 / 替换 / base64 嵌入)
|
||||||
|
├─ Image Conversion (WebP -> JPG wasm)
|
||||||
|
└─ html-to-image (Mermaid/Excalidraw 转 PNG)
|
||||||
|
|
||||||
|
WeChat Integration Layer
|
||||||
|
├─ Token 代理 (wxGetToken)
|
||||||
|
├─ 草稿创建 (wxAddDraft / wxAddDraftImages)
|
||||||
|
├─ 素材批量获取 (wxBatchGetMaterial)
|
||||||
|
└─ 上传图片 (wxUploadImage)
|
||||||
|
|
||||||
|
Utilities & Helpers
|
||||||
|
├─ debounce, applyCSS, waitForLayoutReady
|
||||||
|
└─ CardDataManager (代码卡片序列化/恢复)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 核心模块职责矩阵
|
||||||
|
| 模块 | 关键职责 | 输入 | 输出 | 失败模式 |
|
||||||
|
|------|----------|------|------|----------|
|
||||||
|
| NotePreview | 交互、调度、批处理 | 用户操作 / 活动文件 | 渲染触发、发布调用 | 文件为空 / 未配置公众号 |
|
||||||
|
| ArticleRender | 渲染 + 元数据 + 发布协调 | Markdown + Settings | HTML + DraftArticle | 解析错误 / token 失败 |
|
||||||
|
| MarkedParser + Extensions | Markdown Token 化扩展 | 预处理文本 | Token 树 -> HTML 片段 | 不支持语法 / 路径缺失 |
|
||||||
|
| LocalFile | Wikilink/嵌入/Excalidraw/SVG | Token.raw | 标准 HTML / 占位 | 文件不存在 / AuthKey 缺失 |
|
||||||
|
| LocalImageManager | 图片生命周期 | 图片引用集合 | 上传后的 URL 替换结果 | 上传失败 / 转码失败 |
|
||||||
|
| AssetsManager | 主题 & 高亮加载 | 文件系统 | CSS 文本 | 主题缺失 |
|
||||||
|
| NMPSettings | 全局配置 | 存储数据 | 配置实例 | AuthKey 无效 |
|
||||||
|
| WeChat API | 外部接口封装 | 请求结构 | 响应 JSON | HTTP 非 200 / errcode |
|
||||||
|
| CardDataManager | 代码块序列化 | HTML fragment | 可复制安全内容 | 序列化不匹配 |
|
||||||
|
|
||||||
|
## 3. 关键交互流程简述
|
||||||
|
### 3.1 渲染触发
|
||||||
|
1. 用户打开/切换文件 → `file-open` 事件。
|
||||||
|
2. `NotePreview.update()` → 清理旧图片缓存 → `ArticleRender.renderMarkdown()`。
|
||||||
|
3. 读取 Markdown → (frontmatter 剥离 + 可选图片语法转换) → Marked 解析扩展 → HTML section → 延迟元素二次渲染。
|
||||||
|
4. 更新 UI 下拉:主题 / 高亮 / 公众号。
|
||||||
|
|
||||||
|
### 3.2 上传图片
|
||||||
|
1. 用户点击“上传图片” → `ArticleRender.uploadImages()`。
|
||||||
|
2. 获取 token → 缓存元素转图片 → 本地/远程图片逐步上传 → DOM src 替换 → 复制 HTML 到剪贴板。
|
||||||
|
|
||||||
|
### 3.3 发布草稿
|
||||||
|
1. “发草稿” → token → 缓存元素转图片。
|
||||||
|
2. 上传本地 & 远程图片 → 封面决策(thumb_media_id/frontmatter/正文首图/默认素材)→ 构造 `DraftArticle`。
|
||||||
|
3. 调用 `wxAddDraft` → 返回 media_id。
|
||||||
|
|
||||||
|
### 3.4 发布图片集 (newspic)
|
||||||
|
与草稿类似,但构造 `DraftImages`,content 使用纯文本,图片列表为 `media_id` 数组。
|
||||||
|
|
||||||
|
## 4. 元数据 & 封面策略抽象
|
||||||
|
逻辑函数(伪):
|
||||||
|
```ts
|
||||||
|
function resolveCover(originalMarkdown: string, fmCover?: string, thumbMediaId?: string): string | null {
|
||||||
|
if (thumbMediaId) return null; // 已有 media_id 不再上传本地封面
|
||||||
|
if (fmCover) return fmCover; // frontmatter 指定
|
||||||
|
const body = stripFrontmatter(originalMarkdown);
|
||||||
|
const candidates = collectImageOrder(body); // wikilink + markdown
|
||||||
|
return candidates.length ? `![[${candidates[0]}]]` : null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 图片收集策略统一要点
|
||||||
|
- Wikilink:解析时即登记(路径 → vault file)。
|
||||||
|
- Markdown 图片:可配置预处理转 Wikilink,以复用同一逻辑(避免重复正则后处理)。
|
||||||
|
- DOM 补偿策略(如后续新渲染管线未登记图片):可在上传前扫描 `<img>` 回填缺失项。
|
||||||
|
|
||||||
|
## 6. 错误处理分层
|
||||||
|
| 层 | 代表错误 | 处理策略 |
|
||||||
|
|----|----------|----------|
|
||||||
|
| 输入层 | 没有活动文件 | 静默返回 / UI 提示 |
|
||||||
|
| 渲染层 | Marked 解析异常 | try/catch 显示“渲染失败” HTML |
|
||||||
|
| 媒体层 | 图片上传失败 | Notice + 继续其他图片 |
|
||||||
|
| 转码层 | wasm 未加载 | 降级:保持 webp 原样尝试上传 |
|
||||||
|
| 发布层 | token 获取失败 | 抛异常终止流程 |
|
||||||
|
| API 层 | errcode != 0 | 聚合错误信息 + 建议手动复制 |
|
||||||
|
|
||||||
|
## 7. 性能关注点
|
||||||
|
| 项目 | 当前 | 潜在优化 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 图片上传 | 串行 | 并发队列 + 重试退避 |
|
||||||
|
| Mermaid/Excalidraw | 前台转 PNG | 结果缓存 / worker 线程 |
|
||||||
|
| 主题加载 | 每次刷新全部组合 | 按需拼接 + 缓存 hash |
|
||||||
|
| Markdown 解析 | 单线程 | 增量渲染(监听 diff) |
|
||||||
|
|
||||||
|
## 8. 可扩展挂载点 (未来)
|
||||||
|
| Hook | 说明 | 预期参数 |
|
||||||
|
|------|------|----------|
|
||||||
|
| beforeRender | Markdown 转 HTML 前 | { markdown, file } |
|
||||||
|
| afterRender | HTML 完成但未上传 | { html, metadata } |
|
||||||
|
| beforeUploadImages | 上传前 | { images[] } |
|
||||||
|
| afterUploadImages | 上传后 | { mapping } |
|
||||||
|
| beforePublish | 调用 wxAddDraft 前 | { draft } |
|
||||||
|
| afterPublish | 成功返回 | { media_id } |
|
||||||
|
|
||||||
|
## 9. 风险摘录
|
||||||
|
- 多管线并存(旧 ArticleRender vs 新 RenderService 占位)易导致图片未登记 → 需统一抽象。
|
||||||
|
- 自动封面选取对远程图片/非图片扩展尚无过滤完备策略。
|
||||||
|
- 正则预处理与 Marked tokenizer 逻辑耦合度高,重构时需单测保护。
|
||||||
|
|
||||||
|
## 10. 索引 / 交叉引用
|
||||||
|
| 文档 | 内容概述 |
|
||||||
|
|------|----------|
|
||||||
|
| detaildesign.md | 全量深度设计描述 |
|
||||||
|
| image-pipeline.md | 图片解析、上传、封面、正则清单 |
|
||||||
|
| render-service-blueprint.md | 新一代渲染服务设计计划 |
|
||||||
|
| diagrams.md | 架构 / 时序 / 类关系图 (Mermaid) |
|
||||||
|
|
||||||
|
---
|
||||||
|
> 若修改架构(新增层或跨层依赖),需同步更新本文件与 `diagrams.md`。
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# note-to-mp 设计文档 (Detail Design)
|
# note-to-mp 设计文档 (Detail Design)
|
||||||
|
|
||||||
|
> 拆分文档索引:
|
||||||
|
> - 架构总览:`architecture.md`
|
||||||
|
> - 图片管线:`image-pipeline.md`
|
||||||
|
> - 渲染服务蓝图:`render-service-blueprint.md`
|
||||||
|
> - 图示 (Mermaid):`diagrams.md`
|
||||||
|
> 本文件保留全量细节,增量演进请同步上述子文档。
|
||||||
|
|
||||||
## 1. 背景
|
## 1. 背景
|
||||||
为满足从 Obsidian 笔记到微信公众号文章的高质量发布需求,插件需要:
|
为满足从 Obsidian 笔记到微信公众号文章的高质量发布需求,插件需要:
|
||||||
- 支持多种图片书写形式(Wikilink 与标准 Markdown)。
|
- 支持多种图片书写形式(Wikilink 与标准 Markdown)。
|
||||||
|
|||||||
155
diagrams.md
Normal file
155
diagrams.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Diagrams (Mermaid)
|
||||||
|
|
||||||
|
> 动态架构与主要交互可视化。与文字说明对应:`architecture.md` / `image-pipeline.md` / `render-service-blueprint.md`。
|
||||||
|
|
||||||
|
## 1. 模块类图 (当前实现概览)
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class NotePreview {
|
||||||
|
+renderMarkdown()
|
||||||
|
+uploadImages()
|
||||||
|
+postArticle()
|
||||||
|
+postImages()
|
||||||
|
+exportHTML()
|
||||||
|
}
|
||||||
|
class ArticleRender {
|
||||||
|
+renderMarkdown(file)
|
||||||
|
+getMetadata()
|
||||||
|
+uploadImages(appid)
|
||||||
|
+postArticle(appid,cover?)
|
||||||
|
+postImages(appid)
|
||||||
|
+exportHTML()
|
||||||
|
}
|
||||||
|
class LocalImageManager {
|
||||||
|
+setImage(path,info)
|
||||||
|
+uploadLocalImage(token,vault)
|
||||||
|
+uploadRemoteImage(root,token)
|
||||||
|
+replaceImages(root)
|
||||||
|
}
|
||||||
|
class LocalFile {
|
||||||
|
+markedExtension()
|
||||||
|
}
|
||||||
|
class AssetsManager {
|
||||||
|
+loadAssets()
|
||||||
|
+getTheme(name)
|
||||||
|
+getHighlight(name)
|
||||||
|
}
|
||||||
|
class NMPSettings {
|
||||||
|
+wxInfo
|
||||||
|
+authKey
|
||||||
|
+enableMarkdownImageToWikilink
|
||||||
|
}
|
||||||
|
class WeChatAPI {
|
||||||
|
+wxGetToken()
|
||||||
|
+wxAddDraft()
|
||||||
|
+wxUploadImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
NotePreview --> ArticleRender
|
||||||
|
ArticleRender --> LocalImageManager
|
||||||
|
ArticleRender --> AssetsManager
|
||||||
|
ArticleRender --> NMPSettings
|
||||||
|
ArticleRender --> WeChatAPI
|
||||||
|
ArticleRender --> LocalFile
|
||||||
|
LocalFile --> LocalImageManager
|
||||||
|
NotePreview --> NMPSettings
|
||||||
|
NotePreview --> AssetsManager
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 发布草稿时序图
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as User
|
||||||
|
participant NP as NotePreview
|
||||||
|
participant AR as ArticleRender
|
||||||
|
participant WX as WeChatAPI
|
||||||
|
participant LIM as LocalImageManager
|
||||||
|
|
||||||
|
U->>NP: 点击 发草稿
|
||||||
|
NP->>AR: postArticle(appid)
|
||||||
|
AR->>WX: wxGetToken(authKey, appid)
|
||||||
|
WX-->>AR: token
|
||||||
|
AR->>AR: cachedElementsToImages()
|
||||||
|
AR->>LIM: uploadLocalImage(token)
|
||||||
|
LIM-->>AR: local media_id(s)
|
||||||
|
AR->>LIM: uploadRemoteImage(token)
|
||||||
|
LIM-->>AR: remote media_id(s)
|
||||||
|
AR->>LIM: replaceImages()
|
||||||
|
AR->>AR: resolve cover (frontmatter / first image / default)
|
||||||
|
AR->>WX: wxAddDraft(draft JSON)
|
||||||
|
WX-->>AR: media_id | err
|
||||||
|
AR-->>NP: 结果
|
||||||
|
NP-->>U: 成功 / 失败提示
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 图片上传流程图
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Start UploadImages] --> B{AuthKey/AppId?}
|
||||||
|
B -- No --> Z[Throw Error]
|
||||||
|
B -- Yes --> C[Get Token]
|
||||||
|
C --> D[cachedElementsToImages]
|
||||||
|
D --> E[uploadLocalImage]
|
||||||
|
E --> F[uploadRemoteImage]
|
||||||
|
F --> G[replaceImages]
|
||||||
|
G --> H[Copy HTML to Clipboard]
|
||||||
|
H --> I[End]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 自动封面推断逻辑
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Need Cover?] -->|No| Z[Skip]
|
||||||
|
A -->|Yes| B[Strip Frontmatter]
|
||||||
|
B --> C[Scan Markdown Images]
|
||||||
|
B --> D[Scan Wikilink Images]
|
||||||
|
C --> E[Merge Candidates]
|
||||||
|
D --> E[Merge Candidates]
|
||||||
|
E --> F{Any Candidate?}
|
||||||
|
F -- No --> Z[Cover stays empty]
|
||||||
|
F -- Yes --> G[Sort by index]
|
||||||
|
G --> H[Select first -> cover wikilink]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 未来 RenderService Pipeline 图
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
L[Loader] --> FM[Frontmatter]
|
||||||
|
FM --> PP[Preprocessors]
|
||||||
|
PP --> P[Parser]
|
||||||
|
P --> TR[Transformers]
|
||||||
|
TR --> RI[ResourceIndex]
|
||||||
|
RI --> R[Renderer]
|
||||||
|
R --> PO[Postprocessors]
|
||||||
|
PO --> EX[Exporters]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 并发上传示意 (未来优化)
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[images[]] --> B[Partition into tasks]
|
||||||
|
B --> C[Promise Pool (N=4)]
|
||||||
|
C --> D[Upload Task]
|
||||||
|
D --> E{Success?}
|
||||||
|
E -- No --> R[Retry with backoff]
|
||||||
|
E -- Yes --> F[Collect media_id]
|
||||||
|
R --> C
|
||||||
|
F --> G[All Done]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 状态机概览 (发布按钮)
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Idle
|
||||||
|
Idle --> Uploading : 点击 上传/发布
|
||||||
|
Uploading --> Publishing : 草稿模式
|
||||||
|
Uploading --> Completed : 仅上传
|
||||||
|
Publishing --> Completed : 响应成功
|
||||||
|
Publishing --> Failed : 接口错误
|
||||||
|
Uploading --> Failed : 资源错误
|
||||||
|
Failed --> Idle : 用户重试
|
||||||
|
Completed --> Idle : 新文件切换
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
需要我将这些图嵌入到 README 的一个“开发者”章节吗?可以继续提出。
|
||||||
158
image-pipeline.md
Normal file
158
image-pipeline.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# Image Pipeline & Cover Strategy
|
||||||
|
|
||||||
|
> 对应代码核心:`src/markdown/local-file.ts`, `src/article-render.ts` 以及相关预处理逻辑。
|
||||||
|
> 若需总体结构参见 `architecture.md`,更广设计参见 `detaildesign.md`。
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
- 统一处理所有图片来源:Wikilink、标准 Markdown、远程 URL、Base64、生成图 (Mermaid/Excalidraw/SVG)。
|
||||||
|
- 保证上传后 HTML 使用微信可访问的正式 URL。
|
||||||
|
- 自动封面选取:未指定时从正文首图推断。
|
||||||
|
- WebP → JPG 转换保障兼容性。
|
||||||
|
|
||||||
|
## 2. 图片来源与归一化
|
||||||
|
| 来源 | 输入示例 | 归一化策略 | 备注 |
|
||||||
|
|------|----------|-----------|------|
|
||||||
|
| Wikilink | `![[foo.png]]` / `![[foo.png|120x80]]` | Marked 扩展直接生成 `<img src="vault://...">` 并登记 | 支持尺寸 / 位置扩展语法 |
|
||||||
|
| Markdown | `` | (可选) 预处理转换成 `![[foo.png]]` | 依赖设置:`enableMarkdownImageToWikilink` |
|
||||||
|
| 远程 URL | `<img src="https://...">` | 解析为远程上传任务 | 跳过已是微信域名 |
|
||||||
|
| Base64 | `<img src="data:image/png;base64,...">` | 解析成 Blob 上传 | 生成虚拟 id 构造文件名 |
|
||||||
|
| Mermaid | 源码块 → SVG/DOM | 转 PNG (html-to-image) / 留 SVG | 替换占位容器 |
|
||||||
|
| Excalidraw | `![[diagram.excalidraw]]` | 远程接口取 SVG 或转 PNG | 需 AuthKey |
|
||||||
|
| SVG 文件 | `![[icon.svg]]` | 内联 SVG 内容 | 直接嵌入,不上传 |
|
||||||
|
|
||||||
|
## 3. 关键数据结构
|
||||||
|
```ts
|
||||||
|
interface ImageInfo {
|
||||||
|
resUrl: string; // 初始占位或本地资源 URL (vault path 映射)
|
||||||
|
filePath: string; // Vault 内实际文件路径
|
||||||
|
url: string | null; // 上传后微信返回 URL
|
||||||
|
media_id: string | null; // 上传后素材 ID(type=image 时)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
管理容器:`Map<string, ImageInfo>`,键值为 `resUrl`(初始唯一标识)。
|
||||||
|
|
||||||
|
## 4. 收集阶段
|
||||||
|
1. Marked 扩展 (`LocalFile.markedExtension`) 在 token 遍历时识别 LocalImage。
|
||||||
|
2. 根据 wikilink / 尺寸参数解析出真实 vault 文件路径:`assetsManager.getResourcePath()`。
|
||||||
|
3. 调用 `LocalImageManager.setImage(res.resUrl, info)` 建档。
|
||||||
|
4. Markdown 图片(标准语法)若转换启用,提前转成 wikilink 进入同一逻辑;否则需未来扩展 tokenizer 直接登记。
|
||||||
|
|
||||||
|
## 5. 上传阶段
|
||||||
|
顺序:
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[准备 token] --> B[cachedElementsToImages]
|
||||||
|
B --> C[uploadLocalImage]
|
||||||
|
C --> D[uploadRemoteImage]
|
||||||
|
D --> E[replaceImages]
|
||||||
|
E --> F[输出 HTML / 复制 / 发布]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.1 本地图片 `uploadLocalImage`
|
||||||
|
流程:
|
||||||
|
- 遍历登记表,读取 vault 文件二进制。
|
||||||
|
- 若扩展名 `.webp` → wasm (`PrepareImageLib`) 转 JPG。
|
||||||
|
- 调用 `wxUploadImage(blob, filename, token, type)`。
|
||||||
|
- 更新 `ImageInfo.url` / `media_id`。
|
||||||
|
- 失败:`Notice + console.error`,继续后续项。
|
||||||
|
|
||||||
|
### 5.2 远程图片 `uploadRemoteImage`
|
||||||
|
匹配 `<img src="http...">`:
|
||||||
|
- 跳过已在微信域名(`mmbiz.qpic.cn`)。
|
||||||
|
- 下载 → Blob → (webp 转换) → 上传。
|
||||||
|
- 更新 Map,键为原始 src。
|
||||||
|
Base64:
|
||||||
|
- 解析 data URI → 生成 Blob/扩展名 → 上传。
|
||||||
|
|
||||||
|
### 5.3 替换阶段 `replaceImages`
|
||||||
|
- 遍历 DOM `<img>` → 查询 Map → 使用上传后 `url` 覆盖 `src`。
|
||||||
|
- 未登记:输出警告(潜在渲染管线遗漏)。
|
||||||
|
|
||||||
|
## 6. 自动封面策略
|
||||||
|
触发点:`getMetadata()` 中,若 frontmatter 未给出封面且无 `thumb_media_id`。步骤:
|
||||||
|
1. 使用 `originalMarkdown`(含 frontmatter 原始文本拷贝)。
|
||||||
|
2. 去除 frontmatter 块。
|
||||||
|
3. 分别用正则匹配:
|
||||||
|
- Markdown:`/!\[[^\]]*\]\(([^)\s]+)\)/g`
|
||||||
|
- Wikilink:`/!\[\[(.+?)\]\]/g`
|
||||||
|
4. 过滤远程 URL(仅保留本地引用或无协议路径)。
|
||||||
|
5. 生成候选 `{ idx, basename }` 列表,按出现位置排序。
|
||||||
|
6. 第一项 → `![[basename]]` 写入 `res.cover`。
|
||||||
|
|
||||||
|
Edge Cases:
|
||||||
|
| 情况 | 处理 |
|
||||||
|
|------|------|
|
||||||
|
| 所有图片都是 http(s) | 不作为封面候选 |
|
||||||
|
| 文件名带 query/hash | 使用前切分去除 `?` `#` |
|
||||||
|
| markdown 图片后缀非图片 | 跳过 |
|
||||||
|
| 重复图片 | 以最先出现的为准 |
|
||||||
|
|
||||||
|
## 7. 正则索引
|
||||||
|
| 目的 | 正则 | 描述 |
|
||||||
|
|------|------|------|
|
||||||
|
| Frontmatter 删除 | `/^(---)$.+?^(---)$.+?/ims` | 首段 YAML |
|
||||||
|
| Markdown 图片 | `/!\[[^\]]*\]\(([^)\s]+)\)/g` | 捕获路径 group1 |
|
||||||
|
| Wikilink 图片 | `/!\[\[(.+?)\]\]/g` | 捕获内部资源 |
|
||||||
|
| WebP 检测 | `/\.webp$/i` | 扩展判断 |
|
||||||
|
| Base64 前缀 | `^data:image/` | 判断内嵌图 |
|
||||||
|
|
||||||
|
## 8. 失败与恢复策略
|
||||||
|
| 环节 | 失败示例 | 策略 |
|
||||||
|
|------|----------|------|
|
||||||
|
| wasm 初始化 | 网络慢 / 未加载 | 继续使用原 webp 上传(失败概率增加) |
|
||||||
|
| 本地文件找不到 | 路径错误 / 移动 | 在控制台警告,图片跳过 |
|
||||||
|
| 上传 403/401 | token 失效 | 抛异常终止流程(需重新获取 token) |
|
||||||
|
| 单图 errcode !=0 | 大小/格式不合规 | Notice 提示 + 继续其他 |
|
||||||
|
| 替换未命中 | DOM 没登记 | 记录警告,建议加入补偿扫描 |
|
||||||
|
|
||||||
|
## 9. 性能优化方向
|
||||||
|
| 问题 | 现状 | 改进 |
|
||||||
|
|------|------|------|
|
||||||
|
| 上传串行 | 顺序 await | 并发池 (N=3~5) + 重试退避 |
|
||||||
|
| WebP 转换阻塞 | 单线程 | 预热 wasm + 批量并行 |
|
||||||
|
| 远程下载串行 | 每图 await | 统一收集 Promise.all 控制并发 |
|
||||||
|
| 首图扫描重复 | 重新正则两次 | 合并一次统一匹配 pipeline |
|
||||||
|
|
||||||
|
## 10. 与发布流程的接口点
|
||||||
|
| 函数 | 上下游 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `cachedElementsToImages()` | 延迟图形 → 图片 | 在上传前确保所有图形成为 <img> 可被收集 |
|
||||||
|
| `uploadLocalImage()` | 发布前 | 更新本地图片 URL/media_id |
|
||||||
|
| `uploadRemoteImage()` | 发布前 | 远程/内嵌资源统一化 |
|
||||||
|
| `replaceImages()` | 发布前 | DOM HTML 成为最终状态 |
|
||||||
|
| `getArticleContent()` | 发布 / 复制 / 导出 | 输出含最终图片 URL 的 HTML |
|
||||||
|
|
||||||
|
## 11. 未来扩展
|
||||||
|
| 需求 | 设想 |
|
||||||
|
|------|------|
|
||||||
|
| CDN 直传 | 微信前置 → 自建或 OSS 中转 |
|
||||||
|
| 图片压缩 | 上传前可选压缩 (canvas/wasm) |
|
||||||
|
| 失效重传 | 保存上传映射 + 校验缺失再补传 |
|
||||||
|
| 分块上传 | 大图分片(若接口支持) |
|
||||||
|
| 图片校验 | MD5 去重避免重复上传 |
|
||||||
|
|
||||||
|
## 12. 快速检查清单 (Debug Checklist)
|
||||||
|
- 图片是否在 `LocalImageManager.images` Map 中?
|
||||||
|
- 是否执行了 `cachedElementsToImages()`(Mermaid/Excalidraw)?
|
||||||
|
- 上传后 `media_id` 是否为空?(格式/大小不合规)
|
||||||
|
- DOM 替换后 `<img src>` 是否为微信域?
|
||||||
|
- 自动封面未生效?检查:frontmatter cover / 有无本地首图 / 正则是否截获远程 URL。
|
||||||
|
|
||||||
|
## 13. 示例
|
||||||
|
输入 Markdown:
|
||||||
|
```
|
||||||
|
---
|
||||||
|
title: 示例
|
||||||
|
---
|
||||||
|

|
||||||
|
正文...
|
||||||
|
![[b-second.jpg]]
|
||||||
|
```
|
||||||
|
处理:
|
||||||
|
1. frontmatter 去除后扫描:首个匹配为 Markdown 图片 a.png → 自动封面 `![[a.png]]`。
|
||||||
|
2. a.png 转 wikilink (开启转换时) → 登记;b-second.jpg 登记。
|
||||||
|
3. 上传顺序:a.png → b-second.jpg。
|
||||||
|
4. 最终 HTML `<img src="https://mmbiz.qpic.cn/...">`。
|
||||||
|
|
||||||
|
---
|
||||||
|
如需补充“并发上传样例代码”或“封面过滤策略扩展”,请提出。
|
||||||
208
render-service-blueprint.md
Normal file
208
render-service-blueprint.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# RenderService Blueprint
|
||||||
|
|
||||||
|
> 目的:规划下一代渲染服务以替换(或包装)现有 `ArticleRender` + `MarkedParser` 组合,降低耦合,支持插件化与增量演进。
|
||||||
|
> 关联文档:`architecture.md`, `image-pipeline.md`, `detaildesign.md`。
|
||||||
|
|
||||||
|
## 1. 现状问题 (Current Pain Points)
|
||||||
|
| 问题 | 说明 | 影响 |
|
||||||
|
|------|------|------|
|
||||||
|
| 逻辑集中 | `ArticleRender` 同时负责读取、解析、样式拼接、延迟元素、上传协调 | 难测试 / 难替换子环节 |
|
||||||
|
| 隐式状态 | `originalMarkdown`, `cachedElements` 内部字段耦合流程顺序 | 外部无法复用或二次解析 |
|
||||||
|
| 图片收集耦合 Marked 扩展 | 依赖前置 wikilink 转换 | 新语法添加需改内核 |
|
||||||
|
| 缺少中间 IR | 无法对 Token/AST 做多阶段转换 | 功能扩展(如统计、Lint)困难 |
|
||||||
|
| 发布耦合渲染 | 上传/发布 API 与渲染混合 | 交互模式无法独立测试 |
|
||||||
|
| 单线程串行 | 图形生成、图片上传均串行 | 性能受限 |
|
||||||
|
|
||||||
|
## 2. 设计目标 (Design Goals)
|
||||||
|
| 目标 | 描述 | 衡量指标 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 分层解耦 | 渲染、转换、资源收集、发布分离 | 新增图片语法无需改核心类 |
|
||||||
|
| 插件式中间件 | 可在 parse → transform → render 各阶段注入 | 插件注册 API 清晰 |
|
||||||
|
| 可测试 | 纯函数/无副作用阶段隔离 | 单元测试覆盖率提升 |
|
||||||
|
| IR 标准化 | 生成统一 AST / 节点类型 | 后续可序列化/缓存 |
|
||||||
|
| 并发能力 | 图形/上传并发控制 | 大图文耗时下降 |
|
||||||
|
| 可观测性 | 事件 & 钩子 | before/after metrics 输出 |
|
||||||
|
|
||||||
|
## 3. 拟议分层 (Proposed Layers)
|
||||||
|
```
|
||||||
|
RenderPipeline
|
||||||
|
├─ Loader (读取源 Markdown / 资源定位)
|
||||||
|
├─ Frontmatter (解析 + Meta Store)
|
||||||
|
├─ Preprocessors[] (文本级转换:图片语法、短代码、宏)
|
||||||
|
├─ Parser (Markdown -> AST 统一节点树)
|
||||||
|
├─ Transformers[] (AST 级:节点重写、图片规范化、封面推断)
|
||||||
|
├─ ResourceIndex (图片/图形/嵌入引用索引构建)
|
||||||
|
├─ Renderer (AST -> HTML Fragment)
|
||||||
|
├─ Postprocessors[] (HTML 级:内联样式、链接修复、统计)
|
||||||
|
└─ Exporters (HTML/Text/JSON/WeChatDraft)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 关键接口 (Core Interfaces - Draft)
|
||||||
|
```ts
|
||||||
|
interface RenderContext {
|
||||||
|
file: TFile;
|
||||||
|
raw: string; // 原始 Markdown
|
||||||
|
frontmatter?: Record<string, any>;
|
||||||
|
meta: WeChatArticleMeta; // 标题/作者/封面等
|
||||||
|
ast?: MdAstRoot; // 标准化 AST
|
||||||
|
resources: ResourceIndex; // 图片/图形等
|
||||||
|
html?: string; // 渲染产物
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
flags: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineStage {
|
||||||
|
name: string;
|
||||||
|
run(ctx: RenderContext): Promise<void> | void;
|
||||||
|
order?: number; // 可选执行排序
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceIndex {
|
||||||
|
images: ImageCandidate[];
|
||||||
|
diagrams: DiagramCandidate[];
|
||||||
|
embeds: EmbedRef[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageCandidate {
|
||||||
|
id: string; // 稳定标识
|
||||||
|
kind: 'local'|'remote'|'base64'|'generated';
|
||||||
|
original: string; // 原始文本或路径
|
||||||
|
basename?: string;
|
||||||
|
vaultPath?: string;
|
||||||
|
position: number; // 在 raw 中的位置,用于封面决策
|
||||||
|
meta?: Record<string, any>; // 尺寸/对齐等
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 执行模型 (Execution Model)
|
||||||
|
```ts
|
||||||
|
class RenderService {
|
||||||
|
private stages: PipelineStage[] = [];
|
||||||
|
use(stage: PipelineStage) { this.stages.push(stage); }
|
||||||
|
async render(file: TFile): Promise<RenderContext> {
|
||||||
|
const ctx = createInitialContext(file);
|
||||||
|
for (const s of sortByOrder(this.stages)) {
|
||||||
|
try { await s.run(ctx); } catch (e) { ctx.diagnostics.push({stage: s.name, error: e}); if (isFatal(e)) break; }
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 示例阶段实现草稿
|
||||||
|
| Stage | 说明 | 是否必须 |
|
||||||
|
|-------|------|----------|
|
||||||
|
| loader | 读取文件与 raw | 是 |
|
||||||
|
| frontmatter | 解析 YAML -> ctx.frontmatter / ctx.meta 预填 | 是 |
|
||||||
|
| markdownImageCompat | Markdown 图片 -> 统一节点形式 | 可选 |
|
||||||
|
| shortcodeGallery | 解析 gallery → 多个 image 节点 | 可选 |
|
||||||
|
| parser | Marked/Remark → AST | 是 |
|
||||||
|
| imageCollect | AST 遍历收集图片,写入 ctx.resources.images | 是 |
|
||||||
|
| coverInfer | 若无封面,从 images 按 position 选首图 | 可选 |
|
||||||
|
| renderHTML | AST -> HTML 字符串 | 是 |
|
||||||
|
| styleInline | 合成主题/高亮,自定义 CSS 注入 | 可选 |
|
||||||
|
| finalize | 产物整理 / hash / 缓存存储 | 可选 |
|
||||||
|
|
||||||
|
## 7. AST 规范 (Simplified)
|
||||||
|
```ts
|
||||||
|
type MdNode = Paragraph | Heading | Code | Image | Link | List | Blockquote | Html | ThematicBreak | Container;
|
||||||
|
interface Image { type:'image'; id:string; alt:string; url:string; title?:string; meta?:{width?:number;height?:number;align?:string;} }
|
||||||
|
```
|
||||||
|
可基于 remark/unified 生态替换当前 marked,或构造最小抽象后桥接。
|
||||||
|
|
||||||
|
## 8. 事件 & Hook 设计
|
||||||
|
```ts
|
||||||
|
interface RenderHooks {
|
||||||
|
on(stage: string, fn: (ctx: RenderContext) => void): void;
|
||||||
|
emit(stage: string, ctx: RenderContext): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
预定义事件:`before:stageName` / `after:stageName` / `error:stageName`。
|
||||||
|
|
||||||
|
## 9. 并发策略
|
||||||
|
- 图片上传与图形生成移出核心渲染,进入 `PublisherService`。
|
||||||
|
- `PublisherService.publish(ctx, opts)`:接受 RenderContext,内部:
|
||||||
|
1. 对 `ctx.resources.images` 并发池(PromisePool)上传。
|
||||||
|
2. 替换 `ctx.html` 中引用。
|
||||||
|
3. 构建 `DraftArticle` / `DraftImages`。
|
||||||
|
|
||||||
|
## 10. 迁移计划 (Phased Migration)
|
||||||
|
| 阶段 | 内容 | 验证点 |
|
||||||
|
|------|------|--------|
|
||||||
|
| P0 | 搭建 RenderService 空管线 + 复用旧 ArticleRender 结果 | 构建无回归 |
|
||||||
|
| P1 | 拆出 loader/frontmatter/parser/coverInfer | 旧行为对比 snapshot |
|
||||||
|
| P2 | 新 AST + imageCollect,弃用 wikilink 转换 hack | 图片计数稳定 |
|
||||||
|
| P3 | 发布逻辑重构到 PublisherService | 草稿发布一致 |
|
||||||
|
| P4 | Hook/插件系统开放 | 外部扩展示例 |
|
||||||
|
| P5 | 并发上传 + 缓存 | 性能基线下降 |
|
||||||
|
|
||||||
|
## 11. 回滚策略
|
||||||
|
- 每阶段保留配置开关:`useLegacyRenderer`。
|
||||||
|
- 出现渲染差异:比较 ctx.html 与 legacy.html diff 提示。
|
||||||
|
- 发布失败回退:走旧 `ArticleRender.postArticle`。
|
||||||
|
|
||||||
|
## 12. 测试策略
|
||||||
|
| 测试 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| Snapshot 渲染 | 同一 Markdown 输入旧 vs 新 HTML 对比 |
|
||||||
|
| AST 结构 | 断言图片/标题节点数量与顺序 |
|
||||||
|
| 封面选择 | 多组合(frontmatter + 混合图片顺序) |
|
||||||
|
| Hook 调用 | 注册 mock 钩子计次 |
|
||||||
|
| 并发上传 | 人工注入延迟 → 顺序与最终替换正确 |
|
||||||
|
|
||||||
|
## 13. 指标与可观测
|
||||||
|
- ctx.diagnostics 数量与类型统计。
|
||||||
|
- 阶段耗时:`performance.now()` 差值注入 ctx.flags。
|
||||||
|
- 上传耗时 / 失败率。
|
||||||
|
|
||||||
|
## 14. 安全考量
|
||||||
|
- Stage 插件沙箱:限制访问仅上下文公开字段。
|
||||||
|
- 阶段超时(可选):超过阈值标记 warning。
|
||||||
|
- HTML 输出再次 sanitize。
|
||||||
|
|
||||||
|
## 15. API 草案
|
||||||
|
```ts
|
||||||
|
const rs = new RenderService();
|
||||||
|
rs.use(loader());
|
||||||
|
rs.use(frontmatter());
|
||||||
|
rs.use(markdownImageCompat({ enable: settings.enableMarkdownImageToWikilink }));
|
||||||
|
rs.use(parser({ engine:'remark' }));
|
||||||
|
rs.use(imageCollect());
|
||||||
|
rs.use(coverInfer());
|
||||||
|
rs.use(renderHTML({ themeProvider, highlightProvider }));
|
||||||
|
rs.use(styleInline());
|
||||||
|
|
||||||
|
const ctx = await rs.render(activeFile);
|
||||||
|
const draft = await publisher.publish(ctx, { type:'article', coverMode:'auto' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16. 依赖与选型比较
|
||||||
|
| 方向 | 方案 A | 方案 B | 选择建议 |
|
||||||
|
|------|-------|-------|----------|
|
||||||
|
| Markdown AST | remark/unified | 继续 marked + 自建 AST 映射 | remark 更标准;初期可混合 |
|
||||||
|
| Hook 实现 | 事件总线 (mitt) | 简单数组回调 | 先内建数组回调,后期引入库 |
|
||||||
|
| 并发控制 | p-limit | 自写 PromisePool | p-limit 简洁可靠 |
|
||||||
|
|
||||||
|
## 17. 风险 & 缓解
|
||||||
|
| 风险 | 缓解 |
|
||||||
|
|------|------|
|
||||||
|
| AST 转换差异导致样式变化 | Snapshot + 逐阶段灰度 |
|
||||||
|
| 性能倒退 | 阶段耗时基线监控;必要时跳过冗余阶段 |
|
||||||
|
| 插件滥用 Hook | 权限白名单 / 文档规范 |
|
||||||
|
| 并发上传触发限流 | 设置最大并发 + 429 重试 |
|
||||||
|
|
||||||
|
## 18. 成功判定 (Success Criteria)
|
||||||
|
- 同一输入 50 篇示例笔记,新旧 HTML 差异行 <= 2%(忽略动态 id)。
|
||||||
|
- 图片收集数量一致,封面判定一致率 100%。
|
||||||
|
- 20+ 图片大文档总发布耗时下降 ≥25%。
|
||||||
|
- 可插拔 demo:增加一个统计字数的 Stage 无需改核心。
|
||||||
|
|
||||||
|
## 19. 下一步行动 (Action Items)
|
||||||
|
1. 建立 `RenderService` 目录与最小类骨架。
|
||||||
|
2. 搬迁读取/frontmatter/封面逻辑并保留旧 API 外壳。
|
||||||
|
3. 引入 AST(先轻量:仅 Image/Heading/Paragraph)。
|
||||||
|
4. Snapshot 测试脚手架。
|
||||||
|
5. 性能基线脚本:统计渲染 + 上传耗时。
|
||||||
|
|
||||||
|
---
|
||||||
|
后续若需要,我可以直接生成骨架代码与测试样例,请指示。
|
||||||
@@ -54,6 +54,7 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
markedParser: MarkedParser;
|
markedParser: MarkedParser;
|
||||||
cachedElements: Map<string, string> = new Map();
|
cachedElements: Map<string, string> = new Map();
|
||||||
debouncedRenderMarkdown: (...args: any[]) => void;
|
debouncedRenderMarkdown: (...args: any[]) => void;
|
||||||
|
originalMarkdown: string | null = null; // 保存去除前处理前的原始 Markdown
|
||||||
|
|
||||||
constructor(app: App, itemView: ItemView, styleEl: HTMLElement, articleDiv: HTMLDivElement) {
|
constructor(app: App, itemView: ItemView, styleEl: HTMLElement, articleDiv: HTMLDivElement) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
@@ -162,10 +163,28 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
else {
|
else {
|
||||||
md = '没有可渲染的笔记或文件不支持渲染';
|
md = '没有可渲染的笔记或文件不支持渲染';
|
||||||
}
|
}
|
||||||
|
this.originalMarkdown = md; // 保存原始内容(含 frontmatter)供封面/摘要自动提取
|
||||||
if (md.startsWith('---')) {
|
if (md.startsWith('---')) {
|
||||||
md = md.replace(FRONT_MATTER_REGEX, '');
|
md = md.replace(FRONT_MATTER_REGEX, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将标准 markdown 图片语法转为 wikilink 语法,便于现有 LocalImageManager 识别
|
||||||
|
if (this.settings.enableMarkdownImageToWikilink) {
|
||||||
|
// 匹配 ;不跨行;忽略包含空格的 URL 末尾注释
|
||||||
|
// 捕获路径和文件名,文件名取最后一段
|
||||||
|
md = md.replace(/!\[[^\]]*\]\(([^)\s]+)\)/g, (full, p1) => {
|
||||||
|
try {
|
||||||
|
// 去掉可能的 query/hash
|
||||||
|
const clean = p1.split('#')[0].split('?')[0];
|
||||||
|
const filename = clean.split('/').pop();
|
||||||
|
if (!filename) return full; // 无法解析
|
||||||
|
// 仅当是常见图片扩展时才替换
|
||||||
|
if (!filename.match(/\.(png|jpe?g|gif|bmp|webp|svg|tiff)$/i)) return full;
|
||||||
|
return `![[${filename}]]`;
|
||||||
|
} catch { return full; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.articleHTML = await this.markedParser.parse(md);
|
this.articleHTML = await this.markedParser.parse(md);
|
||||||
this.setStyle(this.getCSS());
|
this.setStyle(this.getCSS());
|
||||||
this.setArticle(this.articleHTML);
|
this.setArticle(this.articleHTML);
|
||||||
@@ -233,11 +252,14 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
if (metadata?.frontmatter) {
|
if (metadata?.frontmatter) {
|
||||||
const keys = this.assetsManager.expertSettings.frontmatter;
|
const keys = this.assetsManager.expertSettings.frontmatter;
|
||||||
const frontmatter = metadata.frontmatter;
|
const frontmatter = metadata.frontmatter;
|
||||||
res.title = this.getFrontmatterValue(frontmatter, keys.title);
|
// frontmatter 优先:如果存在 title/author 则直接取之
|
||||||
res.author = this.getFrontmatterValue(frontmatter, keys.author);
|
const fmTitle = this.getFrontmatterValue(frontmatter, keys.title) || frontmatter['title'];
|
||||||
|
const fmAuthor = this.getFrontmatterValue(frontmatter, keys.author) || frontmatter['author'];
|
||||||
|
if (fmTitle) res.title = fmTitle;
|
||||||
|
if (fmAuthor) res.author = fmAuthor;
|
||||||
res.digest = this.getFrontmatterValue(frontmatter, keys.digest);
|
res.digest = this.getFrontmatterValue(frontmatter, keys.digest);
|
||||||
res.content_source_url = this.getFrontmatterValue(frontmatter, keys.content_source_url);
|
res.content_source_url = this.getFrontmatterValue(frontmatter, keys.content_source_url);
|
||||||
res.cover = this.getFrontmatterValue(frontmatter, keys.cover);
|
res.cover = this.getFrontmatterValue(frontmatter, keys.cover) || frontmatter['cover'] || frontmatter['image'];
|
||||||
res.thumb_media_id = this.getFrontmatterValue(frontmatter, keys.thumb_media_id);
|
res.thumb_media_id = this.getFrontmatterValue(frontmatter, keys.thumb_media_id);
|
||||||
res.need_open_comment = frontmatter[keys.need_open_comment] ? 1 : undefined;
|
res.need_open_comment = frontmatter[keys.need_open_comment] ? 1 : undefined;
|
||||||
res.only_fans_can_comment = frontmatter[keys.only_fans_can_comment] ? 1 : undefined;
|
res.only_fans_can_comment = frontmatter[keys.only_fans_can_comment] ? 1 : undefined;
|
||||||
@@ -252,6 +274,41 @@ export class ArticleRender implements MDRendererCallback {
|
|||||||
res.pic_crop_1_1 = '0_0.525_0.404_1';
|
res.pic_crop_1_1 = '0_0.525_0.404_1';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果未显式指定封面,尝试从正文首图( markdown 或 wikilink ) 提取,按出现顺序优先
|
||||||
|
if (!res.cover && this.originalMarkdown) {
|
||||||
|
let body = this.originalMarkdown;
|
||||||
|
if (body.startsWith('---')) body = body.replace(FRONT_MATTER_REGEX, '');
|
||||||
|
|
||||||
|
// 同时匹配两种形式并比较 index
|
||||||
|
const mdImgPattern = /!\[[^\]]*\]\(([^)\s]+)\)/g; // group1 为路径
|
||||||
|
const wikilinkPattern = /!\[\[(.+?)\]\]/g; // group1 为文件名或 path
|
||||||
|
interface Candidate { idx:number; basename:string; }
|
||||||
|
const candidates: Candidate[] = [];
|
||||||
|
|
||||||
|
let m: RegExpExecArray | null;
|
||||||
|
while ((m = mdImgPattern.exec(body)) !== null) {
|
||||||
|
const rawPath = m[1];
|
||||||
|
if (/^https?:\/\//i.test(rawPath)) continue; // 跳过远程
|
||||||
|
const clean = rawPath.split('#')[0].split('?')[0];
|
||||||
|
const basename = clean.split('/').pop();
|
||||||
|
if (basename && /\.(png|jpe?g|gif|bmp|webp|svg|tiff)$/i.test(basename)) {
|
||||||
|
candidates.push({ idx: m.index, basename });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while ((m = wikilinkPattern.exec(body)) !== null) {
|
||||||
|
const inner = m[1].trim();
|
||||||
|
const clean = inner.split('#')[0].split('?')[0];
|
||||||
|
const basename = clean.split('/').pop();
|
||||||
|
if (basename && /\.(png|jpe?g|gif|bmp|webp|svg|tiff)$/i.test(basename)) {
|
||||||
|
candidates.push({ idx: m.index, basename });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
candidates.sort((a,b)=> a.idx - b.idx);
|
||||||
|
res.cover = `![[${candidates[0].basename}]]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export class NMPSettings {
|
|||||||
excalidrawToPNG: boolean;
|
excalidrawToPNG: boolean;
|
||||||
isLoaded: boolean = false;
|
isLoaded: boolean = false;
|
||||||
enableEmptyLine: boolean = false;
|
enableEmptyLine: boolean = false;
|
||||||
|
enableMarkdownImageToWikilink: boolean = true; // 自动将  转为 ![[file.ext]]
|
||||||
|
|
||||||
private static instance: NMPSettings;
|
private static instance: NMPSettings;
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ export class NMPSettings {
|
|||||||
this.excalidrawToPNG = false;
|
this.excalidrawToPNG = false;
|
||||||
this.expertSettingsNote = '';
|
this.expertSettingsNote = '';
|
||||||
this.enableEmptyLine = false;
|
this.enableEmptyLine = false;
|
||||||
|
this.enableMarkdownImageToWikilink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetStyelAndHighlight() {
|
resetStyelAndHighlight() {
|
||||||
@@ -101,6 +103,7 @@ export class NMPSettings {
|
|||||||
excalidrawToPNG,
|
excalidrawToPNG,
|
||||||
expertSettingsNote,
|
expertSettingsNote,
|
||||||
ignoreEmptyLine,
|
ignoreEmptyLine,
|
||||||
|
enableMarkdownImageToWikilink,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const settings = NMPSettings.getInstance();
|
const settings = NMPSettings.getInstance();
|
||||||
@@ -155,6 +158,9 @@ export class NMPSettings {
|
|||||||
if (ignoreEmptyLine !== undefined) {
|
if (ignoreEmptyLine !== undefined) {
|
||||||
settings.enableEmptyLine = ignoreEmptyLine;
|
settings.enableEmptyLine = ignoreEmptyLine;
|
||||||
}
|
}
|
||||||
|
if (enableMarkdownImageToWikilink !== undefined) {
|
||||||
|
settings.enableMarkdownImageToWikilink = enableMarkdownImageToWikilink;
|
||||||
|
}
|
||||||
settings.getExpiredDate();
|
settings.getExpiredDate();
|
||||||
settings.isLoaded = true;
|
settings.isLoaded = true;
|
||||||
}
|
}
|
||||||
@@ -179,6 +185,7 @@ export class NMPSettings {
|
|||||||
'excalidrawToPNG': settings.excalidrawToPNG,
|
'excalidrawToPNG': settings.excalidrawToPNG,
|
||||||
'expertSettingsNote': settings.expertSettingsNote,
|
'expertSettingsNote': settings.expertSettingsNote,
|
||||||
'ignoreEmptyLine': settings.enableEmptyLine,
|
'ignoreEmptyLine': settings.enableEmptyLine,
|
||||||
|
'enableMarkdownImageToWikilink': settings.enableMarkdownImageToWikilink,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
todo.list
20
todo.list
@@ -1,4 +1,17 @@
|
|||||||
1. 预处理markdown文件:
|
|
||||||
|
1. 目前图片只能识别![[imagefile]],无法识别,把markdownown文件中转化为[[imagefile]]。
|
||||||
|
如:转为![[2025ZK1-7.jpg]]
|
||||||
|
✅
|
||||||
|
|
||||||
|
2. 读取markdown的frontmatter属性,文章标题取title和作者取author内容。
|
||||||
|
以附件2025ZK1.md为例:
|
||||||
|
提取以下信息(忽略两端的“”):
|
||||||
|
- 如tiltle不为空,文章title不使用文件名,使用: 6月特种兵式观展
|
||||||
|
- 如果author不为空,公众号文章作者: 大童。
|
||||||
|
|
||||||
|
- , ![[name.ext]]不分优先级,看哪个在文章的最前面,取最前面这个作为封面图片
|
||||||
|
|
||||||
|
3. 预处理markdown文件:
|
||||||
对{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}或{{<gallery dir="/img/guanzhan/1"/>}}{{<load-photoswipe>}}
|
对{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}或{{<gallery dir="/img/guanzhan/1"/>}}{{<load-photoswipe>}}
|
||||||
- 获取dir中的内容,如"/img/guanzhan/1",与PREPATH拼接,全局定义PRE_PATH=/Users/gavin/myweb/static
|
- 获取dir中的内容,如"/img/guanzhan/1",与PREPATH拼接,全局定义PRE_PATH=/Users/gavin/myweb/static
|
||||||
图片所在路径:PREPATH+"/img/guanzhan/1",即/Users/gavin/myweb/static/img/guanzhan/1。
|
图片所在路径:PREPATH+"/img/guanzhan/1",即/Users/gavin/myweb/static/img/guanzhan/1。
|
||||||
@@ -7,8 +20,6 @@
|
|||||||
如n=2,取出的图片为xx.jpg,yy.png,那么把{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}替换为:
|
如n=2,取出的图片为xx.jpg,yy.png,那么把{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}替换为:
|
||||||
![[xx.jpg]]
|
![[xx.jpg]]
|
||||||
![[yy.png]]
|
![[yy.png]]
|
||||||
在main.js单独函数中处理,在预处理内容时调用。
|
|
||||||
|
|
||||||
|
|
||||||
2.
|
2.
|
||||||
对如下:
|
对如下:
|
||||||
@@ -54,7 +65,7 @@ slug: guanzhan
|
|||||||
|
|
||||||
提取以下信息(忽略两端的“”):
|
提取以下信息(忽略两端的“”):
|
||||||
- 公众号文章title: 6月特种兵式观展
|
- 公众号文章title: 6月特种兵式观展
|
||||||
- 文章作者: 大童
|
- 公众号文章作者: 大童
|
||||||
- 文章封面图片:GALLERY_PRE_PATH+"/img/shufa/a.jpg",转化为![[a.jpg]]; 如image为空,封面图片取文章中第一张图片
|
- 文章封面图片:GALLERY_PRE_PATH+"/img/shufa/a.jpg",转化为![[a.jpg]]; 如image为空,封面图片取文章中第一张图片
|
||||||
|
|
||||||
4.
|
4.
|
||||||
@@ -66,3 +77,4 @@ slug: guanzhan
|
|||||||
注意:如果我把2025ZK1.md 内容:img改成![[2025ZK1-7.jpg]],以上解析没有问题。
|
注意:如果我把2025ZK1.md 内容:img改成![[2025ZK1-7.jpg]],以上解析没有问题。
|
||||||
|
|
||||||
5. 文章没有图片,封面使用一张默认图片(设计一张)。
|
5. 文章没有图片,封面使用一张默认图片(设计一张)。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user