From 3d2171e837e8bceca060460059dfb74ae2bd0984 Mon Sep 17 00:00:00 2001 From: douboer Date: Mon, 22 Sep 2025 18:54:59 +0800 Subject: [PATCH] update at 2025-09-22 18:54:59 --- README.md | 88 +++++++++++++++++++++++ architecture.md | 24 ++++++- detaildesign.md | 62 +++++++++++++++- diagrams.md | 55 ++++++++++----- image-pipeline.md | 38 ++++++---- src/article-render.ts | 159 ++++++++++++++++++++++++++++++++++++++++++ src/setting-tab.ts | 42 +++++++++++ src/settings.ts | 27 ++++++- todo.list | 70 +++++++++---------- 9 files changed, 493 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index bb36239..1359f86 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,94 @@ NoteToMP插件支持该语法。 ### 插入SVG图标 https://www.bilibili.com/video/BV15XWVeEEJa/ +### Gallery 短代码支持 + +自 1.x 版本起,插件支持将形如 Hugo/Hexo 风格的短代码: + +``` +{{}}{{}} +``` + +在渲染阶段自动展开为若干行图片 WikiLink: + +``` +![[001.jpg]] +![[002.jpg]] +``` + +配置项: + +- Gallery 根路径(galleryPrePath):指向本地实际图片根目录,用于拼接短代码中的 dir 得到真实磁盘路径。 +- Gallery 选取图片数(galleryNumPic):每个 gallery 最多展开前 N 张图片(按文件名排序)。 + +可在插件设置界面直接修改,无需重启。若希望随机选取或按时间排序,可后续在 issue 中反馈需求。 + +### Gallery 块与 figure 支持 + +除了带 dir 的短代码,还支持块级: + +``` +{{}} +{{
}} +{{
}} +{{}} +``` + +渲染为: + +``` +![[a.jpg]] +![[b.png]] +``` + +说明: +- 支持 `src` 或 `link` 属性任选其一。 +- `caption` 当前忽略(可后续增强:写入 `![[file|caption]]` 或紧随段落)。 +- 去重/排序策略:按出现顺序,文件名原样。 + +### 自定义行级语法扩展 + +为提升公众号排版效率,插件内置以下“轻语法”转换(发生在 Markdown 解析前): + +1. 斜体标注:`[fig 一段说明 /]` → `一段说明` +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 --- diff --git a/architecture.md b/architecture.md index 6fea6f7..91a3de2 100644 --- a/architecture.md +++ b/architecture.md @@ -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. 索引 / 交叉引用 | 文档 | 内容概述 | diff --git a/detaildesign.md b/detaildesign.md index a4ba6ef..cb64ba2 100644 --- a/detaildesign.md +++ b/detaildesign.md @@ -24,6 +24,11 @@ | Gallery 支持 | 将 `{{}}{{}}` 转成图片 wikilinks 列表 | | 预处理 | 在 Markdown 渲染前执行自定义语法转 HTML | | 易扩展 | 提供独立函数/接口减少耦合,如 `selectGalleryImages`、`extractWeChatMeta` | +| 默认封面配置 | 无任何图片候选时使用 `defaultCoverPic` (可配置) | +| 前置回退解析 | 若 metadataCache 缺失 frontmatter,启用手动行级解析回退 | +| 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 .../]` → ``(题注样式)。 @@ -106,6 +115,44 @@ Raw Markdown - 若块内未匹配到任何 figure,保留原文本。 - 正则:`/{{}}([\s\S]*?){{<\/gallery>}}/g` 与内部 `figureRegex = /{{]*>}}/g`。 - 输出顺序按出现顺序。 + - `figure` 标签支持 `src="..."` 与可选 `link="..."`,当存在 link 时仍按 `src` 的 basename 作为图片候选;后续可利用 link 生成超链接包装。 + +#### 7.4.2 link 属性与未来 caption 计划 +- 当前:`link` 仅被解析但未输出额外结构,保留在后续渲染扩展阶段使用(例如生成 `` 包裹 ``)。 +- 规划:`caption` 字段可映射为 wikilink alias 或 `
`。 + +### 7.5 行级轻语法扩展 (`applyCustomInlineBlocks`) +- 输入:渲染后 HTML / 或预处理文本段落。 +- 规则: + - `[fig 内容 /]` → `内容`(当前实现可能用内联 style,后续计划换 class)。 + - `||r 文本` / `||g` / `||b` / `||y` / `|| 文本` → 彩色背景段落 `

...

`。 +- 节点安全:通过转义内部 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 | `{{}}{{}}` | 捕获 dir/caption | +| Gallery 块 | `{{}}([\s\S]*?){{<\/gallery>}}` | 块包裹内容 | +| Gallery figure | `{{]*>}}` | 提取图片 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 模式 | 直接生成 `
` 集合而非 wikilink 列表 | | 样式外置 | 行级块样式改为统一 CSS class | +| 默认封面池 | 支持数组随机选择 default cover | +| 默认封面校验 | 选择时校验存在性 + Notice 提示 | +| caption alias | gallery figure caption -> wikilink alias/figcaption | +| link wrap | figure link 生成 `` 包裹图片 | +| debug 开关 | 设置中关闭全部调试日志 | | 目录缓存 | 减少频繁 IO | ## 15. 风险与规避 diff --git a/diagrams.md b/diagrams.md index 84de5d6..113d8c3 100644 --- a/diagrams.md +++ b/diagrams.md @@ -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. 状态机概览 (发布按钮) diff --git a/image-pipeline.md b/image-pipeline.md index 7a52ce0..377a8a3 100644 --- a/image-pipeline.md +++ b/image-pipeline.md @@ -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: + - 短代码:`{{}}{{}}` → 目录枚举(受 `galleryPrePath` + `galleryNumPic` 影响)→ 多行 `![[...]]` 注入。 + - 块级:`{{}} ... {{}}` 内 `figure src="..." link="..." caption="..." >` 解析 src basename 加入候选;`link` 预留后续包装;`caption` 未来映射题注。 ## 5. 上传阶段 顺序: @@ -68,22 +73,21 @@ Base64: - 遍历 DOM `` → 查询 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 块 | `{{}}([\s\S]*?){{<\/gallery>}}` | 包裹内容 | +| Gallery figure | `{{]*>}}` | 提取 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 属性生成 `` 包裹 `` | ## 12. 快速检查清单 (Debug Checklist) - 图片是否在 `LocalImageManager.images` Map 中? - 是否执行了 `cachedElementsToImages()`(Mermaid/Excalidraw)? - 上传后 `media_id` 是否为空?(格式/大小不合规) - DOM 替换后 `` 是否为微信域? -- 自动封面未生效?检查:frontmatter cover / 有无本地首图 / 正则是否截获远程 URL。 +- 自动封面未生效?检查:frontmatter cover / 正文本地图是否存在 / gallery 是否展开 / defaultCoverPic 是否配置。 +- 重复封面日志?确认日志节流是否失效(3 秒窗口内只应出现一次)。 ## 13. 示例 输入 Markdown: diff --git a/src/article-render.ts b/src/article-render.ts index 1f75378..8728237 100644 --- a/src/article-render.ts +++ b/src/article-render.ts @@ -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 配置迁移到 NMPSettings(galleryPrePath, galleryNumPic) +// 匹配示例:{{}}{{}} +// figcaption 可选 +const GALLERY_SHORTCODE_REGEX = /{{}}\s*{{}}/g; +// 块级 gallery: +// {{}}\n{{
}}\n...\n{{}} +// 需要提取所有 figure 的 src basename 生成多行 wikilink +const GALLERY_BLOCK_REGEX = /{{}}([\s\S]*?){{<\/gallery>}}/g; +// figure 支持 src 或 link 属性,两者取其一 +const FIGURE_IN_GALLERY_REGEX = /{{]*>}}/g; + +async function listLocalImages(dirAbs: string): Promise { + 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 { + // 逐个替换(异步)—— 使用 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 `${content}`; + }); + + // 颜色映射:默认|| 与 ||g, ||r, ||b, ||y + const blockStyles: Record = { + '': "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 `

${text}

`; + }).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; diff --git a/src/setting-tab.ts b/src/setting-tab.ts index 4a3ff01..5c1c4d1 100644 --- a/src/setting-tab.ts +++ b/src/setting-tab.ts @@ -270,6 +270,48 @@ export class NoteToMpSettingTab extends PluginSettingTab { }); }) + new Setting(containerEl) + .setName('Gallery 根路径') + .setDesc('用于 {{}} 短代码解析;需指向本地图片根目录') + .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 => { diff --git a/src/settings.ts b/src/settings.ts index ea2f78c..09f7b88 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -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, } } diff --git a/todo.list b/todo.list index 0ca6c6a..46a52c0 100644 --- a/todo.list +++ b/todo.list @@ -10,6 +10,7 @@ - 如果author不为空,公众号文章作者: 大童。 - ![alt](path/to/name.ext), ![[name.ext]]不分优先级,看哪个在文章的最前面,取最前面这个作为封面图片 +✅ 3. 预处理markdown文件: 对{{}}{{}}或{{}}{{}} @@ -20,8 +21,9 @@ 如n=2,取出的图片为xx.jpg,yy.png,那么把{{}}{{}}替换为: ![[xx.jpg]] ![[yy.png]] +✅ -2. +3. 对如下: {{}} {{
}} @@ -33,48 +35,40 @@ ![[晋中晋北行程-2.jpeg]] ![[晋中晋北行程-3.jpeg]] +src可能使用link: +{{}} +{{
}} +{{
}} +{{}} +替换为 +![[2025ZK12.jpg]] +![[2025ZK12-2.jpg]] +✅ -2. 需求:没有成功❌❓ + +4. +参考以下代码,渲染[fig content/],|| content,||r content,||g content,||b content等标签: + `\[fig([^>]*?)/\]` `$1` + `\|\| (.*)` `

$1

` + `\|\|r (.*)` `

$1

` + `\|\|g (.*)` `

$1

` + `\|\|b (.*)` `

$1

` + `\|\|y (.*)` `

$1

` + +||连续多行只渲染第一行,举例: || content1 content2 content3 - -修改代码,连续多行只渲染第一行,举例: -

content1

+渲染为: +

content1

+content2 +content3 而不是: -

content1\ncontent2\ncontent3

- -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]],以上解析没有问题。 +

content1 +content2 +content3 +

+✅ 5. 文章没有图片,封面使用一张默认图片(设计一张)。