update at 2025-09-22 18:54:59

This commit is contained in:
douboer
2025-09-22 18:54:59 +08:00
parent 81c6f52b69
commit 3d2171e837
9 changed files with 493 additions and 72 deletions

View File

@@ -300,6 +300,94 @@ NoteToMP插件支持该语法。
### 插入SVG图标
https://www.bilibili.com/video/BV15XWVeEEJa/
### Gallery 短代码支持
自 1.x 版本起,插件支持将形如 Hugo/Hexo 风格的短代码:
```
{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}
```
在渲染阶段自动展开为若干行图片 WikiLink
```
![[001.jpg]]
![[002.jpg]]
```
配置项:
- Gallery 根路径galleryPrePath指向本地实际图片根目录用于拼接短代码中的 dir 得到真实磁盘路径。
- Gallery 选取图片数galleryNumPic每个 gallery 最多展开前 N 张图片(按文件名排序)。
可在插件设置界面直接修改,无需重启。若希望随机选取或按时间排序,可后续在 issue 中反馈需求。
### Gallery 块与 figure 支持
除了带 dir 的短代码,还支持块级:
```
{{<gallery>}}
{{<figure src="/img/a.jpg" caption="说明" >}}
{{<figure link="/img/b.png" caption="说明" >}}
{{</gallery>}}
```
渲染为:
```
![[a.jpg]]
![[b.png]]
```
说明:
- 支持 `src` 或 `link` 属性任选其一。
- `caption` 当前忽略(可后续增强:写入 `![[file|caption]]` 或紧随段落)。
- 去重/排序策略:按出现顺序,文件名原样。
### 自定义行级语法扩展
为提升公众号排版效率,插件内置以下“轻语法”转换(发生在 Markdown 解析前):
1. 斜体标注:`[fig 一段说明 /]` → `<span style="font-style:italic;...">一段说明</span>`
2. 彩色提示块(只作用当前这一行,不跨行):
- `|| 内容` 默认灰底
- `||r 内容` 棕底白字
- `||g 内容` 黄绿色背景
- `||b 内容` 浅灰背景
- `||y 内容` 浅黄背景
这些语法不会写回原笔记,只影响发布预览。后续可加入:类名替换 + 主题化配置 + caption 支持,欢迎反馈需求。
### 无图片时的默认封面
自动封面选择优先级:
1. frontmatter: cover / image非空
2. 正文首图Markdown 或 WikiLink
3. Gallery 短代码 / 块展开得到的首图
4. 默认封面 `defaultCoverPic`(设置面板可配置,默认 `cover.png`
配置说明:
- 若填写文件名(如 `cover.png`),会按当前笔记目录解析并包装为 `![[cover.png]]`。
- 若填写完整 `![[xxx]]` 语法或 `http(s)://` URL将原样使用。
- 若文件不存在,不会报错(可后续增加存在性提示)。
### Frontmatter 解析回退
如果 Obsidian `metadataCache` 暂未命中(例如首次载入或缓存延迟),插件会手动对首段 `---` YAML 进行轻量行级解析,提取:
- title / author / cover(image)
避免因为缓存未就绪导致标题/作者缺失。若需复杂 YAML数组、多行字符串建议等待官方缓存或后续考虑引入完整 YAML 解析库。
### 调试日志
在控制台(开发者工具)可看到:
```
[note2mp] active file path: your/file/path.md
[note2mp] use default cover: cover.png -> ![[cover.png]]
```
路径日志做了节流:同一文件 3 秒内不重复打印。后续可加“调试开关”以完全关闭。
### 摘要、封面裁剪、原文链接等
```yaml
---

View File

@@ -13,19 +13,28 @@ UI / Interaction (NotePreview)
Rendering & Composition (ArticleRender + MarkedParser + Extensions)
├─ Markdown 预处理 (frontmatter 剥离 / 可选图片语法转换)
├─ Gallery 展开 (短代码 & 块级 figure src|link → wikilink 列表)
├─ 语法扩展 (LocalFile: wikilink 图片、文件嵌入、Excalidraw、SVG)
├─ 行级轻语法 (applyCustomInlineBlocks: fig / ||r||g||b||y||)
├─ 延迟元素缓存 (Mermaid / Excalidraw)
└─ 样式注入 (主题 + 代码高亮 + 自定义 CSS)
Assets & Settings Layer
├─ AssetsManager (themes, highlights, customCSS)
└─ NMPSettings (frontmatter key 映射 / 功能开关 / 微信配置)
└─ NMPSettings (frontmatter key 映射 / 功能开关 / 微信配置 / galleryPrePath & galleryNumPic & defaultCoverPic)
Resource & Media Layer
├─ LocalImageManager (图片登记 / 上传 / 替换 / base64 嵌入)
├─ Image Conversion (WebP -> JPG wasm)
└─ html-to-image (Mermaid/Excalidraw 转 PNG)
Cover Fallback Chain (逻辑横切 Concern)
frontmatter cover/image → 正文首本地图片 → gallery 生成列表首图 → defaultCoverPic (配置) → 空
Debug Logging & Throttle
- 输出:当前文件路径、封面决策(包含 defaultCoverPic 触发)
- 节流:同路径 3 秒内不重复打印
WeChat Integration Layer
├─ Token 代理 (wxGetToken)
├─ 草稿创建 (wxAddDraft / wxAddDraftImages)
@@ -77,10 +86,19 @@ function resolveCover(originalMarkdown: string, fmCover?: string, thumbMediaId?:
if (fmCover) return fmCover; // frontmatter 指定
const body = stripFrontmatter(originalMarkdown);
const candidates = collectImageOrder(body); // wikilink + markdown
return candidates.length ? `![[${candidates[0]}]]` : null;
// 若正文无本地候选再尝试 gallery 展开结果
if (!candidates.length) {
const galleryList = collectGalleryFirstImages(body);
if (galleryList.length) candidates.push(galleryList[0]);
}
if (candidates.length) return `![[${candidates[0]}]]`;
// 最终 defaultCoverPic 由外层 getMetadata 注入(若配置)
return null;
}
```
Fallback 扩展:`getMetadata()` 在上述返回为空且存在 `defaultCoverPic` 时作为最终封面。
## 5. 图片收集策略统一要点
- Wikilink解析时即登记路径 → vault file
- Markdown 图片:可配置预处理转 Wikilink以复用同一逻辑避免重复正则后处理
@@ -118,6 +136,8 @@ function resolveCover(originalMarkdown: string, fmCover?: string, thumbMediaId?:
- 多管线并存(旧 ArticleRender vs 新 RenderService 占位)易导致图片未登记 → 需统一抽象。
- 自动封面选取对远程图片/非图片扩展尚无过滤完备策略。
- 正则预处理与 Marked tokenizer 逻辑耦合度高,重构时需单测保护。
- 默认封面文件不存在导致封面缺失但用户误以为已设置 → 需未来加入存在性校验与 Notice。
- link/caption 属性当前解析未输出 → 未来转型时注意兼容老内容。
## 10. 索引 / 交叉引用
| 文档 | 内容概述 |

View File

@@ -24,6 +24,11 @@
| Gallery 支持 | 将 `{{<gallery .../>}}{{<load-photoswipe>}}` 转成图片 wikilinks 列表 |
| 预处理 | 在 Markdown 渲染前执行自定义语法转 HTML |
| 易扩展 | 提供独立函数/接口减少耦合,如 `selectGalleryImages``extractWeChatMeta` |
| 默认封面配置 | 无任何图片候选时使用 `defaultCoverPic` (可配置) |
| 前置回退解析 | 若 metadataCache 缺失 frontmatter启用手动行级解析回退 |
| Gallery 块扩展 | 支持 `{{<gallery>}}` 块 + 内部 `figure src|link=` 解析 |
| 行级语法扩展 | `[fig .../]``||r`/`||g`/`||b`/`||y`/`||` 统一由 `applyCustomInlineBlocks` 处理 |
| 调试日志节流 | 输出当前文件路径与默认封面选用日志3 秒内同路径不重复 |
## 3. 术语与定义
- **Wikilink 图片语法**`![[xxx.png]]`
@@ -39,7 +44,9 @@ Raw Markdown
↓ extractWeChatMeta (保留 frontmatter 内容供分析)
↓ 去 frontmatter
↓ transformGalleryShortcodes (gallery → ![[...]] 列表)
↓ transformGalleryBlock (gallery 块/figure → ![[...]] 列表)
↓ marked.parse() (图片扩展 -> LocalImage token
↓ applyCustomInlineBlocks (fig/彩色段落 轻语法 HTML 化)
↓ 生成 HTML + 样式注入
↓ setArticle()
↓ getArticleContent() -> preprocessContent(line regex 替换) -> 最终 HTML
@@ -76,6 +83,8 @@ Raw Markdown
- 回退封面:同时匹配 wikilink + markdown 图片,比较 index 取出现最早的一种。
- 返回:`{ title, author, coverLink, rawImage }`
-`getMetadata()` 融合以补齐空缺字段。
- 若 Obsidian `metadataCache` 返回为空或缺失字段,触发手动 fallback扫描首段 frontmatter 行(不依赖外部 YAML 包),支持 `key: value` 单行形式;空字符串的 cover/image 会被视为未提供。
- 追加默认封面逻辑封面候选链frontmatter cover > 正文首本地图/本地 wikilink/markdown > gallery 生成图 > defaultCoverPic
### 7.3 前置处理(`preprocessContent`
- `[fig .../]``<span>`(题注样式)。
@@ -106,6 +115,44 @@ Raw Markdown
- 若块内未匹配到任何 figure保留原文本。
- 正则:`/{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g` 与内部 `figureRegex = /{{<figure\s+src="([^"]+)"[^>]*>}}/g`
- 输出顺序按出现顺序。
- `figure` 标签支持 `src="..."` 与可选 `link="..."`,当存在 link 时仍按 `src` 的 basename 作为图片候选;后续可利用 link 生成超链接包装。
#### 7.4.2 link 属性与未来 caption 计划
- 当前:`link` 仅被解析但未输出额外结构,保留在后续渲染扩展阶段使用(例如生成 `<a>` 包裹 `<img>`)。
- 规划:`caption` 字段可映射为 wikilink alias 或 `<figcaption>`
### 7.5 行级轻语法扩展 (`applyCustomInlineBlocks`)
- 输入:渲染后 HTML / 或预处理文本段落。
- 规则:
- `[fig 内容 /]``<span class="n2m-fig">内容</span>`(当前实现可能用内联 style后续计划换 class
- `||r 文本` / `||g` / `||b` / `||y` / `|| 文本` → 彩色背景段落 `<p style>...</p>`
- 节点安全:通过转义内部 HTML 以防注入(若未实现需列入风险)。
- 后续:提取公共 class + 主题 CSS。
### 7.6 调试日志与节流
- 目的:调试封面选取与路径解析;避免刷屏。
- 机制记录最近一次输出路径时间戳3 秒内同路径日志抑制。
- 日志包括:当前 markdown 文件绝对路径;默认封面 fallback 触发说明gallery 转换统计(可选)。
### 7.7 配置项外化 (Settings 更新)
- 新增:`galleryPrePath`, `galleryNumPic`, `defaultCoverPic`
- 位置:`NMPSettings` + `SettingTab` UI 输入框。
- 迁移:移除硬编码常量 `GALLERY_PRE_PATH` / `GALLERY_NUM_PIC`
- 默认值:`defaultCoverPic = 'cover.png'`(可为相对/绝对/网络 URL 或 wikilink 形式)。
- 风险:用户提供的默认封面不存在 → 目前不校验,可后续增加存在性检查与 Notice。
### 7.8 封面候选决策链(更新版)
1. 若已有 `thumb_media_id`(外部指定)→ 不再上传本地封面,保持 null。
2. frontmatter cover/image非空字符串→ 使用其 basename 生成 wikilink。
3. 正文扫描首个本地图片markdown / wikilink忽略 http/https
4. 若正文无 → 使用 gallery 自动展开生成的第一张候选。
5. 若仍无 → 使用 `defaultCoverPic`(若配置)。
6.`defaultCoverPic` 也无 → cover 为空。
Edge Cases
- frontmatter cover: "" → 视为未提供。
- defaultCoverPic 若为绝对 URL → 在上传阶段需区分远程/本地策略。
- gallery 展开后若所有图片为远程 URL未来支持→ 不作为本地候选,跳到 defaultCoverPic。
## 8. 正则清单
| 场景 | 正则 | 说明 |
@@ -114,6 +161,9 @@ Raw Markdown
| Wikilink 图片 | `!\[\[(.+?)\]\]` | 非贪婪 |
| Markdown 图片 | `!\[[^\]]*\]\(([^\n\r\)]+)\)` | 不跨行 |
| Gallery | `{{<gallery\s+dir=\"([^\"]+)\"(?:\s+figcaption=\"([^\"]*)\")?\s*\/>}}{{<load-photoswipe>}}` | 捕获 dir/caption |
| Gallery 块 | `{{<gallery>}}([\s\S]*?){{<\/gallery>}}` | 块包裹内容 |
| Gallery figure | `{{<figure\s+src=\"([^\"]+)\"[^>]*>}}` | 提取图片 src |
| Figure link 属性 | `link=\"([^\"]+)\"` | 可选外链(当前仅解析) |
| fig | `\[fig([^>]*?)\/]` | 题注 |
| 行块 | `\|\|r (.*)` 等 | 行级匹配 |
@@ -134,8 +184,11 @@ Raw Markdown
## 11. 配置 & 常量
| 常量 | 说明 | 后续计划 |
|------|------|----------|
| `GALLERY_PRE_PATH` | 画廊根目录 | 移入设置面板 |
| `GALLERY_NUM_PIC` | 默认选图数量 | 支持短代码参数覆盖 |
| `galleryPrePath` | 画廊根目录(配置项) | 未来参数化 per-block 覆盖 |
| `galleryNumPic` | 默认选图数量(配置项) | 支持块/短代码 count 覆盖 |
| `defaultCoverPic` | 默认封面备用 | 校验存在 / 多备选随机 |
| 移除GALLERY_PRE_PATH | (已外化) | - |
| 移除GALLERY_NUM_PIC | (已外化) | - |
| 行级样式内联 | 直接 embed style | 可改 class + CSS |
## 12. 对外接口
@@ -167,6 +220,11 @@ Raw Markdown
| 封面策略 | 配置“frontmatter 优先 / 正文优先 / 首图随机” |
| 图廊 HTML 模式 | 直接生成 `<figure>` 集合而非 wikilink 列表 |
| 样式外置 | 行级块样式改为统一 CSS class |
| 默认封面池 | 支持数组随机选择 default cover |
| 默认封面校验 | 选择时校验存在性 + Notice 提示 |
| caption alias | gallery figure caption -> wikilink alias/figcaption |
| link wrap | figure link 生成 `<a>` 包裹图片 |
| debug 开关 | 设置中关闭全部调试日志 |
| 目录缓存 | 减少频繁 IO |
## 15. 风险与规避

View File

@@ -19,6 +19,8 @@ classDiagram
+postArticle(appid,cover?)
+postImages(appid)
+exportHTML()
+transformGalleryBlock()
+applyCustomInlineBlocks()
}
class LocalImageManager {
+setImage(path,info)
@@ -38,6 +40,9 @@ classDiagram
+wxInfo
+authKey
+enableMarkdownImageToWikilink
+galleryPrePath
+galleryNumPic
+defaultCoverPic
}
class WeChatAPI {
+wxGetToken()
@@ -100,20 +105,38 @@ graph TD
```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]
A -->|Yes| B[Frontmatter cover?]
B -- Yes --> H[Use frontmatter]
B -- No --> C[Strip Frontmatter]
C --> D[Scan Markdown Images]
C --> E[Scan Wikilink Images]
D --> F[Collect Candidates]
E --> F[Collect Candidates]
F --> G{Any Body Image?}
G -- Yes --> H[Use first body image]
G -- No --> I[Gallery Expanded?]
I -- Yes --> H[Use first gallery image]
I -- No --> J[defaultCoverPic Config?]
J -- Yes --> H[Use defaultCoverPic]
J -- No --> Z[Cover stays empty]
```
## 4.1 行级轻语法与日志节流 (补充)
```mermaid
graph TD
M[Markdown Raw] --> P[Preprocess Gallery Shortcode]
P --> GB[Gallery Block Parse]
GB --> MD[Marked Parse]
MD --> IB[applyCustomInlineBlocks]
IB --> R[Render HTML]
R --> L{Log Throttle}
L --> R1[Path Log]
L --> R2[Cover Fallback Log]
```
## 5. 未来 RenderService Pipeline 图
```mermaid
graph LR
graph TD
L[Loader] --> FM[Frontmatter]
FM --> PP[Preprocessors]
PP --> P[Parser]
@@ -127,14 +150,14 @@ graph LR
## 6. 并发上传示意 (未来优化)
```mermaid
graph TD
A[images[]] --> B[Partition into tasks]
B --> C[Promise Pool (N=4)]
C --> D[Upload Task]
A[Images] --> B[Partition]
B --> C[Pool]
C --> D[Upload]
D --> E{Success?}
E -- No --> R[Retry with backoff]
E -- Yes --> F[Collect media_id]
E -->|No| R[Retry]
E -->|Yes| F[Collect ids]
R --> C
F --> G[All Done]
F --> G[Done]
```
## 7. 状态机概览 (发布按钮)

View File

@@ -8,6 +8,8 @@
- 保证上传后 HTML 使用微信可访问的正式 URL。
- 自动封面选取:未指定时从正文首图推断。
- WebP → JPG 转换保障兼容性。
- 默认封面配置:若无正文/画廊候选,使用 `defaultCoverPic` (settings)。
- 手动 frontmatter 回退metadata 缺失时行级解析 `title/author/image`
## 2. 图片来源与归一化
| 来源 | 输入示例 | 归一化策略 | 备注 |
@@ -36,6 +38,9 @@ interface ImageInfo {
2. 根据 wikilink / 尺寸参数解析出真实 vault 文件路径:`assetsManager.getResourcePath()`
3. 调用 `LocalImageManager.setImage(res.resUrl, info)` 建档。
4. Markdown 图片(标准语法)若转换启用,提前转成 wikilink 进入同一逻辑;否则需未来扩展 tokenizer 直接登记。
5. Gallery
- 短代码:`{{<gallery dir="..."/>}}{{<load-photoswipe>}}` → 目录枚举(受 `galleryPrePath` + `galleryNumPic` 影响)→ 多行 `![[...]]` 注入。
- 块级:`{{<gallery>}} ... {{</gallery>}}``figure src="..." link="..." caption="..." >` 解析 src basename 加入候选;`link` 预留后续包装;`caption` 未来映射题注。
## 5. 上传阶段
顺序:
@@ -68,22 +73,21 @@ Base64
- 遍历 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`
## 6. 自动封面策略(更新)
触发点:`getMetadata()`;若存在 `thumb_media_id` 则跳过。决策链
1. frontmatter cover/image非空若 metadataCache 缺失,则手动行级解析首个 `---`
2. 正文本地图候选markdown + wikilink
3. gallery 展开产生的首图(短代码或块级 figure 列表)。
4. `defaultCoverPic`settings 配置,允许相对路径 / URL / wikilink
5. 否则封面为空。
Edge Cases
| 情况 | 处理 |
|------|------|
| 所有图片都是 http(s) | 不作为封面候选 |
| 文件名带 query/hash | 使用前切分去除 `?` `#` |
| frontmatter cover: "" | 视为未配置,继续回退 |
| 所有正文图片都是 http(s) | 跳过正文阶段,转 gallery -> defaultCoverPic |
| gallery 块无有效 figure | 不贡献候选 |
| defaultCoverPic 不存在 | 仍返回该引用;上传阶段可能失败(待校验增强) |
| markdown 图片后缀非图片 | 跳过 |
| 重复图片 | 以最先出现的为准 |
@@ -93,6 +97,9 @@ Edge Cases
| Frontmatter 删除 | `/^(---)$.+?^(---)$.+?/ims` | 首段 YAML |
| Markdown 图片 | `/!\[[^\]]*\]\(([^)\s]+)\)/g` | 捕获路径 group1 |
| Wikilink 图片 | `/!\[\[(.+?)\]\]/g` | 捕获内部资源 |
| Gallery 块 | `{{<gallery>}}([\s\S]*?){{<\/gallery>}}` | 包裹内容 |
| Gallery figure | `{{<figure\s+src=\"([^\"]+)\"[^>]*>}}` | 提取 src |
| figure link | `link=\"([^\"]+)\"` | 可选外链属性 |
| WebP 检测 | `/\.webp$/i` | 扩展判断 |
| Base64 前缀 | `^data:image/` | 判断内嵌图 |
@@ -130,13 +137,18 @@ Edge Cases
| 失效重传 | 保存上传映射 + 校验缺失再补传 |
| 分块上传 | 大图分片(若接口支持) |
| 图片校验 | MD5 去重避免重复上传 |
| defaultCoverPic 校验 | 渲染时验证存在性 + Notice |
| 多默认封面池 | 数组随机选择减少视觉重复 |
| caption alias | figure caption -> wikilink alias/figcaption |
| link 包裹 | link 属性生成 `<a>` 包裹 `<img>` |
## 12. 快速检查清单 (Debug Checklist)
- 图片是否在 `LocalImageManager.images` Map 中?
- 是否执行了 `cachedElementsToImages()`Mermaid/Excalidraw
- 上传后 `media_id` 是否为空?(格式/大小不合规)
- DOM 替换后 `<img src>` 是否为微信域?
- 自动封面未生效检查frontmatter cover / 有无本地首图 / 正则是否截获远程 URL
- 自动封面未生效检查frontmatter cover / 正文本地图是否存在 / gallery 是否展开 / defaultCoverPic 是否配置
- 重复封面日志确认日志节流是否失效3 秒窗口内只应出现一次)。
## 13. 示例
输入 Markdown

View File

@@ -34,10 +34,78 @@ import { CardDataManager } from './markdown/code';
import { debounce } from './utils';
import { PrepareImageLib, IsImageLibReady, WebpToJPG } from './imagelib';
import { toPng } from 'html-to-image';
import * as path from 'path';
import { stat, readdir } from 'fs/promises';
const FRONT_MATTER_REGEX = /^(---)$.+?^(---)$.+?/ims;
// gallery 配置迁移到 NMPSettingsgalleryPrePath, galleryNumPic
// 匹配示例:{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}
// figcaption 可选
const GALLERY_SHORTCODE_REGEX = /{{<gallery\s+dir="([^"]+)"(?:\s+figcaption="([^"]*)")?\s*\/?>}}\s*{{<load-photoswipe>}}/g;
// 块级 gallery
// {{<gallery>}}\n{{<figure src="/img/a.png" caption=".." >}}\n...\n{{</gallery>}}
// 需要提取所有 figure 的 src basename 生成多行 wikilink
const GALLERY_BLOCK_REGEX = /{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g;
// figure 支持 src 或 link 属性,两者取其一
const FIGURE_IN_GALLERY_REGEX = /{{<figure\s+(?:src|link)="([^"]+)"[^>]*>}}/g;
async function listLocalImages(dirAbs: string): Promise<string[]> {
try {
const stats = await stat(dirAbs);
if (!stats.isDirectory()) return [];
} catch { return []; }
try {
const files = await readdir(dirAbs);
return files.filter(f => /(png|jpe?g|gif|bmp|webp|svg)$/i.test(f)).sort();
} catch { return []; }
}
function pickImages(all: string[], limit: number): string[] {
if (all.length <= limit) return all;
// 简单:前 n可扩展为随机
return all.slice(0, limit);
}
async function transformGalleryShortcodes(md: string, prePath: string, numPic: number): Promise<string> {
// 逐个替换(异步)—— 使用 replace + 手动遍历实现
const matches: { full: string; dir: string; caption?: string }[] = [];
md.replace(GALLERY_SHORTCODE_REGEX, (full, dir, caption) => {
matches.push({ full, dir, caption });
return full;
});
let result = md;
for (const m of matches) {
const absDir = path.join(prePath, m.dir.replace(/^\//, '')); // 拼接绝对路径
const imgs = await listLocalImages(absDir);
const picked = pickImages(imgs, numPic);
if (picked.length === 0) {
// 无图则清空短代码(或保留原样,这里按需求替换为空)
result = result.replace(m.full, '');
continue;
}
const repl = picked.map(f => `![[${f}]]`).join('\n');
result = result.replace(m.full, repl);
}
// 处理无 dir 的块级 gallery只做本地名提取不访问文件系统
result = result.replace(GALLERY_BLOCK_REGEX, (full: string, inner: string) => {
const names: string[] = [];
inner.replace(FIGURE_IN_GALLERY_REGEX, (_f: string, src: string) => {
if (!src) return _f;
const clean = src.split('#')[0].split('?')[0];
const base = clean.split('/').pop();
if (base && /(png|jpe?g|gif|bmp|webp|svg)$/i.test(base)) {
names.push(base);
}
return _f;
});
if (names.length === 0) return '';
return names.map(n => `![[${n}]]`).join('\n');
});
return result;
}
export class ArticleRender implements MDRendererCallback {
app: App;
itemView: ItemView;
@@ -168,6 +236,12 @@ export class ArticleRender implements MDRendererCallback {
md = md.replace(FRONT_MATTER_REGEX, '');
}
// 处理 gallery 短代码 -> wikilink 图片列表
md = await transformGalleryShortcodes(md, this.settings.galleryPrePath, this.settings.galleryNumPic);
// 自定义行级语法转换: [fig .../] 以及 || 前缀块
md = this.applyCustomInlineBlocks(md);
// 将标准 markdown 图片语法转为 wikilink 语法,便于现有 LocalImageManager 识别
if (this.settings.enableMarkdownImageToWikilink) {
// 匹配 ![alt](path/to/name.ext);不跨行;忽略包含空格的 URL 末尾注释
@@ -195,6 +269,35 @@ export class ArticleRender implements MDRendererCallback {
this.setArticle(this.errorContent(e));
}
}
// 自定义 fig 与 || 语法转换
private applyCustomInlineBlocks(md: string): string {
const figPattern = /\[fig([^\n\]]*?)\/\]/g; // [fig text/]
md = md.replace(figPattern, (_m, inner) => {
const content = inner.trim();
return `<span style="font-style: italic; font-size: 14px; background-color: #f5f5f5; padding: 2px;">${content}</span>`;
});
// 颜色映射:默认|| 与 ||g, ||r, ||b, ||y
const blockStyles: Record<string, string> = {
'': "background-color:#E5E4E2;",
'r': "color:white;background-color:#6F4E37;",
'g': "background-color:#BCE954;",
'b': "background-color:#B6B6B4;",
'y': "background-color:#FFFFC2;",
};
const baseStyle = "font-family:'Microsoft YaHei',sans-serif;font-size:14px; padding:10px;border-radius:20px;line-height:30px;";
// 仅匹配行首 ||[flag]? 空格 之后的单行内容,避免吞并后续多行
md = md.split(/\n/).map(line => {
// 例如 || 内容, ||r 内容
const m = line.match(/^\|\|(r|g|b|y)?\s+(.*)$/);
if (!m) return line;
const flag = m[1] || '';
const text = m[2];
const style = baseStyle + (blockStyles[flag] || blockStyles['']);
return `<p style="${style}">${text}</p>`;
}).join('\n');
return md;
}
getCSS() {
try {
const theme = this.assetsManager.getTheme(this.currentTheme);
@@ -248,6 +351,17 @@ export class ArticleRender implements MDRendererCallback {
}
const file = this.app.workspace.getActiveFile();
if (!file) return res;
// 避免频繁刷屏:仅当路径变化或超过 3s 再输出一次
try {
const globalAny = window as any;
const now = Date.now();
if (!globalAny.__note2mp_lastPathLog ||
globalAny.__note2mp_lastPathLog.path !== file.path ||
now - globalAny.__note2mp_lastPathLog.time > 3000) {
console.log('[note2mp] active file path:', file.path);
globalAny.__note2mp_lastPathLog = { path: file.path, time: now };
}
} catch {}
const metadata = this.app.metadataCache.getFileCache(file);
if (metadata?.frontmatter) {
const keys = this.assetsManager.expertSettings.frontmatter;
@@ -260,6 +374,10 @@ export class ArticleRender implements MDRendererCallback {
res.digest = this.getFrontmatterValue(frontmatter, keys.digest);
res.content_source_url = this.getFrontmatterValue(frontmatter, keys.content_source_url);
res.cover = this.getFrontmatterValue(frontmatter, keys.cover) || frontmatter['cover'] || frontmatter['image'];
// frontmatter 给出的 cover/image 为空字符串时视为未设置
if (typeof res.cover === 'string' && res.cover.trim() === '') {
res.cover = undefined;
}
res.thumb_media_id = this.getFrontmatterValue(frontmatter, keys.thumb_media_id);
res.need_open_comment = frontmatter[keys.need_open_comment] ? 1 : undefined;
res.only_fans_can_comment = frontmatter[keys.only_fans_can_comment] ? 1 : undefined;
@@ -274,6 +392,33 @@ export class ArticleRender implements MDRendererCallback {
res.pic_crop_1_1 = '0_0.525_0.404_1';
}
}
else if (this.originalMarkdown?.startsWith('---')) {
// 元数据缓存未命中时的手动轻量解析(不引入 yaml 依赖,逐行扫描直到第二个 ---
try {
const lines = this.originalMarkdown.split(/\r?\n/);
if (lines[0].trim() === '---') {
let i = 1;
for (; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '---') break;
const m = line.match(/^(\w[\w-]*):\s*(.*)$/); // key: value 形式
if (!m) continue;
const k = m[1].toLowerCase();
const v = m[2].trim();
if (k === 'title' && !res.title) res.title = v;
if (k === 'author' && !res.author) res.author = v;
if ((k === 'image' || k === 'cover') && !res.cover && v) {
// image 可能是路径,取 basename
const clean = v.replace(/^"|"$/g, '').split('#')[0].split('?')[0];
const base = clean.split('/').pop();
if (base) res.cover = `![[${base}]]`;
}
}
}
} catch (err) {
console.warn('fallback frontmatter parse failed', err);
}
}
// 如果未显式指定封面,尝试从正文首图( markdown 或 wikilink ) 提取,按出现顺序优先
if (!res.cover && this.originalMarkdown) {
@@ -307,6 +452,20 @@ export class ArticleRender implements MDRendererCallback {
if (candidates.length > 0) {
candidates.sort((a,b)=> a.idx - b.idx);
res.cover = `![[${candidates[0].basename}]]`;
} else if (!res.cover) {
// 没有找到任何图片候选,应用默认封面(如果配置了)
const def = this.settings.defaultCoverPic?.trim();
if (def) {
if (/^!\[\[.*\]\]$/.test(def) || /^https?:\/\//i.test(def)) {
res.cover = def;
} else {
const base = def.split('/').pop();
if (base) res.cover = `![[${base}]]`;
}
if (res.cover) {
try { console.log('[note2mp] use default cover:', def, '->', res.cover); } catch {}
}
}
}
}
return res;

View File

@@ -270,6 +270,48 @@ export class NoteToMpSettingTab extends PluginSettingTab {
});
})
new Setting(containerEl)
.setName('Gallery 根路径')
.setDesc('用于 {{<gallery dir="..."/>}} 短代码解析;需指向本地图片根目录')
.addText(text => {
text.setPlaceholder('例如 /Users/xxx/site/static 或 相对路径')
.setValue(this.settings.galleryPrePath || '')
.onChange(async (value) => {
this.settings.galleryPrePath = value.trim();
await this.plugin.saveSettings();
});
text.inputEl.setAttr('style', 'width: 360px;');
});
new Setting(containerEl)
.setName('Gallery 选取图片数')
.setDesc('每个 gallery 短代码最多替换为前 N 张图片')
.addText(text => {
text.setPlaceholder('数字 >=1')
.setValue(String(this.settings.galleryNumPic || 2))
.onChange(async (value) => {
const n = parseInt(value, 10);
if (Number.isFinite(n) && n >= 1) {
this.settings.galleryNumPic = n;
await this.plugin.saveSettings();
}
});
text.inputEl.setAttr('style', 'width: 120px;');
});
new Setting(containerEl)
.setName('默认封面图片')
.setDesc('当文章无任何图片/短代码时使用;可填 wikilink 文件名或 http(s) URL')
.addText(text => {
text.setPlaceholder('例如 cover.png 或 https://...')
.setValue(this.settings.defaultCoverPic || '')
.onChange(async (value) => {
this.settings.defaultCoverPic = value.trim();
await this.plugin.saveSettings();
});
text.inputEl.setAttr('style', 'width: 360px;');
});
new Setting(containerEl)
.setName('启用空行渲染')
.addToggle(toggle => {

View File

@@ -44,6 +44,11 @@ export class NMPSettings {
isLoaded: boolean = false;
enableEmptyLine: boolean = false;
enableMarkdownImageToWikilink: boolean = true; // 自动将 ![alt](path/file.ext) 转为 ![[file.ext]]
// gallery 相关配置:根目录前缀 & 选取图片数量
galleryPrePath: string;
galleryNumPic: number;
// 无图片时的默认封面wikilink 或 URL 均可)
defaultCoverPic: string;
private static instance: NMPSettings;
@@ -73,7 +78,12 @@ export class NMPSettings {
this.excalidrawToPNG = false;
this.expertSettingsNote = '';
this.enableEmptyLine = false;
this.enableMarkdownImageToWikilink = true;
this.enableMarkdownImageToWikilink = true;
// 默认值:用户原先硬编码路径 & 前 2 张
this.galleryPrePath = '/Users/gavin/myweb/static';
this.galleryNumPic = 2;
// 默认封面:使用当前笔记同目录下的 cover.png (若存在会被后续流程正常解析;不存在则无效但可被用户覆盖)
this.defaultCoverPic = 'cover.png';
}
resetStyelAndHighlight() {
@@ -104,6 +114,9 @@ export class NMPSettings {
expertSettingsNote,
ignoreEmptyLine,
enableMarkdownImageToWikilink,
galleryPrePath,
galleryNumPic,
defaultCoverPic,
} = data;
const settings = NMPSettings.getInstance();
@@ -161,6 +174,15 @@ export class NMPSettings {
if (enableMarkdownImageToWikilink !== undefined) {
settings.enableMarkdownImageToWikilink = enableMarkdownImageToWikilink;
}
if (galleryPrePath) {
settings.galleryPrePath = galleryPrePath;
}
if (galleryNumPic !== undefined && Number.isFinite(galleryNumPic)) {
settings.galleryNumPic = Math.max(1, parseInt(galleryNumPic));
}
if (defaultCoverPic !== undefined) {
settings.defaultCoverPic = String(defaultCoverPic).trim();
}
settings.getExpiredDate();
settings.isLoaded = true;
}
@@ -186,6 +208,9 @@ export class NMPSettings {
'expertSettingsNote': settings.expertSettingsNote,
'ignoreEmptyLine': settings.enableEmptyLine,
'enableMarkdownImageToWikilink': settings.enableMarkdownImageToWikilink,
'galleryPrePath': settings.galleryPrePath,
'galleryNumPic': settings.galleryNumPic,
'defaultCoverPic': settings.defaultCoverPic,
}
}

View File

@@ -10,6 +10,7 @@
- 如果author不为空公众号文章作者: 大童。
- ![alt](path/to/name.ext), ![[name.ext]]不分优先级,看哪个在文章的最前面,取最前面这个作为封面图片
3. 预处理markdown文件
对{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}或{{<gallery dir="/img/guanzhan/1"/>}}{{<load-photoswipe>}}
@@ -20,8 +21,9 @@
如n=2取出的图片为xx.jpg,yy.png那么把{{<gallery dir="/img/guanzhan/1" figcaption="毕业展"/>}}{{<load-photoswipe>}}替换为:
![[xx.jpg]]
![[yy.png]]
2.
3.
对如下:
{{<gallery>}}
{{<figure src="/img/晋中晋北行程.jpeg" caption="晋中晋北行程" >}}
@@ -33,48 +35,40 @@
![[晋中晋北行程-2.jpeg]]
![[晋中晋北行程-3.jpeg]]
src可能使用link
{{<gallery>}}
{{<figure link="/img/2025ZK12.jpg" caption="">}}
{{<figure link="/img/2025ZK12-2.jpg" caption="">}}
{{</gallery>}}
替换为
![[2025ZK12.jpg]]
![[2025ZK12-2.jpg]]
2. 需求:没有成功❌❓
4.
参考以下代码,渲染[fig content/],|| content,||r content,||g content,||b content等标签
`\[fig([^>]*?)/\]` `<span style="font-style: italic; font-size: 14px; background-color: #f5f5f5; padding: 2px;">$1</span>`
`\|\| (.*)` `<p style="font-family:'Microsoft YaHei',sans-serif;background-color:#E5E4E2 ;padding:10px;border-radius:20px;line-height:30px;">$1</p>`
`\|\|r (.*)` `<p style="font-family:'Microsoft YaHei',sans-serif;color:white;background-color:#6F4E37;padding:10px;border-radius:20px;line-height:30px;">$1</p>`
`\|\|g (.*)` `<p style="font-family:'Microsoft YaHei',sans-serif;background-color:#BCE954;padding:10px;border-radius:20px;line-height:30px;">$1</p>`
`\|\|b (.*)` `<p style="font-family:'Microsoft YaHei',sans-serif;background-color:#B6B6B4;padding:10px;border-radius:20px;line-height:30px;">$1</p>`
`\|\|y (.*)` `<p style="font-family:'Microsoft YaHei',sans-serif;background-color:#FFFFC2;padding:10px;border-radius:20px;line-height:30px;">$1</p>`
||连续多行只渲染第一行,举例:
|| content1
content2
content3
修改代码,连续多行只渲染第一行,举例:
<p style="font-family:'Microsoft YaHei',sans-serif;background-color:#FFFFC2;padding:20px;border-radius:20px;line-height:35px;">content1</p>
渲染为:
<p style="font-family:'Microsoft YaHei',sans-serif;font-size:14px; background-color:#FFFFC2;padding:10px;border-radius:20px;line-height:30px;">content1</p>
content2
content3
而不是:
<p style="font-family:'Microsoft YaHei',sans-serif;background-color:#FFFFC2;padding:20px;border-radius:20px;line-height:35px;">content1\ncontent2\ncontent3</p>
3.
读取markdown属性
---
layout: post
title: 6月特种兵式观展
subtitle:
description:
date: 2025-06-11 11:00:00
author: 大童
image: "/img/shufa/a.jpg"
showtoc: true
tags:
- 旅行
URL:
categories:
- live
slug: guanzhan
---
提取以下信息(忽略两端的“”):
- 公众号文章title: 6月特种兵式观展
- 公众号文章作者: 大童
- 文章封面图片GALLERY_PRE_PATH+"/img/shufa/a.jpg",转化为![[a.jpg]]; 如image为空封面图片取文章中第一张图片
4.
2025ZK1.md 没有正确解析公众号标题2025ZK1封面图片解析也不对。
正确的应该是:
公众号标题“2025篆刻记录-0426”
封面图片:![[2025ZK1-7.jpg]]
注意如果我把2025ZK1.md 内容img改成![[2025ZK1-7.jpg]],以上解析没有问题。
<p style="font-family:'Microsoft YaHei',sans-serif;font-size:14px; background-color:#FFFFC2;padding:10px;border-radius:20px;line-height:30px;">content1
content2
content3
</p>
5. 文章没有图片,封面使用一张默认图片(设计一张)。