@topmindspace/topmind-writing-skills 0.8.44 → 0.8.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.en.md +11 -11
- package/README.md +11 -11
- package/package.json +1 -1
- package/topmind-briefs/CHANGELOG.md +5 -0
- package/topmind-briefs/README.en.md +1 -1
- package/topmind-briefs/README.md +1 -1
- package/topmind-briefs/SKILL.md +1 -1
- package/topmind-briefs/package.json +1 -1
- package/topmind-briefs/references/image-sourcing.md +1 -1
- package/topmind-briefs/references/writing-principles.md +7 -3
- package/topmind-briefs/scripts/de_ai_lint.py +9 -2
- package/topmind-briefs/scripts/md2wechat.py +77 -19
- package/topmind-briefs/scripts/md2x-html.py +88 -7
- package/topmind-cover/README.en.md +1 -1
- package/topmind-cover/README.md +1 -1
- package/topmind-cover/SKILL.md +1 -1
- package/topmind-cover/package.json +1 -1
- package/topmind-cover/scripts/de_ai_lint.py +9 -2
- package/topmind-format/README.en.md +1 -1
- package/topmind-format/README.md +1 -1
- package/topmind-format/SKILL.md +1 -1
- package/topmind-format/package.json +1 -1
- package/topmind-format/scripts/de_ai_lint.py +9 -2
- package/topmind-poster/README.en.md +1 -1
- package/topmind-poster/README.md +1 -1
- package/topmind-revise/CHANGELOG.md +5 -0
- package/topmind-revise/README.en.md +1 -1
- package/topmind-revise/README.md +1 -1
- package/topmind-revise/SKILL.md +2 -1
- package/topmind-revise/package.json +1 -1
- package/topmind-revise/references/image-sourcing.md +1 -1
- package/topmind-revise/scripts/audit-provenance.py +12 -1
- package/topmind-revise/scripts/check_article.py +206 -0
- package/topmind-revise/scripts/de_ai_lint.py +9 -2
- package/topmind-revise/scripts/negative_tests.py +10 -1
- package/topmind-viral-posts/README.en.md +1 -1
- package/topmind-viral-posts/README.md +1 -1
- package/topmind-viral-posts/SKILL.md +1 -1
- package/topmind-viral-posts/package.json +1 -1
- package/topmind-viral-posts/references/image-sourcing.md +1 -1
- package/topmind-viral-posts/references/writing-principles.md +7 -3
- package/topmind-viral-posts/scripts/de_ai_lint.py +9 -2
- package/topmind-wechat-post/README.en.md +29 -17
- package/topmind-wechat-post/README.md +32 -20
- package/topmind-wechat-post/SKILL.md +72 -53
- package/topmind-wechat-post/package.json +1 -1
- package/topmind-wechat-post/references/element-spec.md +9 -6
- package/topmind-wechat-post/references/image-sourcing.md +1 -1
- package/topmind-wechat-post/references/known-pits.md +11 -3
- package/topmind-wechat-post/references/typography-rules.md +13 -15
- package/topmind-wechat-post/references/workflow.md +33 -23
- package/topmind-wechat-post/references/writing-principles.md +7 -3
- package/topmind-wechat-post/scripts/article_scaffold.py +95 -0
- package/topmind-wechat-post/scripts/audit-provenance.py +12 -1
- package/topmind-wechat-post/scripts/check_article.py +206 -0
- package/topmind-wechat-post/scripts/de_ai_lint.py +9 -2
- package/topmind-wechat-post/scripts/lint-wechat.py +12 -2
- package/topmind-wechat-post/scripts/md2wechat.py +77 -19
- package/topmind-wechat-post/scripts/negative_tests.py +77 -4
- package/topmind-wechat-post/scripts/new-article.py +80 -9
- package/topmind-wechat-post/scripts/push-to-topstream.py +5 -4
- package/topmind-wechat-post/scripts/sync-mapping.py +20 -12
- package/topmind-wechat-post/scripts/sync-status.py +19 -10
- package/topmind-wechat-post/scripts/wechat_paths.py +21 -0
- package/topmind-x-article/README.en.md +15 -11
- package/topmind-x-article/README.md +15 -10
- package/topmind-x-article/SKILL.md +67 -85
- package/topmind-x-article/package.json +1 -1
- package/topmind-x-article/references/image-sourcing.md +1 -1
- package/topmind-x-article/references/publish-checklist.md +10 -6
- package/topmind-x-article/references/writing-principles.md +7 -3
- package/topmind-x-article/references/x-format.md +13 -12
- package/topmind-x-article/references/x-html-format.md +8 -5
- package/topmind-x-article/scripts/article_scaffold.py +95 -0
- package/topmind-x-article/scripts/audit-provenance.py +12 -1
- package/topmind-x-article/scripts/check_article.py +206 -0
- package/topmind-x-article/scripts/de_ai_lint.py +9 -2
- package/topmind-x-article/scripts/md2x-html.py +88 -7
- package/topmind-x-article/scripts/md2x.py +26 -10
- package/topmind-x-article/scripts/negative_tests.py +52 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## [0.8.45] - 2026-10-18
|
|
2
|
+
|
|
3
|
+
> 根包 0.8.44 → **0.8.45**(patch);技能版本:topmind-wechat-post 0.3.15 → 0.3.16、topmind-x-article 0.4.21 → 0.4.22、topmind-revise 0.1.0 → 0.1.1、topmind-briefs 0.2.23 → 0.2.24、topmind-cover 0.4.12 → 0.4.13、topmind-format 0.1.8 → 0.1.9、topmind-viral-posts 0.3.15 → 0.3.16(后四个只是随包共享副本更新);topmind-poster 0.1.6 不变。
|
|
4
|
+
|
|
5
|
+
### 新增(长文交付改造:单源 Markdown,HTML 按需)
|
|
6
|
+
|
|
7
|
+
- 长文改为一个文章文件夹 `<文章名>/`:唯一源稿 `<文章名>.md`(frontmatter 五项 title / date / status / tags / cover,H1 = title = 文件名)、`images/`(按出现顺序编号)、`cover/`、私有 `素材来源.md`、`raw/`、`out/`(派生产物,可随时重建)。默认只交付 Markdown,Obsidian 里直接能看;要 X 版或公众号版时再派生到 `out/`。
|
|
8
|
+
- 新增共享脚本 `article_scaffold.py`(建文章文件夹;文件名安全化、最长 80 字;已存在就报错,不覆盖)和 `check_article.py`(E 级:frontmatter 五项、标题一致、图片存在、编号顺序与出现顺序一致(后出现的图序号反而更小才报)、无 `![[ ]]` / callout / `:::`;W 级:没被引用的图、远程图片、编号不连续或带 11a 这类插入后缀、自制图标注 W18;有 E 退出码 1)。
|
|
9
|
+
- topmind-wechat-post / topmind-x-article 默认组合 topmind-revise:写完先由 revise 执行改稿与质量三关(嵌入模式,完整报告落 `out/改稿报告.md`),HTML / 纯文本只在用户要时生成。
|
|
10
|
+
|
|
11
|
+
### 变更
|
|
12
|
+
|
|
13
|
+
- `md2x-html.py`:源稿没有 `[图N]` 时直接按文档顺序读独占一行的 `![]()`(路径相对 md 所在目录,代码块内不动,远程图片跳过并告警),不再需要派生 `X长文.md`;图片下一行的「来源:」渲染成小号灰字图注;`--cover` 缺省读 frontmatter 的 `cover`;`--out` 缺省 `<文章文件夹>/out/X长文.html`;有 `[图N]` 却没给图片来源时直接报错。`--images-from` / `--images` 旧用法保留。
|
|
14
|
+
- `md2x.py`:列表默认 `plain`(「· 」「1)」,二级缩进两格),`--list-style emoji` 可选;`--out` 缺省 `out/X发布稿.txt`。
|
|
15
|
+
- `md2wechat.py`:二级标题有手写序号(「一、」「1.」)时原样保留,不再剥离、不再叠加自动编号,导读用「·」;新增 `--auto-number` 恢复旧行为;`--out-dir` 缺省 `<输入目录>/out`、`--slug` 缺省文件名、`--asset-root` 缺省输入目录;图片后的「来源:」行渲染成图注。
|
|
16
|
+
- wechat-post:`new-article.py` 默认建新结构,`--legacy --slug` 保留旧的 `YYYY-MM-DD-slug/公众号稿.md`;`sync-status.py` / `sync-mapping.py` / `push-to-topstream.py` 新旧结构都认,新结构只改 frontmatter 的 status、不改目录名,旧包的 `-released` 改名逻辑不变;`lint-wechat.py` 对新源稿里的 `:::` 报错(`公众号稿.md` 或 `--legacy` 只告警);`direction / source_file / target_file / word_count` 改为可选字段。
|
|
17
|
+
- `de_ai_lint.py` 的 W18:图片下一行写了「来源:自制,数据:…」也算标清楚;`audit-provenance.py` 的 `--draft` 可以直接给文章文件夹。
|
|
18
|
+
- 文档:wechat-post / x-article 的 SKILL.md、README 与 references(workflow、typography-rules、element-spec、known-pits、x-html-format、x-format、publish-checklist)换成新文件夹与按需派生;x-article 的「文本派生铁律」「HTML 占位符」「配图错位」三节合并为「派生规则」;`writing-principles.md` 第六节改为「单源、按需派生」。
|
|
19
|
+
- 旧交付包只读兼容,不自动迁移。
|
|
20
|
+
|
|
1
21
|
## [0.8.44] - 2026-10-18
|
|
2
22
|
|
|
3
23
|
> 根包 0.8.43 → **0.8.44**(patch);技能版本:新增 topmind-revise **0.1.0**;topmind-briefs 0.2.22 → 0.2.23、topmind-cover 0.4.11 → 0.4.12、topmind-format 0.1.7 → 0.1.8、topmind-viral-posts 0.3.14 → 0.3.15、topmind-wechat-post 0.3.14 → 0.3.15、topmind-x-article 0.4.20 → 0.4.21;topmind-poster 0.1.6 不变。
|
package/README.en.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
### topmind-wechat-post · WeChat article authoring
|
|
21
21
|
|
|
22
|
-
Full lifecycle for WeChat articles:
|
|
22
|
+
Full lifecycle for WeChat articles: single-source article folder (`<title>/<title>.md`), review & rewrite, three quality gates (run by topmind-revise by default: facts / logic / de-AI-flavor), WeChat inline typography on request (images must be embedded, output goes to `out/`), publish checklist and status sync. Hand-written heading numbers are kept as written; legacy `公众号稿.md` packages stay readable. Python stdlib only — zero dependencies.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
npx @topmindspace/topmind-writing-skills install topmind-wechat-post
|
|
@@ -27,7 +27,7 @@ npx @topmindspace/topmind-writing-skills install topmind-wechat-post
|
|
|
27
27
|
|
|
28
28
|
### topmind-x-article · One-click X long-form publishing
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Delivers one `<title>.md` by default (after topmind-revise); when you want to post on X, derives into `out/` on request: one-click-copy HTML (rich text; reads the source's `![]()` lines and embeds images in order, "来源:" lines become captions) / plain-text fallback (`md2x.py`: images → `[图N]`, tables → "item: value" lists, lists → "·" / "1)" with emoji optional, because plain-text X does not render Markdown lists) + cover art + publish checklist. Publishing is done by hand-pasting; fetch the text back for verification after posting.
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
npx @topmindspace/topmind-writing-skills install topmind-x-article
|
|
@@ -119,7 +119,7 @@ The writing skills share one rule set (`shared/references/`, copied into each sk
|
|
|
119
119
|
|
|
120
120
|
Also:
|
|
121
121
|
|
|
122
|
-
- **Originals first** (`image-sourcing.md`): find official / original-post / third-party images before writing and log sources in
|
|
122
|
+
- **Originals first** (`image-sourcing.md`): find official / original-post / third-party images before writing and log sources in the private `素材来源.md` inside the article folder; self-made charts are a fallback and must be labeled 自制 with the data source.
|
|
123
123
|
- **De-AI pass is mandatory**: rewrite boundaries first (change wording, never facts or certainty); W warnings need a decision rather than a zero count; the regression set `scripts/test_lint_rules.py` keeps normal writing from being flagged. topmind-format reports hits without rewriting.
|
|
124
124
|
- **Fixed revision order** (`revision-workflow.md`): facts → logic → cut redundancy → de-AI → voice check → layout → fidelity check → over-edit check; back up first and report before/after word counts (`count_words.py`). `topmind-revise` runs it when installed.
|
|
125
125
|
|
|
@@ -145,16 +145,16 @@ Every skill ships sample outputs written by us (fictional or neutral topics; no
|
|
|
145
145
|
|
|
146
146
|
| Skill | Version | What it does |
|
|
147
147
|
|-------|---------|--------------|
|
|
148
|
-
| [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.
|
|
149
|
-
| [`topmind-x-article`](./topmind-x-article/) | **0.4.
|
|
150
|
-
| [`topmind-cover`](./topmind-cover/) | **0.4.
|
|
151
|
-
| [`topmind-briefs`](./topmind-briefs/) | **0.2.
|
|
152
|
-
| [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.
|
|
153
|
-
| [`topmind-format`](./topmind-format/) | **0.1.
|
|
154
|
-
| [`topmind-revise`](./topmind-revise/) | **0.1.
|
|
148
|
+
| [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.16** | WeChat article lifecycle: single-source article folder, review, 3 quality gates (revise by default), inline typography on request & publish checklist |
|
|
149
|
+
| [`topmind-x-article`](./topmind-x-article/) | **0.4.22** | X long-form: single-source Markdown (revise by default) → on-request one-click-copy HTML (images/prompt copy, click-to-zoom images) / plain-text fallback + cover + checklist |
|
|
150
|
+
| [`topmind-cover`](./topmind-cover/) | **0.4.13** | Cover art for X / WeChat (X master 1500×600 / 5:2): striking, theme-focused; 22-style cover style library + 16:9 X short-post cover scene + 24 title treatments + 7-layer scene recipe + 55 example images (22 styles × 2 sizes + 11 variant samples) + 1 style overview |
|
|
151
|
+
| [`topmind-briefs`](./topmind-briefs/) | **0.2.24** | Short-form briefs (WeChat + X): one topic, one chart; data rankings / paper explainers / product alerts / mechanism explainers; sources → images → content → layout; one copy-ready plain-text X block (no HTML by default, emoji optional, no AI tone), links / images sent separately; asks before generating any image |
|
|
152
|
+
| [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.16** | X viral short posts: 18 template types + combinations, duanzi layout (blank lines, no emoji, plain bullet lines), one copy-ready plain-text block per post with hashtags last, links / images sent separately, classification tags listed separately; asks before generating an image |
|
|
153
|
+
| [`topmind-format`](./topmind-format/) | **0.1.9** | X plain-text formatter: to duanzi (blank lines, no emoji) / to short post (paragraphing + sensible lists, emoji optional); layout only, words unchanged, with a fidelity check; one copy-ready plain-text body, links / images sent separately |
|
|
154
|
+
| [`topmind-revise`](./topmind-revise/) | **0.1.1** | Revision and quality check: edit, tighten, de-AI, three quality gates and fidelity audit; four modes (audit / revise / embed / quick); every change maps to a rule code, with an over-edit guard; the long-form skills use it by default |
|
|
155
155
|
| [`topmind-poster`](./topmind-poster/) | **0.1.6** | HTML+CSS → high-res long image / poster / infographic PNG: headless Chrome 2× screenshot + auto-trim; "one image per section" batch workflow (generator + renderer) |
|
|
156
156
|
|
|
157
|
-
Installer [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) is **0.8.
|
|
157
|
+
Installer [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) is **0.8.45** (whole-repo same-tag releases).
|
|
158
158
|
|
|
159
159
|
### Which skill, and how they combine
|
|
160
160
|
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
### topmind-wechat-post · 公众号创作
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
公众号文章全生命周期:单源文章文件夹(`<文章名>/<文章名>.md`)、审校改写、质量三关(默认组合 topmind-revise:事实 / 逻辑 / 去 AI 味)、按需出微信内联排版(图片必内嵌,产物进 `out/`)、发布清单与状态同步。手写小标题序号原样保留;旧 `公众号稿.md` 交付包只读兼容。脚本纯 Python 标准库,零依赖。
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npx @topmindspace/topmind-writing-skills install topmind-wechat-post
|
|
@@ -25,7 +25,7 @@ npx @topmindspace/topmind-writing-skills install topmind-wechat-post
|
|
|
25
25
|
|
|
26
26
|
### topmind-x-article · X 长文一键发布
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
默认交付一份 `<文章名>.md`(先过 topmind-revise);要发 X 时再按需派生到 `out/`:一键复制 HTML(富文本,直接读源稿的 `![]()` 按顺序内嵌配图,「来源:」行成图注)/ 纯文本兜底(`md2x.py`:图片→`[图N]`、表格→"项:值"列表、列表→「·」「1)」,emoji 可选,因为 X 纯文本不渲染 Markdown 列表)+ 封面图 + 发布清单。发布走人工粘贴,发布后抓回核对。
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npx @topmindspace/topmind-writing-skills install topmind-x-article
|
|
@@ -119,7 +119,7 @@ npx @topmindspace/topmind-writing-skills install topmind-poster
|
|
|
119
119
|
|
|
120
120
|
另外:
|
|
121
121
|
|
|
122
|
-
- **原图优先**(`image-sourcing.md`):写字之前先找官方 / 原帖 /
|
|
122
|
+
- **原图优先**(`image-sourcing.md`):写字之前先找官方 / 原帖 / 第三方原图,来源记进文章文件夹里的私有 `素材来源.md`;自制图只作兜底,标「自制」和数据来源。
|
|
123
123
|
- **去 AI 味必过**:先守改写边界(只改表达,不改事实和确定性);W 警告不要求清零,要求每条有结论;规则回归测试 `scripts/test_lint_rules.py` 守住「不应报」的正常写法。topmind-format 只报告命中、不改字。
|
|
124
124
|
- **固定改稿流程**(`revision-workflow.md`):事实 → 逻辑 → 删冗余 → 去 AI 味 → 核心经验对照 → 排版 → 保真审计 → 过度改稿检查;每轮先备份,报改前 / 改后字数(`count_words.py`)。装了 `topmind-revise` 就由它执行。
|
|
125
125
|
|
|
@@ -145,16 +145,16 @@ npx @topmindspace/topmind-writing-skills install topmind-poster
|
|
|
145
145
|
|
|
146
146
|
| 技能 | 版本 | 做什么 |
|
|
147
147
|
|------|------|--------|
|
|
148
|
-
| [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.
|
|
149
|
-
| [`topmind-x-article`](./topmind-x-article/) | **0.4.
|
|
150
|
-
| [`topmind-cover`](./topmind-cover/) | **0.4.
|
|
151
|
-
| [`topmind-briefs`](./topmind-briefs/) | **0.2.
|
|
152
|
-
| [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.
|
|
153
|
-
| [`topmind-format`](./topmind-format/) | **0.1.
|
|
154
|
-
| [`topmind-revise`](./topmind-revise/) | **0.1.
|
|
148
|
+
| [`topmind-wechat-post`](./topmind-wechat-post/) | **0.3.16** | 公众号文章全生命周期:单源文章文件夹、审校改写、质量三关(默认组合 revise)、按需出微信内联排版与发布清单 |
|
|
149
|
+
| [`topmind-x-article`](./topmind-x-article/) | **0.4.22** | X 长文:单源 Markdown(默认组合 revise)→ 按需出一键复制 HTML(含配图/提示词复制、图片点击放大)/ 纯文本兜底 + 封面图 + 发布清单 |
|
|
150
|
+
| [`topmind-cover`](./topmind-cover/) | **0.4.13** | 文章封面配图(X / 公众号共用,X 主尺寸 1500×600 / 5:2):震撼醒目主题突出;22 种封面风格库 + X 短文 16:9 封面场景 + 24 种标题花式 + 七层场景配方 + 55 张示例图(22 种风格 × 2 尺寸 + 11 张变体样张) + 1 张风格总览图 |
|
|
151
|
+
| [`topmind-briefs`](./topmind-briefs/) | **0.2.24** | 干货短文(公众号 + X 双平台):一件事讲透,数据榜单/论文解读/新品速递;信源 → 原图 → 内容 → 排版;X 版交付一段可复制纯文本(默认不出 HTML,emoji 可选,去 AI 味),链接 / 原图 / 配图另发,无原图先询问配图 |
|
|
152
|
+
| [`topmind-viral-posts`](./topmind-viral-posts/) | **0.3.16** | X 引流段子/爆款短篇:18 大类型模板 + 组合玩法,段子体(行间空行,零 emoji,分点不加符号)、每条一段可复制纯文本、话题标签在末行,链接 / 原图另发、分类标签另列;无原图先询问再走 topmind-cover |
|
|
153
|
+
| [`topmind-format`](./topmind-format/) | **0.1.9** | X 纯文本格式化:转段子(行间空行,零 emoji)/ 格式化成短文(分段 + 合理列表,emoji 可选),只动排版不改字,带原文保真校验;交付一段可复制纯文本,链接 / 原图另发 |
|
|
154
|
+
| [`topmind-revise`](./topmind-revise/) | **0.1.1** | 改稿与质检:改稿、精简、去 AI 味、质量三关与保真审计;四种模式(只审不改 / 改稿 / 嵌入 / 快审),每处改动对应规则编号,防过度改稿;长文技能默认组合 |
|
|
155
155
|
| [`topmind-poster`](./topmind-poster/) | **0.1.6** | HTML+CSS → 高清长图 / 海报 / 信息图 PNG:无头 Chrome 2× 截图 + 自动裁白;「一段一图」分段图批量出图(生成器 + 渲染器) |
|
|
156
156
|
|
|
157
|
-
安装器 [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) 为 **0.8.
|
|
157
|
+
安装器 [`@topmindspace/topmind-writing-skills`](https://www.npmjs.com/package/@topmindspace/topmind-writing-skills) 为 **0.8.45**(整仓同 tag 发版)。
|
|
158
158
|
|
|
159
159
|
### 怎么选、怎么组合
|
|
160
160
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.24 (2026-10-18)
|
|
4
|
+
|
|
5
|
+
- 随包共享脚本更新:`md2x-html.py` 能直接读源稿的 `![]()` 按顺序嵌图、「来源:」行渲染成图注、`--out` 缺省 `out/X长文.html`(原有 `--images-from` 用法不变);`md2wechat.py` 默认保留手写小标题序号(`--auto-number` 恢复旧行为),`--out-dir` / `--slug` 有缺省值
|
|
6
|
+
- `de_ai_lint.py` 的 W18:图片下一行写了「来源:自制,数据:…」也算标清楚
|
|
7
|
+
|
|
3
8
|
## 0.2.23 (2026-10-18)
|
|
4
9
|
|
|
5
10
|
- 写法规则改为引用共享的 `core-voice.md`:首行一句话题陈述、事实讲完就停(去掉收尾句)、列表默认「·」、话题标签默认 2 个;引语正文写「人名:中文意思」;未完全确认的热点首行用一次「?!」,不写泛泛的「据报道」
|
|
@@ -7,7 +7,7 @@ A brief is not a long-form article — **no HTML by default**: the workflow is v
|
|
|
7
7
|
The X version is one copy-ready plain-text block (emoji optional); **links (including sources), original images and any generated images are sent separately**, never mixed into the body.
|
|
8
8
|
The WeChat version is ~300–800 Chinese characters of Markdown. With no original image it asks before generating one.
|
|
9
9
|
|
|
10
|
-
- Version: **v0.2.
|
|
10
|
+
- Version: **v0.2.24** (ships with `@topmindspace/topmind-writing-skills@0.8.45`)
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
package/topmind-briefs/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
X 版交付一段可直接复制的纯文本(emoji 可选),**链接(含来源)、原图、配图另发**,不和正文混在一起;公众号版约 300–800 字 Markdown。
|
|
8
8
|
没有原图时先问要不要配图(`topmind-cover`);链接那一条写明来源名称、发布时间与 URL。
|
|
9
9
|
|
|
10
|
-
- 版本:**v0.2.
|
|
10
|
+
- 版本:**v0.2.24**(随 `@topmindspace/topmind-writing-skills@0.8.45` 发布)
|
|
11
11
|
|
|
12
12
|
## 安装
|
|
13
13
|
|
package/topmind-briefs/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: topmind-briefs
|
|
|
3
3
|
description: "干货短文:一件事讲透,X + 公众号双平台。短文不是长文——默认不出 HTML:工作流按「核实一手信源 → 找原帖原图 → 写内容 → 排版」;X 版交付一段可直接复制的纯文本(首行一句话题陈述、合理分段与列表、去 AI 味、emoji 可选且只用 TopMind 体系、末行默认 2 个话题标签),链接(含来源)、原图(1–2 张)、配图另发,绝不混进正文;公众号版是 300–800 字 Markdown;没有原图时主动询问是否配图,不擅自生图。尊重原始信息、原图、原文。只交付不代发。Use when 写短文、公众号短文、X 短文、发快讯、简讯、速报、数据榜单、榜单速报、论文解读、新品速递、来篇短的、短平快、brief;信息密度型(数据、榜单、发布)走本技能。Do NOT use for 把现成文字格式化、转段子、排版(→ topmind-format)、X 长文与发 X 文章(→ topmind-x-article)、公众号长文与公众号排版 HTML(→ topmind-wechat-post)、引流互动型短帖、段子、涨粉帖、互关帖、爆款短篇(→ topmind-viral-posts)、单独出长图/海报/信息图 PNG(→ topmind-poster)、封面与头图(→ topmind-cover)、单独改稿、去 AI 味与质检(→ topmind-revise)、把稿子发出去(→ topmind-x,须用户确认)。"
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
|
-
version: "0.2.
|
|
6
|
+
version: "0.2.24"
|
|
7
7
|
action_category: "write"
|
|
8
8
|
triggers: "写短文, 公众号短文, X 短文, 发快讯, 快讯, 简讯, 速报, 干货短文, 论文解读, 数据榜单, 榜单速报, 新品速递, brief, 来篇短的, 短平快, 发个快讯"
|
|
9
9
|
author: "TopMindSpace"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
## 二、自制图规则
|
|
14
14
|
|
|
15
15
|
- 图内或图注写明「自制」和数据来源,例如「自制,数据:xx 官方 2026-09」。
|
|
16
|
-
-
|
|
16
|
+
- 长文源稿 `<文章名>.md` 的自制图写成 ``,或在图片下一行写「来源:自制,数据:xx」;`lint_x_post.py --md` / `--long` 会对没标清的自制图报 W18。
|
|
17
17
|
- 不用 AI 生成的图冒充原图或数据图;示意图标「示意」。
|
|
18
18
|
|
|
19
19
|
## 三、私有信源清单(不进公开仓库)
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
> 能用可视化传达的,不用文字啰嗦。这是 X 和公众号共同的内容要求。
|
|
34
34
|
|
|
35
35
|
1. **表格 > 段落**:对比类内容用简单表格,不写大段文字。
|
|
36
|
-
2. **列表 >
|
|
36
|
+
2. **列表 > 段落**:并列要点用列表,不写成长段。长文源稿用 Markdown 列表;X 纯文本帖不渲染 Markdown,默认用「·」「1)」,emoji 项目符号可选(规则见 `x-post-format.md`)。
|
|
37
37
|
3. **"概要 + 列表"**:信息密集的节,先给一句概要(这节的结论),再用列表展开论据。
|
|
38
38
|
不写长段论述;非写不可的长段,拆成多个短段或转成"概要+列表"。
|
|
39
39
|
4. **图表 > 描述**:数据趋势用图表,图片用素材原图。
|
|
@@ -165,7 +165,8 @@ AI 味多数来自句式,不是单个词。下面每一类都给出改法,
|
|
|
165
165
|
> 以下为 topmind 工作区默认规约;其他用户可通过环境变量或 CLI 参数覆盖,
|
|
166
166
|
> 技能本身不强制依赖特定目录结构。
|
|
167
167
|
|
|
168
|
-
1.
|
|
168
|
+
1. **目录规约(默认)**:一篇长文一个文章文件夹 `<文章名>/`,里面只有一份源稿 `<文章名>.md`
|
|
169
|
+
(外加 `images/`、`cover/`、`素材来源.md`、`raw/`、`out/`),放在工作区的长文类专题下(按类别**名称**找「长文」,
|
|
169
170
|
兼容旧名「创作」「专题」;专题默认 `{当年}-公众号`),正式稿 HTML 交付到 role:delivery 类别
|
|
170
171
|
(以工作区 `topmind.yaml` 的实际目录为准,例如 `88-输出/`)。编号与年份不写死。
|
|
171
172
|
覆盖方式:`--base` 参数 > `TOPMIND_WECHAT_BASE` > `TOPMIND_WORKSPACE`(按名称发现类别);
|
|
@@ -173,7 +174,10 @@ AI 味多数来自句式,不是单个词。下面每一类都给出改法,
|
|
|
173
174
|
2. **云端备份**:所有文章创作结果默认在云端工作区按规约格式目录备份存档,
|
|
174
175
|
方便找回/暂存。
|
|
175
176
|
3. **封面默认 2 版**:每篇长文默认提供至少 2 版不同风格 cover 供用户选择。
|
|
176
|
-
4.
|
|
177
|
+
4. **单源、按需派生**:长文默认只交付一份 Markdown(`<文章名>.md`,Obsidian 里直接能看);
|
|
178
|
+
用户要哪个平台的版本,再从它派生到 `out/`(X 长文 HTML / 纯文本 → `topmind-x-article`,
|
|
179
|
+
公众号 HTML → `topmind-wechat-post`)。`out/` 是生成文件,不手改;改稿永远只改源稿,改完重新派生。
|
|
180
|
+
短文、段子、回复不变:一段可复制纯文本,链接、原图另发。旧的「公众号稿 + 派生 X 稿」交付包只读兼容。
|
|
177
181
|
|
|
178
182
|
## 七、研究深度要求(用户给定主题)
|
|
179
183
|
|
|
@@ -290,8 +290,15 @@ def image_label_hits(lines: list[str], start: int):
|
|
|
290
290
|
if not m:
|
|
291
291
|
continue
|
|
292
292
|
alt, path = m.group(1), m.group(2)
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
# 新结构里出处写在图片下一行(可隔一个空行)的「来源:」,也算图注
|
|
294
|
+
nxt = ""
|
|
295
|
+
for k in (off + 1, off + 2):
|
|
296
|
+
if k < len(lines) and lines[k].strip():
|
|
297
|
+
nxt = lines[k].strip() if re.match(r"^来源[::]", lines[k].strip()) else ""
|
|
298
|
+
break
|
|
299
|
+
label = alt + " " + nxt
|
|
300
|
+
if SELFMADE_HINT_RE.search(alt + " " + path) and not (SELFMADE_HINT_RE.search(label) and (SOURCE_HINT_RE.search(alt) or nxt)):
|
|
301
|
+
out.append((start + off, "W18", "自制图没标清楚:图注(alt)或下一行「来源:」里写「自制」和数据来源,例如「来源:自制,数据:xx 官方」"))
|
|
295
302
|
return out
|
|
296
303
|
|
|
297
304
|
|
|
@@ -14,9 +14,16 @@ Supported beyond CommonMark:
|
|
|
14
14
|
- four heading-2 style variants
|
|
15
15
|
|
|
16
16
|
Usage:
|
|
17
|
+
python3 md2wechat.py --input <文章名>/<文章名>.md --embed-images
|
|
18
|
+
# 文章文件夹结构:--out-dir 缺省 <文章名>/out,--slug 缺省文件名,
|
|
19
|
+
# --asset-root 缺省文章文件夹
|
|
17
20
|
python3 md2wechat.py --input 稿.md --out-dir 交付包 --slug foo \
|
|
18
21
|
--asset-root /path/to/repo [--theme theme.json] [--link-mode footnote] \
|
|
19
|
-
[--embed-images]
|
|
22
|
+
[--embed-images] [--auto-number]
|
|
23
|
+
|
|
24
|
+
Headings: 二级标题写了「一、」「1.」这类手写序号就原样保留,不再自动编号;
|
|
25
|
+
都没写时按主题样式自动编号。--auto-number 恢复旧行为(剥离手写序号后统一编号)。
|
|
26
|
+
Images: 图片后紧跟(或隔一个空行)的「来源:……」行渲染成图注样式。
|
|
20
27
|
|
|
21
28
|
Outputs:
|
|
22
29
|
<out-dir>/<slug>-公众号版.html full page, copy body section into WeChat
|
|
@@ -581,13 +588,16 @@ def match_img_only(line):
|
|
|
581
588
|
return None
|
|
582
589
|
return line[2:k], src
|
|
583
590
|
CAPTION_RE = re.compile(r"^\*(.+)\*\s*$")
|
|
591
|
+
# 图片后紧跟的「来源:」行渲染成图注(core-voice V14);中间最多隔一个空行
|
|
592
|
+
SOURCE_RE = re.compile(r"^(来源[::].+)$")
|
|
584
593
|
TABLE_SEP_RE = re.compile(r"^\|?[\s:|-]+\|[\s:|-]*$")
|
|
585
594
|
CONTAINER_RE = re.compile(r"^:::+\s*(\w+)?\s*(.*)$")
|
|
586
595
|
|
|
587
596
|
CN_NUM = "一二三四五六七八九十壹贰叁肆伍陆柒捌玖拾"
|
|
588
597
|
|
|
589
|
-
#
|
|
590
|
-
#
|
|
598
|
+
# 标题里手写的中文/阿拉伯序号。默认:稿子里的二级标题带手写序号(「一、」「1.」)就原样保留,
|
|
599
|
+
# 排版层不再自动编号;二级标题都没写序号时才按主题样式自动编号。--auto-number 恢复旧行为:
|
|
600
|
+
# 一律剥离手写序号后由排版层重新编号。
|
|
591
601
|
# 注意:\d{1,2}[、..](?!\d) 带负向断言,避免把「3.4× 成本」这类
|
|
592
602
|
# 以数字开头的标题误剥成「4× 成本」。
|
|
593
603
|
HAND_NUM_RE = re.compile(
|
|
@@ -625,6 +635,16 @@ def para_style(theme):
|
|
|
625
635
|
% (theme["para_gap"], theme["line_height"], theme["align"]))
|
|
626
636
|
|
|
627
637
|
|
|
638
|
+
def keep_hand_num(theme):
|
|
639
|
+
return bool(theme.get("_keep_hand_num"))
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def detect_hand_numbers(lines):
|
|
643
|
+
"""二级标题里有没有手写序号(「一、」「1.」「第一章」)。"""
|
|
644
|
+
return any(HAND_NUM_RE.match(l.strip()[3:].strip())
|
|
645
|
+
for l in lines if l.strip().startswith("## "))
|
|
646
|
+
|
|
647
|
+
|
|
628
648
|
def render_heading(level, text, theme, index=None):
|
|
629
649
|
accent = theme["accent"]
|
|
630
650
|
if level == 1:
|
|
@@ -633,12 +653,18 @@ def render_heading(level, text, theme, index=None):
|
|
|
633
653
|
return ('<p style="font-size:20px;font-weight:700;color:%s;'
|
|
634
654
|
'text-align:center;margin:0 0 18px;line-height:1.5;'
|
|
635
655
|
'letter-spacing:0.5px;">%s</p>'
|
|
636
|
-
% (theme["text_strong"], render_inline(
|
|
656
|
+
% (theme["text_strong"], render_inline(text.strip() if keep_hand_num(theme)
|
|
657
|
+
else (HAND_NUM_RE.sub("", text).strip() or text))))
|
|
637
658
|
|
|
638
659
|
if level == 2:
|
|
639
660
|
style = theme.get("h2_style", "number")
|
|
640
|
-
|
|
641
|
-
|
|
661
|
+
if keep_hand_num(theme):
|
|
662
|
+
# 保留手写序号:标题原样输出,不再补排版层编号
|
|
663
|
+
plain = text.strip()
|
|
664
|
+
index = None
|
|
665
|
+
else:
|
|
666
|
+
# 先剥离手写序号,再按主题样式决定是否由排版层补编号
|
|
667
|
+
plain = HAND_NUM_RE.sub("", text).strip() or text.strip()
|
|
642
668
|
t = render_inline(plain)
|
|
643
669
|
num = ""
|
|
644
670
|
if "number" in style and index:
|
|
@@ -672,7 +698,8 @@ def render_heading(level, text, theme, index=None):
|
|
|
672
698
|
% (theme["text_strong"], accent, num, t))
|
|
673
699
|
|
|
674
700
|
if level == 3:
|
|
675
|
-
t = render_inline(
|
|
701
|
+
t = render_inline(text.strip() if keep_hand_num(theme)
|
|
702
|
+
else (HAND_NUM_RE.sub("", text).strip() or text))
|
|
676
703
|
return ('<h3 style="font-size:16px;font-weight:600;color:%s;'
|
|
677
704
|
'margin:26px 0 12px;line-height:1.5;">%s</h3>'
|
|
678
705
|
% (theme["text_strong"], t))
|
|
@@ -878,12 +905,18 @@ def render_hr(theme):
|
|
|
878
905
|
'margin:0 auto;"></section></section>' % theme["accent"])
|
|
879
906
|
|
|
880
907
|
|
|
881
|
-
def render_figure(src, caption, theme, alt=""):
|
|
908
|
+
def render_figure(src, caption, theme, alt="", source=None):
|
|
882
909
|
cap = ""
|
|
883
910
|
if caption:
|
|
884
911
|
cap = ('<p style="font-size:13px;color:%s;text-align:center;'
|
|
885
912
|
'margin:9px 0 %s;line-height:1.6;">%s</p>'
|
|
886
|
-
% (theme["text_light"], theme["para_gap"], render_inline(caption)))
|
|
913
|
+
% (theme["text_light"], "4px" if source else theme["para_gap"], render_inline(caption)))
|
|
914
|
+
if source:
|
|
915
|
+
cap += ('<p style="font-size:12px;color:%s;text-align:center;'
|
|
916
|
+
'margin:%s 0 %s;line-height:1.6;">%s</p>'
|
|
917
|
+
% (theme["text_light"], "0" if caption else "9px", theme["para_gap"],
|
|
918
|
+
render_inline(source)))
|
|
919
|
+
caption = caption or source
|
|
887
920
|
border = "" # MD3 优雅:图片无边框,只保留圆角
|
|
888
921
|
altattr = ' alt="%s"' % esc(alt) if alt else ""
|
|
889
922
|
# 图片一律 max-width:100%,绝不用 width:100%:正文容器约 677px,
|
|
@@ -1093,7 +1126,9 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1093
1126
|
toc_at = None # 导读落点:首个正文段落之后
|
|
1094
1127
|
|
|
1095
1128
|
# 预扫描:末章是否为收束类,决定它的编号用数字还是 ∞
|
|
1096
|
-
|
|
1129
|
+
keep = keep_hand_num(theme)
|
|
1130
|
+
h2_texts = [(l.strip()[3:].strip() if keep else
|
|
1131
|
+
(HAND_NUM_RE.sub("", l.strip()[3:]).strip() or l.strip()[3:].strip()))
|
|
1097
1132
|
for l in lines if l.strip().startswith("## ")]
|
|
1098
1133
|
h2_total = len(h2_texts)
|
|
1099
1134
|
last_is_summary = bool(h2_total) and bool(SUMMARY_H2_RE.match(h2_texts[-1]))
|
|
@@ -1144,8 +1179,8 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1144
1179
|
marker = h2_index
|
|
1145
1180
|
if level == 2:
|
|
1146
1181
|
h2_index += 1
|
|
1147
|
-
h2_titles.append(
|
|
1148
|
-
or m.group(2).strip())
|
|
1182
|
+
h2_titles.append(m.group(2).strip() if keep else
|
|
1183
|
+
(HAND_NUM_RE.sub("", m.group(2)).strip() or m.group(2).strip()))
|
|
1149
1184
|
marker = ("∞" if (last_is_summary and h2_index == h2_total)
|
|
1150
1185
|
else h2_index)
|
|
1151
1186
|
out.append(render_heading(level, m.group(2).strip(), theme, marker))
|
|
@@ -1199,14 +1234,24 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1199
1234
|
m = match_img_only(stripped)
|
|
1200
1235
|
if m:
|
|
1201
1236
|
caption = None
|
|
1237
|
+
source = None
|
|
1202
1238
|
if i + 1 < n:
|
|
1203
1239
|
cm = CAPTION_RE.match(lines[i + 1].strip())
|
|
1204
1240
|
if cm:
|
|
1205
1241
|
caption = cm.group(1)
|
|
1206
1242
|
i += 1
|
|
1243
|
+
# 「来源:」行(紧跟或隔一个空行)→ 图注样式
|
|
1244
|
+
j = i + 1
|
|
1245
|
+
if j < n and not lines[j].strip() and j + 1 < n:
|
|
1246
|
+
j += 1
|
|
1247
|
+
if j < n:
|
|
1248
|
+
sm = SOURCE_RE.match(lines[j].strip())
|
|
1249
|
+
if sm:
|
|
1250
|
+
source = sm.group(1)
|
|
1251
|
+
i = j
|
|
1207
1252
|
src = resolve_image(m[1], md_path, out_dir,
|
|
1208
1253
|
ctx["assets"], ctx["asset_root"])
|
|
1209
|
-
out.append(render_figure(src, caption, theme, alt=m[0]))
|
|
1254
|
+
out.append(render_figure(src, caption, theme, alt=m[0], source=source))
|
|
1210
1255
|
i += 1
|
|
1211
1256
|
continue
|
|
1212
1257
|
|
|
@@ -1243,8 +1288,10 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1243
1288
|
|
|
1244
1289
|
def convert(md_text, md_path, out_dir, theme, link_mode="footnote",
|
|
1245
1290
|
render_mermaid=False, asset_root=None, use_pangu=True,
|
|
1246
|
-
toc=True, sign_mode="auto", author="", author_bio=""):
|
|
1291
|
+
toc=True, sign_mode="auto", author="", author_bio="", auto_number=False):
|
|
1247
1292
|
lines = strip_frontmatter(md_text.split("\n"))
|
|
1293
|
+
theme = dict(theme)
|
|
1294
|
+
theme["_keep_hand_num"] = (not auto_number) and detect_hand_numbers(lines)
|
|
1248
1295
|
han_chars = sum(len(HAN_RE.findall(l)) for l in lines)
|
|
1249
1296
|
ctx = {"assets": [], "diagrams": [], "footnotes": [],
|
|
1250
1297
|
"render_mermaid": render_mermaid, "asset_root": asset_root,
|
|
@@ -1320,7 +1367,7 @@ def render_toc(titles, theme):
|
|
|
1320
1367
|
# 导读的标记必须跟正文标题一致:
|
|
1321
1368
|
# - 标题带编号(h2_style 含 number)→ 导读也用编号;末章收束类用 ∞,避免「导读 04 / 正文 ∞」
|
|
1322
1369
|
# - 标题不带编号(center/underline/plain)→ 导读只用中性圆点,不要凭空造出 01/02
|
|
1323
|
-
numbered = "number" in theme.get("h2_style", "number")
|
|
1370
|
+
numbered = "number" in theme.get("h2_style", "number") and not keep_hand_num(theme)
|
|
1324
1371
|
last_summary = bool(titles) and bool(SUMMARY_H2_RE.match(titles[-1]))
|
|
1325
1372
|
|
|
1326
1373
|
def label(k):
|
|
@@ -1696,6 +1743,9 @@ def main():
|
|
|
1696
1743
|
ap.add_argument("--author", default="",
|
|
1697
1744
|
help="签名区署名;留空则不输出署名行(不再写 {{作者名}} 占位)")
|
|
1698
1745
|
ap.add_argument("--author-bio", default="", help="签名区一句话简介")
|
|
1746
|
+
ap.add_argument("--auto-number", action="store_true",
|
|
1747
|
+
help="二级标题一律剥离手写序号、由排版层重新编号(旧行为);"
|
|
1748
|
+
"默认:稿里写了「一、」这类序号就原样保留")
|
|
1699
1749
|
args = ap.parse_args()
|
|
1700
1750
|
|
|
1701
1751
|
if args.list_themes:
|
|
@@ -1704,10 +1754,17 @@ def main():
|
|
|
1704
1754
|
"、".join(t["genre"]) or "(未声明题材)"))
|
|
1705
1755
|
return
|
|
1706
1756
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1757
|
+
if not args.input:
|
|
1758
|
+
sys.exit("缺少必填参数:--input")
|
|
1759
|
+
# 文章文件夹结构(<文章名>/<文章名>.md):--out-dir 缺省 <文章文件夹>/out,
|
|
1760
|
+
# --slug 缺省为文件名,--asset-root 缺省为文章文件夹
|
|
1761
|
+
in_dir = os.path.dirname(os.path.abspath(args.input))
|
|
1762
|
+
if not args.out_dir:
|
|
1763
|
+
args.out_dir = os.path.join(in_dir, "out")
|
|
1764
|
+
if not args.slug:
|
|
1765
|
+
args.slug = os.path.splitext(os.path.basename(args.input))[0]
|
|
1766
|
+
if not args.asset_root:
|
|
1767
|
+
args.asset_root = in_dir
|
|
1711
1768
|
|
|
1712
1769
|
if os.path.isdir(args.input):
|
|
1713
1770
|
sys.exit("错误:输入是目录不是文件 %s" % args.input)
|
|
@@ -1733,6 +1790,7 @@ def main():
|
|
|
1733
1790
|
not args.no_pangu,
|
|
1734
1791
|
toc=not args.no_toc, sign_mode=args.signature,
|
|
1735
1792
|
author=args.author, author_bio=args.author_bio,
|
|
1793
|
+
auto_number=args.auto_number,
|
|
1736
1794
|
)
|
|
1737
1795
|
|
|
1738
1796
|
title = args.title
|
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
"""md2x-html.py — Markdown 原稿 → 可直接复制的 X 长文 HTML(单文件)。
|
|
3
3
|
|
|
4
4
|
用法:
|
|
5
|
-
python3 md2x-html.py
|
|
6
|
-
|
|
5
|
+
python3 md2x-html.py <文章名>/<文章名>.md
|
|
6
|
+
# 文章文件夹结构:直接读源稿里的 ,按文档顺序嵌图;
|
|
7
|
+
# --out 缺省 <文章名>/out/X长文.html,--cover 缺省读 frontmatter 的 cover
|
|
8
|
+
python3 md2x-html.py X长文.md --out X长文.html --images-from 公众号稿.md # 旧结构
|
|
9
|
+
python3 md2x-html.py 原稿.md --out X长文.html --images 1.png 2.png [--cover c.png]
|
|
7
10
|
|
|
8
11
|
约定(与 md2x.py 对齐):
|
|
12
|
+
- 源稿没有 `[图N]`、只有独占一行的 `` 时,按文档顺序直接取图(路径相对 md 所在目录),
|
|
13
|
+
内部换成 `[图N]`,不需要再派生 X长文.md;远程图片无法内嵌,跳过并告警
|
|
14
|
+
- 图片后紧跟(或隔一个空行)的「来源:……」行渲染成小号灰字图注,复制时作为一段文字保留
|
|
9
15
|
- `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure>;
|
|
10
16
|
点击图片弹出 lightbox 放大查看原图(右键可另存/截图,不拦截右键菜单)
|
|
11
17
|
- ``` 围栏代码块(直出提示词)→ <blockquote> + 「复制提示词」按钮。
|
|
@@ -78,6 +84,7 @@ body { margin: 0; background: #f7f9fa; color: #0f1419;
|
|
|
78
84
|
figure.ximg { margin: 22px 0; }
|
|
79
85
|
figure.ximg img { width: 100%; border-radius: 10px; display: block;
|
|
80
86
|
border: 1px solid #eff3f4; cursor: zoom-in; }
|
|
87
|
+
.article p.src { font-size: 13px; line-height: 1.6; color: #536471; margin: -12px 0 18px; }
|
|
81
88
|
figure.ximg figcaption { display: flex; justify-content: space-between; align-items: center;
|
|
82
89
|
margin-top: 8px; font-size: 13px; color: #536471; }
|
|
83
90
|
.prompt { position: relative; margin: 14px 0 20px; }
|
|
@@ -341,6 +348,51 @@ def inline_md(s):
|
|
|
341
348
|
return s
|
|
342
349
|
|
|
343
350
|
|
|
351
|
+
IMG_LINE_RE = re.compile(r"^\s*!\[([^\]]*)\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)\s*$")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def inline_images(md_text, base):
|
|
355
|
+
"""把独占一行的  换成 [图N],返回 (新文本, 图片绝对路径列表)。
|
|
356
|
+
|
|
357
|
+
围栏代码块里的行不动;远程图片无法内嵌,换成一行「(远程图片:url)」并告警。"""
|
|
358
|
+
out, images, fence = [], [], None
|
|
359
|
+
for line in md_text.split("\n"):
|
|
360
|
+
st = line.strip()
|
|
361
|
+
fm = re.match(r"^(`{3,})", st)
|
|
362
|
+
if fm:
|
|
363
|
+
if fence is None:
|
|
364
|
+
fence = fm.group(1)
|
|
365
|
+
elif len(fm.group(1)) >= len(fence) and st.strip("`") == "":
|
|
366
|
+
fence = None
|
|
367
|
+
out.append(line)
|
|
368
|
+
continue
|
|
369
|
+
m = IMG_LINE_RE.match(line) if fence is None else None
|
|
370
|
+
if not m:
|
|
371
|
+
out.append(line)
|
|
372
|
+
continue
|
|
373
|
+
src = m.group(2)
|
|
374
|
+
if re.match(r"https?://", src):
|
|
375
|
+
print("警告:跳过远程图片(无法内嵌):%s" % src, file=sys.stderr)
|
|
376
|
+
out.append("(远程图片:%s)" % src)
|
|
377
|
+
continue
|
|
378
|
+
images.append(src if os.path.isabs(src) else os.path.normpath(os.path.join(base, src)))
|
|
379
|
+
out.append("[图%d]" % len(images))
|
|
380
|
+
return "\n".join(out), images
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def frontmatter_value(md_text, key):
|
|
384
|
+
lines = md_text.split("\n")
|
|
385
|
+
if not lines or lines[0].strip() != "---":
|
|
386
|
+
return None
|
|
387
|
+
for ln in lines[1:]:
|
|
388
|
+
if ln.strip() == "---":
|
|
389
|
+
break
|
|
390
|
+
m = re.match(r"^%s:\s*(.+?)\s*$" % re.escape(key), ln)
|
|
391
|
+
if m:
|
|
392
|
+
return m.group(1).strip().strip("\"'")
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
|
|
344
396
|
def md_to_html(md_text, images, cover):
|
|
345
397
|
lines = md_text.split("\n")
|
|
346
398
|
out = []
|
|
@@ -510,6 +562,11 @@ def md_to_html(md_text, images, cover):
|
|
|
510
562
|
continue
|
|
511
563
|
|
|
512
564
|
close_lists()
|
|
565
|
+
# 图片后的「来源:」行 → 小号灰字图注(上一个输出块是图片时)
|
|
566
|
+
if re.match(r"^来源[::]", s) and out and out[-1].startswith('<figure class="ximg"'):
|
|
567
|
+
out.append('<p class="src">%s</p>' % inline_md(s))
|
|
568
|
+
i += 1
|
|
569
|
+
continue
|
|
513
570
|
# 保留行首缩进(X 稿里用 emoji 项目符号表示二级条目时需要缩进):
|
|
514
571
|
# 行首 ≥2 个空格 / 全角空格转成 ,避免 HTML 把缩进吞掉
|
|
515
572
|
lead = len(line) - len(line.lstrip(" \u3000"))
|
|
@@ -541,14 +598,15 @@ def md_to_html(md_text, images, cover):
|
|
|
541
598
|
|
|
542
599
|
def main():
|
|
543
600
|
ap = argparse.ArgumentParser(description="Markdown 原稿 → X 长文 HTML(一键复制)")
|
|
544
|
-
ap.add_argument("md", help="原稿 markdown
|
|
545
|
-
ap.add_argument("--out",
|
|
601
|
+
ap.add_argument("md", help="原稿 markdown 路径(文章文件夹结构下就是 <文章名>.md)")
|
|
602
|
+
ap.add_argument("--out", default=None, help="输出 HTML 路径(缺省 <md 所在目录>/out/X长文.html)")
|
|
546
603
|
ap.add_argument("--images", nargs="*", default=[], help="[图N] 对应的图片路径(按顺序)")
|
|
547
604
|
ap.add_argument("--images-from", default=None, metavar="WECHAT_MD",
|
|
548
|
-
help="从指定的 markdown
|
|
549
|
-
"作为 [图N]
|
|
605
|
+
help="从指定的 markdown(如旧结构的公众号稿.md)中按文档顺序提取 ![]() 本地图片路径,"
|
|
606
|
+
"作为 [图N] 的图片来源。缺省:源稿自己有 ![]() 就直接读它;"
|
|
550
607
|
"禁止手工拼 --images 顺序(曾因此出现配图错位)")
|
|
551
|
-
ap.add_argument("--cover", default=None,
|
|
608
|
+
ap.add_argument("--cover", default=None,
|
|
609
|
+
help="封面图路径(1500×600,5:2);缺省读 frontmatter 的 cover(相对 md 所在目录)")
|
|
552
610
|
args = ap.parse_args()
|
|
553
611
|
|
|
554
612
|
if not os.path.isfile(args.md):
|
|
@@ -556,6 +614,29 @@ def main():
|
|
|
556
614
|
sys.exit(1)
|
|
557
615
|
with open(args.md, encoding="utf-8") as f:
|
|
558
616
|
md_text = f.read()
|
|
617
|
+
md_dir = os.path.dirname(os.path.abspath(args.md))
|
|
618
|
+
if not args.out:
|
|
619
|
+
args.out = os.path.join(md_dir, "out", "X长文.html")
|
|
620
|
+
if not args.cover:
|
|
621
|
+
fc = frontmatter_value(md_text, "cover")
|
|
622
|
+
if fc:
|
|
623
|
+
cp = fc if os.path.isabs(fc) else os.path.normpath(os.path.join(md_dir, fc))
|
|
624
|
+
if os.path.isfile(cp):
|
|
625
|
+
args.cover = cp
|
|
626
|
+
else:
|
|
627
|
+
print("警告:frontmatter 的 cover 文件不存在,跳过封面:%s" % fc, file=sys.stderr)
|
|
628
|
+
|
|
629
|
+
has_markers = any(re.fullmatch(r"\[图\d+\]", ln.strip()) for ln in md_text.split("\n"))
|
|
630
|
+
if not has_markers and not args.images and not args.images_from:
|
|
631
|
+
# 新结构:源稿只有 ![](),直接按文档顺序取图
|
|
632
|
+
md_text, derived = inline_images(md_text, md_dir)
|
|
633
|
+
if derived:
|
|
634
|
+
print("从源稿直接取图:%d 张" % len(derived), file=sys.stderr)
|
|
635
|
+
args.images = derived
|
|
636
|
+
elif has_markers and not args.images and not args.images_from:
|
|
637
|
+
print("源稿里有 [图N] 但没给图片来源:加 --images-from <带 ![]() 的稿子>,"
|
|
638
|
+
"或把 [图N] 换回 ", file=sys.stderr)
|
|
639
|
+
sys.exit(1)
|
|
559
640
|
|
|
560
641
|
# 图片来源:优先 --images-from(从公众号稿等源 md 按文档顺序派生),杜绝手工排序错位
|
|
561
642
|
images = list(args.images)
|