From 9b8ec73c8328f5df6079c99d1a1fdaa208dcf81d Mon Sep 17 00:00:00 2001 From: douboer Date: Mon, 22 Sep 2025 14:58:45 +0800 Subject: [PATCH] update at 2025-09-22 14:58:45 --- detaildesign.md | 300 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +- src/gallery/index.ts | 47 ------- src/image/index.ts | 64 --------- src/meta/index.ts | 76 ---------- src/note-preview.ts | 57 ++++---- src/preprocess/index.ts | 36 ----- src/refactor-plan.md | 69 --------- src/render/index.ts | 50 ------- todo.list | 68 +++++++++ 10 files changed, 394 insertions(+), 377 deletions(-) create mode 100644 detaildesign.md delete mode 100644 src/gallery/index.ts delete mode 100644 src/image/index.ts delete mode 100644 src/meta/index.ts delete mode 100644 src/preprocess/index.ts delete mode 100644 src/refactor-plan.md delete mode 100644 src/render/index.ts create mode 100644 todo.list diff --git a/detaildesign.md b/detaildesign.md new file mode 100644 index 0000000..a5fb823 --- /dev/null +++ b/detaildesign.md @@ -0,0 +1,300 @@ +# note-to-mp 设计文档 (Detail Design) + +## 1. 背景 +为满足从 Obsidian 笔记到微信公众号文章的高质量发布需求,插件需要: +- 支持多种图片书写形式(Wikilink 与标准 Markdown)。 +- 统一图片处理与上传(包括 WebP 转换、水印、封面选择)。 +- 自动抽取文章元数据(标题、作者、封面图)。 +- 支持自定义短代码(`gallery`)与行级语法扩展(`||` 样式块、`[fig .../]` 等)。 +- 提供灵活的封面回退逻辑(frontmatter 指定优先,缺省取正文第一图)。 + +## 2. 目标 +| 目标 | 说明 | +|------|------| +| 图片语法统一 | `![[file.png]]` 与 `![alt](path/file.png)` 最终统一进入 LocalImage 管线 | +| 元数据抽取 | 自动获取标题、作者、封面图(可回退)供后续上传逻辑使用 | +| 封面回退 | 未显式指定封面时,自动决策第一张图片 | +| Gallery 支持 | 将 `{{}}{{}}` 转成图片 wikilinks 列表 | +| 预处理 | 在 Markdown 渲染前执行自定义语法转 HTML | +| 易扩展 | 提供独立函数/接口减少耦合,如 `selectGalleryImages`、`extractWeChatMeta` | + +## 3. 术语与定义 +- **Wikilink 图片语法**:`![[xxx.png]]` +- **标准 Markdown 图片**:`![描述](path/to/xxx.png)` +- **Frontmatter**:位于首部 `---` 包裹的元数据区域。 +- **Cover(封面)**:用于公众号首图上传的图片。 +- **Gallery Shortcode**:`{{}}{{}}` + +## 4. 系统现状概览 +主要处理链路: +``` +Raw Markdown + ↓ extractWeChatMeta (保留 frontmatter 内容供分析) + ↓ 去 frontmatter + ↓ transformGalleryShortcodes (gallery → ![[...]] 列表) + ↓ marked.parse() (图片扩展 -> LocalImage token) + ↓ 生成 HTML + 样式注入 + ↓ setArticle() + ↓ getArticleContent() -> preprocessContent(line regex 替换) -> 最终 HTML +``` + +## 5. 架构模块划分 +| 模块 | 关键函数/变量 | 作用 | +|------|---------------|------| +| 内容预处理 | `preprocessContent()` | 行级 Regex 转 HTML(图片路径修正、`||` 块、`[fig .../]`) | +| 图片统一解析 | `LocalFileRegex`、MarkdownImage tokenizer | 标准化所有图片为 LocalImage token | +| 图片资源管理 | `LocalImageManager` | 记录本地图片、上传、替换 URL、Base64 嵌入 | +| Gallery | `_listGalleryImages` / `selectGalleryImages` / `transformGalleryShortcodes` | 短代码 → wikilink 列表(可扩展 figcaption) | +| 元数据抽取 | `extractWeChatMeta` / `getWeChatArticleMeta` | 标题 / 作者 / 封面图计算 | +| 封面自动补全 | `getMetadata()` 尾部逻辑 | 无 frontmatter cover 时回填 | +| 图片上传 | `uploadLocalImage` / `uploadCover` | WebP→JPG、加水印、水印依赖 wasm | +| WebP 支持 | `PrepareImageLib` + wasm | 转换后再上传 | +| 渲染管线 | `renderMarkdown` | 串联以上逻辑 | + +## 6. 数据流示意 +参见第 4 节图。每个阶段保证产物单向流入下一层,避免循环依赖。 + +## 7. 关键算法与实现细节 +### 7.1 图片统一转换 +- Regex:`LocalFileRegex = /^(?:!\[\[(.*?)\]\]|!\[[^\]]*\]\(([^\n\r\)]+)\))/` +- Markdown 标准图片 tokenizer: + 1. 匹配 `![alt](path)` → `matches[0]`。 + 2. 取 basename → 构造 `![[basename]]` 语义(内部直接建 LocalImage token,不再二次正则回匹配)。 + 3. 避免原先多余 `-2.png)` 残留问题。 + +### 7.2 元数据抽取(`extractWeChatMeta`) +- 捕获 frontmatter 简易块(首个 `---` 区间)。 +- 解析 `title / author / image` 单行 KV。 +- `image` → 取 basename → `![[basename]]`。 +- 回退封面:同时匹配 wikilink + markdown 图片,比较 index 取出现最早的一种。 +- 返回:`{ title, author, coverLink, rawImage }`。 +- 与 `getMetadata()` 融合以补齐空缺字段。 + +### 7.3 前置处理(`preprocessContent`) +- `[fig .../]` → ``(题注样式)。 +- 行级命令:`||r / ||g / ||b / ||y / ||` → 不同背景色 `

`。 +- `` → 前缀补全 `/img/`。 + +### 7.4 Gallery 功能 +- 短代码 Regex:`{{}}{{}}` +- `_listGalleryImages`:读目录 + 过滤扩展 + 排序 + 截断。 +- `selectGalleryImages`:对外通用(支持未来 random / prefix / includeDirInLink)。 +- 输出:多行 `![[file]]`,并追加可选 `figcaption` div。 + +#### 7.4.1 块级 Gallery 语法(新增) +支持: +``` +{{}} +{{

}} +{{
}} +{{}} +``` +转换: +``` +![[foo-1.png]] +![[foo-2.jpeg]] +``` +规则: +- 仅取 src 的 basename,忽略 caption(后续可扩展为题注输出)。 +- 若块内未匹配到任何 figure,保留原文本。 +- 正则:`/{{}}([\s\S]*?){{<\/gallery>}}/g` 与内部 `figureRegex = /{{]*>}}/g`。 +- 输出顺序按出现顺序。 + +## 8. 正则清单 +| 场景 | 正则 | 说明 | +|------|------|------| +| frontmatter | `^---[\s\S]*?\n---` | 仅首段 | +| Wikilink 图片 | `!\[\[(.+?)\]\]` | 非贪婪 | +| Markdown 图片 | `!\[[^\]]*\]\(([^\n\r\)]+)\)` | 不跨行 | +| Gallery | `{{}}{{}}` | 捕获 dir/caption | +| fig | `\[fig([^>]*?)\/]` | 题注 | +| 行块 | `\|\|r (.*)` 等 | 行级匹配 | + +## 9. 错误与边界 +| 情况 | 行为 | +|------|------| +| frontmatter 缺尾部 | 不解析,当普通正文 | +| 无 image 且正文无图 | `coverLink` 为空 | +| Gallery 目录缺失 | 原样保留短代码 | +| WebP 转换失败 | 记录日志,使用原文件 | +| 非支持图片扩展 | 忽略该文件 | + +## 10. 性能 +- 正则线性扫描 O(n)。 +- Gallery 目录排序 O(m log m)。 +- 可后续对 `_listGalleryImages` 结果加缓存。 + +## 11. 配置 & 常量 +| 常量 | 说明 | 后续计划 | +|------|------|----------| +| `GALLERY_PRE_PATH` | 画廊根目录 | 移入设置面板 | +| `GALLERY_NUM_PIC` | 默认选图数量 | 支持短代码参数覆盖 | +| 行级样式内联 | 直接 embed style | 可改 class + CSS | + +## 12. 对外接口 +| 方法 | 描述 | +|------|------| +| `getWeChatArticleMeta()` | 获取最近一次渲染抽取的 meta | +| `getMetadata()` | 微信上传所需聚合元数据,含封面补回 | +| `uploadCover()` | 上传封面,含 webp 处理 | +| `uploadLocalImage()` | 上传正文图片 | +| `renderMarkdown()` | 触发整个渲染链路 | + +## 13. 测试建议 +| 测试项 | 用例 | +|--------|------| +| frontmatter | 正常/缺尾部/缺字段/中文标题 | +| 首图回退 | wikilink 与 markdown 顺序交错 | +| Gallery | 有/无目录;含 caption;空目录 | +| 图片文件名 | 中文/空格/连字符/数字/大小写扩展 | +| 行级语法 | 多种颜色并存/与普通段落混排 | +| WebP | 可转换/未准备 wasm | +| 覆盖逻辑 | frontmatter 不同组合(仅 author、仅 title 等) | + +## 14. 可扩展点 +| 方向 | 说明 | +|------|------| +| 更完整 YAML | 使用 `js-yaml` 支持多行、列表、复杂类型 | +| tags/categories | 抽取为数组并暴露接口 | +| Gallery 参数 | 支持 `count=`、`random=`、`includeDir=` 等 | +| 封面策略 | 配置“frontmatter 优先 / 正文优先 / 首图随机” | +| 图廊 HTML 模式 | 直接生成 `
` 集合而非 wikilink 列表 | +| 样式外置 | 行级块样式改为统一 CSS class | +| 目录缓存 | 减少频繁 IO | + +## 15. 风险与规避 +| 风险 | 缓解 | +|------|------| +| 简化 frontmatter 误判 | 提示限制 + 计划引入 YAML 解析 | +| 正则误伤 | 增加单元测试覆盖边界字符 | +| Gallery IO 阻塞 | 后续异步 + loading 占位 | +| 移动端缺 fs | try/catch + 环境判断 | +| 样式散落行内 | 后续集中到主题 CSS | + +## 16. 示例复盘 +示例: +``` +--- +title: 6月特种兵式观展 +author: 大童 +image: "/img/shufa/a.jpg" +--- +前言 +![首图](img/b-first.png) +![[c-second.png]] +``` +结果: +- 封面:`![[a.jpg]]`(frontmatter 优先) +- 若删去 image 行 → 封面:`![[b-first.png]]`(首图) + +## 17. 迭代优先级建议 +| 优先级 | 项目 | +|--------|------| +| 高 | 封面 UI 选择确认 | +| 中 | YAML 解析器集成 | +| 中 | Gallery 参数化(count/random) | +| 中 | tags/categories 抽取 | +| 低 | 图廊 HTML figure 模式 | + +## 18. 关键函数索引 +| 函数 | 作用 | +|------|------| +| `extractWeChatMeta` | 抽取标题/作者/封面回退 | +| `transformGalleryShortcodes` | gallery 短代码 → wikilinks | +| `selectGalleryImages` | 画廊图片选择封装 | +| `preprocessContent` | 行级语法 HTML 化 | +| `getWeChatArticleMeta` | 获取抽取的 meta | +| `getMetadata` | 最终上传元数据(含封面回填) | +| `MarkdownImage.tokenizer` | 标准图片转 LocalImage token | +| `LocalFileRegex` | 统一匹配图片语法 | + +## 19. 总结 +通过“标准化 → 抽取 → 预处理 → 渲染 → 上传”分层设计,确保各功能模块低耦合并可独立演进。当前设计已满足基础运营发布需求,后续可按优先级增强 YAML 解析、封面配置、多图策略与 gallery 表现力。 + +--- +*若需我继续实现 tags/categories 抽取或 gallery 参数扩展,请直接提出。* + +## 附录 A. 草稿箱清空功能 + +### A.1 背景 +运营过程中测试/多次上传会堆积大量“草稿”,需要一键清理能力,并具备安全保护与预览模式。 + +### A.2 接口 +| 方法 | 说明 | +|------|------| +| `clearAllDrafts(appid, { confirm, batchSize=20, retainLatest=0, dryRun=false })` | 批量列出并删除草稿;需 `confirm:true` 才执行实际删除 | + +### A.3 选项说明 +| 选项 | 类型 | 说明 | +|------|------|------| +| confirm | boolean | 必须显式 true,否则抛错中止 | +| batchSize | number | 分页拉取条数(默认 20,受微信接口限制) | +| retainLatest | number | 保留最新 N 条(按接口返回顺序) | +| dryRun | boolean | 仅统计将删除的数量,不执行删除 | + +### A.4 返回结构 +``` +{ + total: number, // 收集到的全部 media_id 数 + skip: number, // 被保留的数量(= retainLatest 实际保留) + success: number, // 实际删除成功数(dryRun= true 时恒 0) + fail: number, // 删除失败数 + fails: Array<{ media_id, status? , errcode?, errmsg?, text? }>, + dryRun: boolean +} +``` + +### A.5 安全措施 +1. `confirm` 必须为 true。 +2. 可设置 `retainLatest` 防止误删全部。 +3. `dryRun` 先预览再正式执行。 +4. 删除逐条执行,可在失败时保留失败列表审计。 + +### A.6 未来增强 +| 方向 | 说明 | +|------|------| +| 并发删除 | Promise pool 控制并发提升速度 | +| 过滤条件 | 按标题关键词/日期范围选择性删除 | +| 进度通知 | 分批实时进度 Notice / 状态栏 | +| UI 集成 | 命令面板 + 二次确认弹窗 | +| 时间排序校验 | 根据返回 `update_time` 明确排序而非假设 | + +### A.7 命令面板入口 +已添加命令:`清空微信草稿箱 (危险)` (id: `note-to-mp-clear-drafts`) + +流程: +1. 首次 confirm:提示风险。 +2. 询问是否 dryRun(输入 y 仅预览)。 +3. 若非 dryRun,再询问保留最近 N 条。 +4. 二次 confirm 再次确认删除范围。 +5. 调用 `clearAllDrafts(null, { confirm:true, dryRun, retainLatest })`。 + +失败处理:捕获异常并 Notice 显示;控制台输出详细错误。 + +### A.8 可视化操作面板 (Modal) +新增 `ClearDraftsModal`:提供表单而非多级 confirm/prompt。 + +表单字段: +- appid (可留空自动从当前文章 frontmatter 获取) +- 保留最近 N 条(number,默认 0) +- DryRun 复选框(默认勾选) + +交互流程: +1. 打开命令 → 弹出 Modal。 +2. 用户填写/确认参数,首次点“执行”→ 若为真实删除且非 dryRun,会再弹出 confirm。 +3. 结果以 JSON 形式写入下方
 区域,便于复制。
+4. Notice 简要提示(DryRun 或 完成)。
+
+错误处理:
+- try/catch 包裹,失败写入 resultPre 文本 + Notice。
+- run 按钮在执行期间 disabled,防止重复触发。
+
+后续增强设想:
+| 项目 | 说明 |
+|------|------|
+| 进度条 | 删除大批量时显示当前进度/总数 |
+| 失败重试 | 针对 fails 列表单独重试按钮 |
+| 过滤条件 | 增加标题关键词 / 日期起止输入 |
+| 多账号选择 | 下拉列出已配置的 appid 列表 |
+| 日志导出 | 一键复制 JSON 结果 |
+
diff --git a/package-lock.json b/package-lock.json
index cd928e1..9bde4ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
 	"name": "note-to-mp",
-	"version": "1.3.0",
+	"version": "1.0.0",
 	"lockfileVersion": 3,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "note-to-mp",
-			"version": "1.3.0",
+			"version": "1.0.0",
 			"license": "MIT",
 			"dependencies": {
 				"@zip.js/zip.js": "^2.7.43",
diff --git a/src/gallery/index.ts b/src/gallery/index.ts
deleted file mode 100644
index c781eb3..0000000
--- a/src/gallery/index.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-// [note-to-mp 重构] Gallery 模块
-import { App } from 'obsidian';
-
-export interface GalleryTransformResult {
-  content: string;
-  replaced: boolean;
-}
-
-// 单行 self-closing 形式: {{}}{{}}
-const GALLERY_INLINE_RE = /{{}}\s*{{}}/g;
-// 块级形式
-const GALLERY_BLOCK_RE = /{{}}([\s\S]*?){{<\/gallery>}}/g;
-const FIGURE_RE = /{{]*>}}/g;
-
-export function transformGalleryShortcodes(raw: string): GalleryTransformResult {
-  let replaced = false;
-  // 处理块级
-  raw = raw.replace(GALLERY_BLOCK_RE, (_m, inner) => {
-    const imgs: string[] = [];
-    let fm: RegExpExecArray | null;
-    while ((fm = FIGURE_RE.exec(inner)) !== null) {
-      const src = fm[1];
-      const base = src.split(/[?#]/)[0].split('/').pop();
-      if (base) imgs.push(`![[${base}]]`);
-    }
-    if (imgs.length === 0) return _m; // 保留原文本
-    replaced = true;
-    return imgs.join('\n') + '\n';
-  });
-
-  // 处理单行自闭合形式
-  raw = raw.replace(GALLERY_INLINE_RE, (_m, dir, figcaption) => {
-    replaced = true;
-    const comment = figcaption ? `\n` : '';
-    // 暂不实际列目录;由后续 selectGalleryImages 扩展
-    return comment + ``;
-  });
-
-  return { content: raw, replaced };
-}
-
-// 占位:真实实现可遍历 vault 目录
-export async function selectGalleryImages(app: App, dir: string, options?: { limit?: number }): Promise {
-  // TODO: 遍历 app.vault.getAbstractFileByPath(dir)
-  // 返回文件名数组(不含路径)
-  return [];
-}
diff --git a/src/image/index.ts b/src/image/index.ts
deleted file mode 100644
index aedf3c9..0000000
--- a/src/image/index.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-// [note-to-mp 重构] 图片处理模块
-// 负责统一解析 wikilink 与 markdown 图片,并提供集中管理
-
-export interface LocalImage {
-  original: string;       // 原始匹配串(包括语法标记)
-  basename: string;       // 文件基本名(不含路径)
-  alt?: string;           // alt 描述(若来自 markdown 语法)
-  sourceType: 'wikilink' | 'markdown';
-  index: number;          // 在原文中的出现顺序
-}
-
-export const LocalFileRegex = /^(?:!\[\[(.*?)\]\]|!\[[^\]]*\]\(([^\n\r\)]+)\))/;
-
-export class LocalImageManager {
-  private images: LocalImage[] = [];
-  private byBasename: Map = new Map();
-
-  add(image: LocalImage) {
-    this.images.push(image);
-    const list = this.byBasename.get(image.basename) || [];
-    list.push(image);
-    this.byBasename.set(image.basename, list);
-  }
-
-  all(): LocalImage[] { return this.images.slice(); }
-
-  first(): LocalImage | undefined { return this.images[0]; }
-
-  findByBasename(name: string): LocalImage | undefined {
-    const list = this.byBasename.get(name);
-    return list && list[0];
-  }
-
-  clear() { this.images = []; this.byBasename.clear(); }
-}
-
-export function parseImagesFromMarkdown(markdown: string): LocalImage[] {
-  // 扫描整篇,统一抽取,不做替换
-  const result: LocalImage[] = [];
-  const wikilinkRe = /!\[\[(.+?)\]\]/g; // 非贪婪
-  const mdImgRe = /!\[([^\]]*)\]\(([^\n\r\)]+)\)/g;
-  let index = 0;
-
-  let m: RegExpExecArray | null;
-  while ((m = wikilinkRe.exec(markdown)) !== null) {
-    const full = m[0];
-    const inner = m[1].trim();
-    const basename = inner.split('/').pop() || inner;
-    result.push({ original: full, basename, sourceType: 'wikilink', index: index++ });
-  }
-  while ((m = mdImgRe.exec(markdown)) !== null) {
-    const full = m[0];
-    const alt = m[1].trim();
-    const link = m[2].trim();
-    const basename = link.split(/[?#]/)[0].split('/').pop() || link;
-    result.push({ original: full, basename, alt, sourceType: 'markdown', index: index++ });
-  }
-
-  // 按出现顺序(两个正则独立扫描会破坏顺序,重新排序 by 原始位置)
-  result.sort((a, b) => markdown.indexOf(a.original) - markdown.indexOf(b.original));
-  // 重排 index
-  result.forEach((r, i) => r.index = i);
-  return result;
-}
diff --git a/src/meta/index.ts b/src/meta/index.ts
deleted file mode 100644
index 5d12baf..0000000
--- a/src/meta/index.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-// [note-to-mp 重构] 元数据与封面模块
-import { LocalImage } from '../image';
-
-export interface WeChatMetaRaw {
-  title?: string;
-  author?: string;
-  coverLink?: string;   // frontmatter 或行内指定的图片 basename 形式
-  rawImage?: string;    // 原 frontmatter 中的 image 字段原始值(可包含路径)
-  hasFrontmatter: boolean;
-}
-
-export interface FinalMeta {
-  title: string;
-  author?: string;
-  coverImage?: LocalImage;  // 解析到的封面图片对象
-  coverLink?: string;       // 决策后的封面 basename
-}
-
-const FRONTMATTER_RE = /^---[\s\S]*?\n---/;
-
-export function extractWeChatMeta(raw: string): { meta: WeChatMetaRaw; body: string } {
-  const fmMatch = raw.match(FRONTMATTER_RE);
-  if (!fmMatch) {
-    return { meta: { hasFrontmatter: false }, body: raw };
-  }
-  const block = fmMatch[0];
-  const lines = block.split(/\r?\n/).slice(1, -1); // 去除首尾 ---
-  let title: string | undefined;
-  let author: string | undefined;
-  let image: string | undefined;
-
-  for (const line of lines) {
-    const m = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)$/);
-    if (!m) continue;
-    const key = m[1].toLowerCase();
-    let val = m[2].trim();
-    // 去除包裹引号
-    if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
-      val = val.slice(1, -1);
-    }
-    if (key === 'title') title = val;
-    else if (key === 'author') author = val;
-    else if (key === 'image' || key === 'cover') image = val;
-  }
-
-  let coverLink: string | undefined;
-  if (image) {
-    const basename = image.split(/[?#]/)[0].split('/').pop() || image;
-    coverLink = basename;
-  }
-
-  const body = raw.slice(block.length).replace(/^\s+/, '');
-  return { meta: { title, author, coverLink, rawImage: image, hasFrontmatter: true }, body };
-}
-
-export function getMetadata(images: LocalImage[], rawMeta: WeChatMetaRaw): FinalMeta {
-  // 标题回退策略:若无 frontmatter title,尝试第一行一级标题
-  let title = rawMeta.title;
-  if (!title) {
-    // 简单取第一行 markdown 一级/二级标题
-    // 实际调用方可传入 body 再做改进;这里保持接口简单
-    title = '未命名文章';
-  }
-
-  let coverLink = rawMeta.coverLink;
-  let coverImage: LocalImage | undefined;
-  if (coverLink) {
-    coverImage = images.find(img => img.basename === coverLink);
-  }
-  if (!coverImage) {
-    coverImage = images[0];
-    coverLink = coverImage?.basename;
-  }
-
-  return { title, author: rawMeta.author, coverImage, coverLink };
-}
diff --git a/src/note-preview.ts b/src/note-preview.ts
index 1029d68..2641c95 100644
--- a/src/note-preview.ts
+++ b/src/note-preview.ts
@@ -22,8 +22,6 @@
 
 import { EventRef, ItemView, Workspace, WorkspaceLeaf, Notice, Platform, TFile, TFolder, TAbstractFile, Plugin } from 'obsidian';
 import { uevent, debounce, waitForLayoutReady } from './utils';
-// [note-to-mp 重构] 引入新渲染管线
-import { RenderService, RenderedArticle } from './render';
 import { NMPSettings } from './settings';
 import AssetsManager from './assets';
 import { MarkedParser } from './markdown/parser';
@@ -64,9 +62,6 @@ export class NotePreview extends ItemView {
     _articleRender: ArticleRender | null = null;
     isCancelUpload: boolean = false;
     isBatchRuning: boolean = false;
-    // [note-to-mp 重构] 新渲染服务实例与最近一次渲染结果
-    newRenderService: RenderService | null = null;
-    lastArticle?: RenderedArticle;
 
 
     constructor(leaf: WorkspaceLeaf, plugin: Plugin) {
@@ -118,8 +113,6 @@ export class NotePreview extends ItemView {
         }
 
         this.buildUI();
-        // [note-to-mp 重构] 初始化新渲染服务
-        this.newRenderService = new RenderService(this.app);
         this.listeners = [
             this.workspace.on('file-open', () => {
                 this.update();
@@ -447,33 +440,31 @@ export class NotePreview extends ItemView {
             return;
         }
         this.currentFile = af;
-        // [note-to-mp 重构] 使用新渲染服务进行渲染
-        if (this.newRenderService) {
-            try {
-                const article = await this.newRenderService.renderFile(af);
-                this.lastArticle = article;
-                if (this.articleDiv) {
-                    this.articleDiv.empty();
-                    const wrap = this.articleDiv.createDiv();
-                    wrap.innerHTML = article.html;
+        await this.render.renderMarkdown(af);
+        const metadata = this.render.getMetadata();
+        if (metadata.appid) {
+            this.wechatSelect.value = metadata.appid;
+        }
+        else {
+            this.wechatSelect.value = this.currentAppId;
+        }
+
+        if (metadata.theme) {
+            this.assetsManager.themes.forEach(theme => {
+                if (theme.name === metadata.theme) { 
+                    this.themeSelect.value = theme.className;
                 }
-                // 元数据适配(当前新 meta 不含 appid/theme/highlight,保持现有选择状态)
-                if (this.wechatSelect) {
-                    this.wechatSelect.value = this.currentAppId || '';
-                }
-                if (this.themeSelect) {
-                    this.themeSelect.value = this.currentTheme;
-                }
-                if (this.highlightSelect) {
-                    this.highlightSelect.value = this.currentHighlight;
-                }
-            } catch (e) {
-                console.error('[note-to-mp 重构] 渲染失败', e);
-                new Notice('渲染失败: ' + e.message);
-            }
-        } else {
-            // 兜底:仍使用旧渲染
-            await this.render.renderMarkdown(af);
+            });
+        }
+        else {
+            this.themeSelect.value = this.currentTheme;
+        }
+
+        if (metadata.highlight) {
+            this.highlightSelect.value = this.render.currentHighlight;
+        }
+        else {
+            this.highlightSelect.value = this.currentHighlight;
         }
     }
 
diff --git a/src/preprocess/index.ts b/src/preprocess/index.ts
deleted file mode 100644
index 28163de..0000000
--- a/src/preprocess/index.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-// [note-to-mp 重构] 内容预处理模块
-
-// 行级颜色块语法:||r text / ||g text / ||b text / ||y text / || text
-const LINE_COLOR_RE = /^\|\|(r|g|b|y)?\s+(.*)$/;
-const FIG_RE = /\[fig([^\n]*?)\/_?]/g; // 简单题注
-
-function wrapColorLine(code: string | undefined, text: string): string {
-  const colorMap: Record = {
-    r: '#ffe5e5',
-    g: '#e5ffe9',
-    b: '#e5f1ff',
-    y: '#fff7d6',
-    '': '#f2f2f2'
-  };
-  const c = (code && colorMap[code]) || colorMap[''];
-  return `

${text}

`; -} - -export function preprocessContent(markdown: string): string { - const lines = markdown.split(/\r?\n/); - const out: string[] = []; - for (const line of lines) { - const m = line.match(LINE_COLOR_RE); - if (m) { - out.push(wrapColorLine(m[1], m[2])); - } else { - out.push(line); - } - } - let joined = out.join('\n'); - joined = joined.replace(FIG_RE, (_m, g1) => { - const text = g1.trim(); - return `${text}`; - }); - return joined; -} diff --git a/src/refactor-plan.md b/src/refactor-plan.md deleted file mode 100644 index ab6f617..0000000 --- a/src/refactor-plan.md +++ /dev/null @@ -1,69 +0,0 @@ -# note-to-mp 重构规划 (模块与接口草案) - -> 标记格式: // [note-to-mp 重构] - -## 目标概要 -- 模块化:图片处理、元数据、Gallery、内容预处理、渲染管线分离。 -- 清晰接口:对外暴露统一渲染与数据提取 API。 -- 可测试:核心逻辑函数纯函数化,最小化对 Obsidian 运行时依赖。 - -## 模块划分 -1. 图片处理模块 (image/) - - 统一识别 wikilink 与 markdown 图片语法 - - LocalImage 结构: { original: string; basename: string; alt?: string; sourceType: 'wikilink'|'markdown'; index: number; } - - LocalImageManager: 收集、查询、封面候选、上传占位接口 - - 正则常量: LocalFileRegex - -2. 元数据与封面 (meta/) - - extractWeChatMeta(raw: string): WeChatMetaRaw - - getWeChatArticleMeta(): 返回最近一次渲染缓存的 meta - - getMetadata(images: LocalImage[], metaRaw: WeChatMetaRaw): FinalMeta - - 回退策略: frontmatter cover > metaRaw.coverLink > images[0] - -3. Gallery 支持 (gallery/) - - transformGalleryShortcodes(content: string): { content: string; extracted?: GalleryInfo } - - selectGalleryImages(dir: string, options): Promise - - 语法: 单行 self-closing 与 块级形式 - -4. 内容预处理 (preprocess/) - - preprocessContent(markdown: string): string - - 行级语法: ||r / ||g / ||b / ||y / || (默认灰) - - figure 语法: [fig text/] - -5. 渲染管线 (render/) - - renderMarkdown(file: TFile): Promise - - 内部阶段: - Raw -> extractWeChatMeta -> strip frontmatter -> transformGalleryShortcodes -> preprocessContent -> markdown parse (自定义 tokenizer) -> HTML + 样式注入 -> metadata 汇总 - -6. 上传/微信接口 (weixin/) - - 包装现有 weixin-api.ts 函数 + 错误封装 - -## 数据结构 -```ts -interface LocalImage { original: string; basename: string; alt?: string; sourceType: 'wikilink'|'markdown'; index: number; } -interface WeChatMetaRaw { title?: string; author?: string; coverLink?: string; rawImage?: string; hasFrontmatter: boolean; } -interface FinalMeta { title: string; author?: string; coverImage?: LocalImage; coverLink?: string; } -interface RenderedArticle { html: string; css?: string; meta: FinalMeta; images: LocalImage[]; raw: string; } -``` - -## 关键正则 -- frontmatter: ^---[\s\S]*?\n--- -- wikilink image: !\[\[(.+?)\]\] -- markdown image: !\[[^\]]*\]\(([^\n\r\)]+)\) -- gallery block: /{{}}([\s\S]*?){{<\/gallery>}}/g -- gallery figure: /{{]*>}}/g - -## 风险点 -- 正则误判 frontmatter -- 图片在预处理阶段被破坏索引 -- 多次渲染缓存污染 - -## 缓解 -- 提取后不修改原文本副本 -- 维护渲染上下文对象 (RenderContext) - -## 后续实现顺序 -图片处理 -> 元数据 -> Gallery -> 预处理 -> 渲染组装 -> 接口对接现有 NotePreview - ---- -(实现过程中该文档可增补) diff --git a/src/render/index.ts b/src/render/index.ts deleted file mode 100644 index 62f60a9..0000000 --- a/src/render/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -// [note-to-mp 重构] 渲染管线模块 -import { App, TFile, MarkdownRenderer } from 'obsidian'; -import { parseImagesFromMarkdown, LocalImage, LocalImageManager } from '../image'; -import { extractWeChatMeta, getMetadata, FinalMeta } from '../meta'; -import { transformGalleryShortcodes } from '../gallery'; -import { preprocessContent } from '../preprocess'; - -export interface RenderedArticle { - html: string; - meta: FinalMeta; - images: LocalImage[]; - raw: string; -} - -export class RenderService { - private app: App; - private imageManager = new LocalImageManager(); - - constructor(app: App) { - this.app = app; - } - - async renderFile(file: TFile): Promise { - const raw = await this.app.vault.read(file); - return this.renderRaw(raw, file.path); - } - - async renderRaw(raw: string, path?: string): Promise { - this.imageManager.clear(); - // 1. frontmatter + 基础元数据 - const { meta: rawMeta, body } = extractWeChatMeta(raw); - // 2. gallery 转换 - const galleryRes = transformGalleryShortcodes(body); - // 3. 预处理行级语法 - const preprocessed = preprocessContent(galleryRes.content); - // 4. 图片解析 - const images = parseImagesFromMarkdown(preprocessed); - images.forEach(i => this.imageManager.add(i)); - // 5. 获取最终 meta(封面回退) - const finalMeta = getMetadata(images, rawMeta); - // 6. markdown -> HTML (使用 Obsidian 内部渲染管线) - const el = document.createElement('div'); - // NOTE: 这里简化,实际应考虑自定义 tokenizer;后续可补充 - await MarkdownRenderer.renderMarkdown(preprocessed, el, path || '', this.app as any); - // 7. 注入简单样式 (可外置) - const style = ``; - - return { html: style + el.innerHTML, meta: finalMeta, images, raw }; - } -} diff --git a/todo.list b/todo.list new file mode 100644 index 0000000..8567523 --- /dev/null +++ b/todo.list @@ -0,0 +1,68 @@ +1. 预处理markdown文件: +对{{}}{{}}或{{}}{{}} + - 获取dir中的内容,如"/img/guanzhan/1",与PREPATH拼接,全局定义PRE_PATH=/Users/gavin/myweb/static + 图片所在路径:PREPATH+"/img/guanzhan/1",即/Users/gavin/myweb/static/img/guanzhan/1。 + - 这个/Users/gavin/myweb/static/img/guanzhan/1路径下图片<5张,取出所有图片; >n张,任意取出n张。n=NUM_PIC作为全局定义。 + - 比如n=1,取出的图片为xx.jpg,那么把{{}}{{}}替换为![[xx.jpg]] + 如n=2,取出的图片为xx.jpg,yy.png,那么把{{}}{{}}替换为: + ![[xx.jpg]] + ![[yy.png]] + 在main.js单独函数中处理,在预处理内容时调用。 + + +2. +对如下: +{{}} +{{
}} +{{
}} +{{
}} +{{}} +替换为 +![[晋中晋北行程.jpeg]] +![[晋中晋北行程-2.jpeg]] +![[晋中晋北行程-3.jpeg]] + + +2. 需求:没有成功❌❓ +|| content1 +content2 +content3 + +修改代码,连续多行只渲染第一行,举例: +

content1

+而不是: +

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]],以上解析没有问题。 + +5. 文章没有图片,封面使用一张默认图片(设计一张)。