@topmindspace/tms-skills 0.3.9 → 0.5.0

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.
Files changed (168) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.en.md +12 -9
  3. package/README.md +12 -9
  4. package/bin/tms-skills.js +144 -12
  5. package/package.json +1 -1
  6. package/top-ppt-html/README.en.md +2 -2
  7. package/top-ppt-html/README.md +1 -1
  8. package/top-ppt-html/SKILL.md +8 -7
  9. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +16 -171
  10. package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +13 -3
  11. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +16 -324
  12. package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +15 -1
  13. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +16 -917
  14. package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +15 -1
  15. package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +99 -18
  16. package/top-ppt-html/assets/icons/index.json +557 -0
  17. package/top-ppt-html/assets/icons//344/270/212/344/274/240.png +0 -0
  18. package/top-ppt-html/assets/icons//344/270/212/344/274/240.svg +1 -0
  19. package/top-ppt-html/assets/icons//344/270/213/350/275/275.png +0 -0
  20. package/top-ppt-html/assets/icons//344/270/213/350/275/275.svg +1 -0
  21. package/top-ppt-html/assets/icons//344/270/213/351/231/215.png +0 -0
  22. package/top-ppt-html/assets/icons//344/270/213/351/231/215.svg +1 -0
  23. package/top-ppt-html/assets/icons//344/271/246/347/255/276.png +0 -0
  24. package/top-ppt-html/assets/icons//344/271/246/347/255/276.svg +1 -0
  25. package/top-ppt-html/assets/icons//344/272/221.png +0 -0
  26. package/top-ppt-html/assets/icons//344/272/221.svg +1 -0
  27. package/top-ppt-html/assets/icons//344/275/215/347/275/256.png +0 -0
  28. package/top-ppt-html/assets/icons//344/275/215/347/275/256.svg +1 -0
  29. package/top-ppt-html/assets/icons//344/277/241/346/201/257.png +0 -0
  30. package/top-ppt-html/assets/icons//344/277/241/346/201/257.svg +1 -0
  31. package/top-ppt-html/assets/icons//345/210/206/344/272/253.png +0 -0
  32. package/top-ppt-html/assets/icons//345/210/206/344/272/253.svg +1 -0
  33. package/top-ppt-html/assets/icons//345/213/276/351/200/211.png +0 -0
  34. package/top-ppt-html/assets/icons//345/213/276/351/200/211.svg +1 -0
  35. package/top-ppt-html/assets/icons//345/215/240/346/257/224.png +0 -0
  36. package/top-ppt-html/assets/icons//345/215/240/346/257/224.svg +1 -0
  37. package/top-ppt-html/assets/icons//345/233/242/351/230/237.png +0 -0
  38. package/top-ppt-html/assets/icons//345/233/242/351/230/237.svg +1 -0
  39. package/top-ppt-html/assets/icons//345/233/276/350/241/250.png +0 -0
  40. package/top-ppt-html/assets/icons//345/233/276/350/241/250.svg +1 -0
  41. package/top-ppt-html/assets/icons//345/242/236/351/225/277.png +0 -0
  42. package/top-ppt-html/assets/icons//345/242/236/351/225/277.svg +1 -0
  43. package/top-ppt-html/assets/icons//345/256/211/345/205/250.png +0 -0
  44. package/top-ppt-html/assets/icons//345/256/211/345/205/250.svg +1 -0
  45. package/top-ppt-html/assets/icons//345/257/271/350/257/235.png +0 -0
  46. package/top-ppt-html/assets/icons//345/257/271/350/257/235.svg +1 -0
  47. package/top-ppt-html/assets/icons//345/267/245/345/205/267.png +0 -0
  48. package/top-ppt-html/assets/icons//345/267/245/345/205/267.svg +1 -0
  49. package/top-ppt-html/assets/icons//345/276/252/347/216/257.png +0 -0
  50. package/top-ppt-html/assets/icons//345/276/252/347/216/257.svg +1 -0
  51. package/top-ppt-html/assets/icons//346/210/220/346/236/234.png +0 -0
  52. package/top-ppt-html/assets/icons//346/210/220/346/236/234.svg +1 -0
  53. package/top-ppt-html/assets/icons//346/211/213/346/234/272.png +0 -0
  54. package/top-ppt-html/assets/icons//346/211/213/346/234/272.svg +1 -0
  55. package/top-ppt-html/assets/icons//346/225/210/347/216/207.png +0 -0
  56. package/top-ppt-html/assets/icons//346/225/210/347/216/207.svg +1 -0
  57. package/top-ppt-html/assets/icons//346/225/260/346/215/256.png +0 -0
  58. package/top-ppt-html/assets/icons//346/225/260/346/215/256.svg +1 -0
  59. package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.png +0 -0
  60. package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.svg +1 -0
  61. package/top-ppt-html/assets/icons//346/226/207/346/241/243.png +0 -0
  62. package/top-ppt-html/assets/icons//346/226/207/346/241/243.svg +1 -0
  63. package/top-ppt-html/assets/icons//346/227/227/345/270/234.png +0 -0
  64. package/top-ppt-html/assets/icons//346/227/227/345/270/234.svg +1 -0
  65. package/top-ppt-html/assets/icons//346/227/266/351/227/264.png +0 -0
  66. package/top-ppt-html/assets/icons//346/227/266/351/227/264.svg +1 -0
  67. package/top-ppt-html/assets/icons//346/230/237/346/240/207.png +0 -0
  68. package/top-ppt-html/assets/icons//346/230/237/346/240/207.svg +1 -0
  69. package/top-ppt-html/assets/icons//346/231/272/350/203/275.png +0 -0
  70. package/top-ppt-html/assets/icons//346/231/272/350/203/275.svg +1 -0
  71. package/top-ppt-html/assets/icons//346/235/203/351/231/220.png +0 -0
  72. package/top-ppt-html/assets/icons//346/235/203/351/231/220.svg +1 -0
  73. package/top-ppt-html/assets/icons//346/243/200/346/237/245.png +0 -0
  74. package/top-ppt-html/assets/icons//346/243/200/346/237/245.svg +1 -0
  75. package/top-ppt-html/assets/icons//346/264/236/345/257/237.png +0 -0
  76. package/top-ppt-html/assets/icons//346/264/236/345/257/237.svg +1 -0
  77. package/top-ppt-html/assets/icons//346/265/201/347/250/213.png +0 -0
  78. package/top-ppt-html/assets/icons//346/265/201/347/250/213.svg +1 -0
  79. package/top-ppt-html/assets/icons//346/270/205/345/215/225.png +0 -0
  80. package/top-ppt-html/assets/icons//346/270/205/345/215/225.svg +1 -0
  81. package/top-ppt-html/assets/icons//347/201/253/347/256/255.png +0 -0
  82. package/top-ppt-html/assets/icons//347/201/253/347/256/255.svg +1 -0
  83. package/top-ppt-html/assets/icons//347/201/257/346/263/241.png +0 -0
  84. package/top-ppt-html/assets/icons//347/201/257/346/263/241.svg +1 -0
  85. package/top-ppt-html/assets/icons//347/224/250/346/210/267.png +0 -0
  86. package/top-ppt-html/assets/icons//347/224/250/346/210/267.svg +1 -0
  87. package/top-ppt-html/assets/icons//347/233/256/346/240/207.png +0 -0
  88. package/top-ppt-html/assets/icons//347/233/256/346/240/207.svg +1 -0
  89. package/top-ppt-html/assets/icons//347/256/255/345/244/264.png +0 -0
  90. package/top-ppt-html/assets/icons//347/256/255/345/244/264.svg +1 -0
  91. package/top-ppt-html/assets/icons//347/273/204/347/273/207.png +0 -0
  92. package/top-ppt-html/assets/icons//347/273/204/347/273/207.svg +1 -0
  93. package/top-ppt-html/assets/icons//347/275/227/347/233/230.png +0 -0
  94. package/top-ppt-html/assets/icons//347/275/227/347/233/230.svg +1 -0
  95. package/top-ppt-html/assets/icons//350/241/250/346/240/274.png +0 -0
  96. package/top-ppt-html/assets/icons//350/241/250/346/240/274.svg +1 -0
  97. package/top-ppt-html/assets/icons//350/256/241/345/210/222.png +0 -0
  98. package/top-ppt-html/assets/icons//350/256/241/345/210/222.svg +1 -0
  99. package/top-ppt-html/assets/icons//350/256/276/347/275/256.png +0 -0
  100. package/top-ppt-html/assets/icons//350/256/276/347/275/256.svg +1 -0
  101. package/top-ppt-html/assets/icons//350/266/213/345/212/277.png +0 -0
  102. package/top-ppt-html/assets/icons//350/266/213/345/212/277.svg +1 -0
  103. package/top-ppt-html/assets/icons//351/202/256/344/273/266.png +0 -0
  104. package/top-ppt-html/assets/icons//351/202/256/344/273/266.svg +1 -0
  105. package/top-ppt-html/assets/icons//351/207/221/351/222/261.png +0 -0
  106. package/top-ppt-html/assets/icons//351/207/221/351/222/261.svg +1 -0
  107. package/top-ppt-html/assets/icons//351/223/276/346/216/245.png +0 -0
  108. package/top-ppt-html/assets/icons//351/223/276/346/216/245.svg +1 -0
  109. package/top-ppt-html/assets/icons//351/227/252/347/224/265.png +0 -0
  110. package/top-ppt-html/assets/icons//351/227/252/347/224/265.svg +1 -0
  111. package/top-ppt-html/assets/icons//351/243/216/351/231/251.png +0 -0
  112. package/top-ppt-html/assets/icons//351/243/216/351/231/251.svg +1 -0
  113. package/top-ppt-html/assets/pptx-export.js +2 -2
  114. package/top-ppt-html/assets/style-gallery.html +1 -1
  115. package/top-ppt-html/assets/templates/architecture.html +15 -3
  116. package/top-ppt-html/assets/templates/engine.css +12 -0
  117. package/top-ppt-html/assets/templates/presentation.html +15 -3
  118. package/top-ppt-html/assets/templates/research.html +15 -3
  119. package/top-ppt-html/package-lock.json +2 -2
  120. package/top-ppt-html/package.json +2 -2
  121. package/top-ppt-html/references/icons.md +22 -19
  122. package/top-ppt-html/references/layout-variants.md +69 -0
  123. package/top-ppt-html/references/style-pack.md +40 -0
  124. package/top-ppt-html/scripts/build_pptx.js +93 -26
  125. package/top-ppt-html/scripts/check_icons.py +119 -0
  126. package/top-ppt-html/scripts/hydrate_charts.py +60 -5
  127. package/top-ppt-html/scripts/icon_lib.js +66 -37
  128. package/top-ppt-html/scripts/layout-constants.json +91 -4
  129. package/top-ppt-html/scripts/make_icon_pack.py +172 -0
  130. package/top-ppt-html/scripts/model-schema.json +4 -1
  131. package/top-ppt-html/scripts/render_from_model.py +116 -55
  132. package/top-ppt-html/scripts/validate_pptx.py +1 -0
  133. package/top-ppt-html/scripts/validate_report.py +153 -0
  134. package/topmind-cover/README.en.md +4 -4
  135. package/topmind-cover/README.md +4 -4
  136. package/topmind-cover/SKILL.md +36 -18
  137. package/topmind-cover/assets/examples/README.md +13 -7
  138. package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
  139. package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
  140. package/topmind-cover/assets/examples/bg-blur.png +0 -0
  141. package/topmind-cover/assets/examples/overview.png +0 -0
  142. package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
  143. package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
  144. package/topmind-cover/assets/examples/paper-collage.png +0 -0
  145. package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
  146. package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
  147. package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
  148. package/topmind-cover/assets/examples/white-clean.png +0 -0
  149. package/topmind-cover/package.json +2 -2
  150. package/topmind-cover/references/cover-prompts.md +436 -0
  151. package/topmind-cover/references/cover-styles.md +149 -14
  152. package/topmind-cover/references/impact-language.md +9 -6
  153. package/topmind-cover/references/ref-white-clean-1.png +0 -0
  154. package/topmind-cover/references/ref-white-clean-2.png +0 -0
  155. package/topmind-wechat-post/README.en.md +1 -1
  156. package/topmind-wechat-post/README.md +1 -1
  157. package/topmind-wechat-post/references/wechat-constraints.md +5 -0
  158. package/topmind-wechat-post/scripts/md2wechat.py +130 -27
  159. package/topmind-wechat-post/scripts/negative_tests.py +74 -0
  160. package/topmind-x-article/README.en.md +25 -5
  161. package/topmind-x-article/README.md +21 -8
  162. package/topmind-x-article/SKILL.md +37 -13
  163. package/topmind-x-article/package.json +1 -1
  164. package/topmind-x-article/references/publish-checklist.md +9 -2
  165. package/topmind-x-article/references/x-html-format.md +73 -0
  166. package/topmind-x-article/scripts/md2x-html.py +394 -0
  167. package/topmind-x-article/scripts/md2x.py +18 -3
  168. package/topmind-x-article/scripts/negative_tests.py +17 -0
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env python3
2
+ """md2x-html.py — Markdown 原稿 → 可直接复制的 X 长文 HTML(单文件)。
3
+
4
+ 用法:
5
+ python3 md2x-html.py 原稿.md --out X长文.html \\
6
+ --images 样张1.png 样张2.png ... [--cover cover-1200x675.png]
7
+
8
+ 约定(与 md2x.py 对齐):
9
+ - `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure>
10
+ - ``` 围栏代码块(直出提示词)→ <blockquote> + 「复制提示词」按钮。
11
+ 原因:X 文章编辑器没有"粘贴 <pre> 即代码块"的识别,原生代码块只能走
12
+ Insert 菜单;<blockquote> 粘贴后即 X 原生引用块,格式不丢、读者全选即拷
13
+ - `#` → h1(X 标题栏专用,复制全文时自动剔除);`##`/`###` → h2/h3
14
+ (对应 X 的"标题/副标题"两级)
15
+ - `- ` / `1. ` → ul/ol;`**x**` → strong;`` `x` `` → code(X 粘贴后变纯文本);
16
+ `---` → hr(X 粘贴可能丢弃,需手动 Insert → 分割线,见 references/x-html-format.md)
17
+ - GFM 表格 → ul.xtable(X 粘贴会丢弃 <table>,转列表保内容)
18
+ - frontmatter(--- ... ---)跳过,只取 title
19
+
20
+ 设计目标(对标 md2wechat 的公众号体验):
21
+ - 文本+格式:页顶「一键复制全文」按钮 → 剪贴板富文本 → 粘贴进 X 文章编辑器。
22
+ 复制前自动剔除按钮/UI 与 .no-paste 块(标题、封面预览),只剩 X 可识别的
23
+ 语义标签:h2/h3/p/ul/ol/blockquote/a/hr/img
24
+ - 图片:data-URI 内嵌(X 若支持则随粘贴带入);每张图带 [图N] 编号 + 「下载图片」
25
+ 按钮保底,图片没跟过去时按编号下载再上传,不用找文件、不用对顺序
26
+ - 封面:X 有独立封面上传入口,始终单独文件;HTML 顶部仅作预览 + 下载链接
27
+
28
+ 只用 Python 标准库。
29
+ """
30
+
31
+ import argparse
32
+ import base64
33
+ import html
34
+ import os
35
+ import re
36
+ import sys
37
+
38
+ CSS = """
39
+ :root { color-scheme: light; }
40
+ * { box-sizing: border-box; }
41
+ body { margin: 0; background: #f7f9fa; color: #0f1419;
42
+ font-family: -apple-system, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; }
43
+ .toolbar { position: sticky; top: 0; z-index: 50; background: rgba(255,255,255,.96);
44
+ border-bottom: 1px solid #eff3f4; padding: 10px 16px;
45
+ display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
46
+ .toolbar .hint { font-size: 13px; color: #536471; }
47
+ .toolbar .steps { font-size: 13px; color: #0f1419; font-weight: 700; }
48
+ .btn { appearance: none; border: 1px solid #cfd9de; background: #fff; color: #0f1419;
49
+ border-radius: 9999px; padding: 8px 18px; font-size: 14px; font-weight: 700;
50
+ cursor: pointer; }
51
+ .btn.primary { background: #0f1419; color: #fff; border-color: #0f1419; }
52
+ .btn:active { transform: scale(.97); }
53
+ .article { max-width: 680px; margin: 0 auto; background: #fff;
54
+ padding: 32px 28px 64px; }
55
+ .article h1 { font-size: 26px; line-height: 1.4; margin: 0 0 20px; }
56
+ .article h2 { font-size: 20px; margin: 34px 0 12px; padding-top: 6px; }
57
+ .article h3 { font-size: 18px; margin: 26px 0 10px; }
58
+ .article p { font-size: 17px; line-height: 1.85; margin: 0 0 14px; word-break: break-word; }
59
+ .article ul, .article ol { font-size: 17px; line-height: 1.85; margin: 0 0 14px; padding-left: 1.4em; }
60
+ .article hr { border: none; border-top: 1px solid #eff3f4; margin: 28px 0; }
61
+ .article a { color: #1d9bf0; }
62
+ .article code { background: #f1f3f4; border-radius: 4px; padding: 1px 6px;
63
+ font-size: .92em; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
64
+ .article ul.xtable { list-style: none; padding-left: 0; }
65
+ .article ul.xtable li { background: #f7f9fa; border: 1px solid #eff3f4;
66
+ border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; }
67
+ .cover-note { background: #f7f9fa; border: 1px dashed #cfd9de; border-radius: 12px;
68
+ padding: 14px 16px; font-size: 14px; color: #536471; margin-bottom: 24px; }
69
+ .cover-note img { width: 100%; border-radius: 8px; display: block; margin-bottom: 10px; }
70
+ figure.ximg { margin: 22px 0; }
71
+ figure.ximg img { width: 100%; border-radius: 10px; display: block;
72
+ border: 1px solid #eff3f4; }
73
+ figure.ximg figcaption { display: flex; justify-content: space-between; align-items: center;
74
+ margin-top: 8px; font-size: 13px; color: #536471; }
75
+ .prompt { position: relative; margin: 14px 0 20px; }
76
+ .prompt blockquote { margin: 0; background: #f7f9fa; border-left: 4px solid #1d9bf0;
77
+ border-radius: 0 10px 10px 0; padding: 16px 18px;
78
+ font-size: 14px; line-height: 1.8; }
79
+ .prompt blockquote code { background: none; padding: 0; font-size: 1em;
80
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
81
+ .prompt .btn { position: absolute; top: 8px; right: 8px; padding: 5px 12px;
82
+ font-size: 12px; }
83
+ .toast { position: fixed; left: 50%; bottom: 40px; transform: translateX(-50%);
84
+ background: #0f1419; color: #fff; font-size: 14px; padding: 10px 20px;
85
+ border-radius: 9999px; opacity: 0; transition: opacity .25s; pointer-events: none; }
86
+ """
87
+
88
+ JS = """
89
+ function toast(msg){ var t=document.getElementById('toast'); t.textContent=msg;
90
+ t.style.opacity=1; setTimeout(function(){ t.style.opacity=0; }, 1800); }
91
+ function cleanClone(){
92
+ var el=document.getElementById('article');
93
+ var c=el.cloneNode(true);
94
+ var kill=c.querySelectorAll('button, .no-paste');
95
+ for(var i=0;i<kill.length;i++){ kill[i].parentNode.removeChild(kill[i]); }
96
+ return c;
97
+ }
98
+ async function copyArticle(){
99
+ var c=cleanClone();
100
+ var htm='<meta charset="utf-8">'+c.innerHTML;
101
+ try{
102
+ var item=new ClipboardItem({
103
+ 'text/html': new Blob([htm],{type:'text/html'}),
104
+ 'text/plain': new Blob([c.innerText],{type:'text/plain'})
105
+ });
106
+ await navigator.clipboard.write([item]);
107
+ toast('已复制全文(含格式),去 X 粘贴吧');
108
+ }catch(e){
109
+ try{ await navigator.clipboard.writeText(c.innerText); toast('已复制纯文本(富文本复制失败)'); }
110
+ catch(e2){ toast('复制失败,请手动全选复制'); }
111
+ }
112
+ }
113
+ async function copyPrompt(btn){
114
+ var code=btn.parentElement.querySelector('code').innerText;
115
+ try{ await navigator.clipboard.writeText(code); toast('提示词已复制,去 AI 生图工具粘贴'); }
116
+ catch(e){ toast('复制失败,请手动复制'); }
117
+ }
118
+ function dlImg(btn){
119
+ var img=btn.closest('figure').querySelector('img');
120
+ var a=document.createElement('a');
121
+ a.href=img.src; a.download=btn.getAttribute('data-fname');
122
+ document.body.appendChild(a); a.click(); a.remove();
123
+ toast('开始下载 '+btn.getAttribute('data-fname'));
124
+ }
125
+ function dlCover(btn){
126
+ var img=document.querySelector('#coverbox img');
127
+ var a=document.createElement('a');
128
+ a.href=img.src; a.download=btn.getAttribute('data-fname');
129
+ document.body.appendChild(a); a.click(); a.remove();
130
+ toast('封面开始下载');
131
+ }
132
+ """
133
+
134
+ HTML_TMPL = """<!DOCTYPE html>
135
+ <html lang="zh-CN">
136
+ <head>
137
+ <meta charset="utf-8">
138
+ <meta name="viewport" content="width=device-width, initial-scale=1">
139
+ <title>{title}</title>
140
+ <style>{css}</style>
141
+ </head>
142
+ <body>
143
+ <div class="toolbar">
144
+ <span class="steps">1. 标题填入 X 标题栏 → 2. 一键复制全文 → 3. 粘贴到 X 正文</span>
145
+ <button class="btn primary" onclick="copyArticle()">一键复制全文</button>
146
+ <span class="hint">图片随粘贴带入则直接用;没带入就按 [图N] 下载上传。封面始终单独上传。</span>
147
+ </div>
148
+ <article class="article" id="article">
149
+ {body}
150
+ </article>
151
+ <div class="toast" id="toast"></div>
152
+ <script>{js}</script>
153
+ </body>
154
+ </html>
155
+ """
156
+
157
+
158
+ def data_uri(path):
159
+ with open(path, "rb") as f:
160
+ raw = f.read()
161
+ ext = os.path.splitext(path)[1].lower()
162
+ mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
163
+ "webp": "image/webp", "gif": "image/gif"}.get(ext.lstrip("."), "image/png")
164
+ return "data:%s;base64,%s" % (mime, base64.b64encode(raw).decode("ascii"))
165
+
166
+
167
+ def inline_md(s):
168
+ """行内 markdown → html:加粗、行内代码、裸链接。"""
169
+ s = html.escape(s)
170
+ s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
171
+ s = re.sub(r"`([^`]+?)`", r"<code>\1</code>", s)
172
+ s = re.sub(r"(https?://[^\s<>()\uff08\uff09\u3010\u3011\u300c\u300d\u300e\u300f]+)",
173
+ r'<a href="\1">\1</a>', s)
174
+ return s
175
+
176
+
177
+ def md_to_html(md_text, images, cover):
178
+ lines = md_text.split("\n")
179
+ out = []
180
+ img_idx = 0
181
+ cover_uri = data_uri(cover) if cover else None
182
+
183
+ i = 0
184
+ if lines and lines[0].strip() == "---":
185
+ i = 1
186
+ while i < len(lines) and lines[i].strip() != "---":
187
+ i += 1
188
+ i += 1
189
+
190
+ in_code = False
191
+ code_buf = []
192
+ in_ul = False
193
+ in_ol = False
194
+ in_table = False
195
+ table_buf = []
196
+
197
+ def close_lists():
198
+ nonlocal in_ul, in_ol
199
+ if in_ul:
200
+ out.append("</ul>")
201
+ in_ul = False
202
+ if in_ol:
203
+ out.append("</ol>")
204
+ in_ol = False
205
+
206
+ def flush_table():
207
+ nonlocal in_table, table_buf
208
+ if not in_table:
209
+ return
210
+ rows = []
211
+ for r in table_buf:
212
+ cells = [c.strip() for c in r.strip().strip("|").split("|")]
213
+ rows.append(cells)
214
+ rows = [r for r in rows if not all(re.fullmatch(r":?-+:?", c or "-") for c in r)]
215
+ if rows:
216
+ out.append('<ul class="xtable">')
217
+ for j, r in enumerate(rows):
218
+ cells = " | ".join(inline_md(c) for c in r)
219
+ if j == 0:
220
+ out.append("<li><strong>%s</strong></li>" % cells)
221
+ else:
222
+ out.append("<li>%s</li>" % cells)
223
+ out.append("</ul>")
224
+ in_table = False
225
+ table_buf = []
226
+
227
+ while i < len(lines):
228
+ line = lines[i]
229
+ s = line.strip()
230
+
231
+ if s.startswith("```"):
232
+ if not in_code:
233
+ close_lists()
234
+ flush_table()
235
+ in_code = True
236
+ code_buf = []
237
+ else:
238
+ in_code = False
239
+ body = "\n".join(code_buf)
240
+ esc = html.escape(body).replace("\n", "<br>")
241
+ out.append(
242
+ '<div class="prompt"><blockquote><code>%s</code></blockquote>'
243
+ '<button class="btn" onclick="copyPrompt(this)">复制提示词</button>'
244
+ "</div>" % esc
245
+ )
246
+ i += 1
247
+ continue
248
+ if in_code:
249
+ code_buf.append(line)
250
+ i += 1
251
+ continue
252
+
253
+ if re.match(r"^\|.*\|\s*$", s) and "|" in s.strip("|"):
254
+ if not in_table:
255
+ close_lists()
256
+ in_table = True
257
+ table_buf = []
258
+ table_buf.append(s)
259
+ i += 1
260
+ continue
261
+ else:
262
+ flush_table()
263
+
264
+ m = re.fullmatch(r"\[图(\d+)\]", s)
265
+ if m:
266
+ close_lists()
267
+ n = int(m.group(1))
268
+ if 1 <= n <= len(images):
269
+ uri = data_uri(images[n - 1])
270
+ fname = "图%d-%s" % (n, os.path.basename(images[n - 1]))
271
+ out.append(
272
+ '<figure class="ximg"><img src="%s" alt="图%d">'
273
+ '<figcaption><span class="cap">[图%d]</span>'
274
+ '<button class="btn" data-fname="%s" '
275
+ 'onclick="dlImg(this)">下载图片</button></figcaption></figure>'
276
+ % (uri, n, n, html.escape(fname, quote=True))
277
+ )
278
+ else:
279
+ out.append("<p>[图%d](图片缺失)</p>" % n)
280
+ img_idx = max(img_idx, n)
281
+ i += 1
282
+ continue
283
+
284
+ if s.startswith("### "):
285
+ close_lists()
286
+ out.append("<h3>%s</h3>" % inline_md(s[4:]))
287
+ i += 1
288
+ continue
289
+ if s.startswith("## "):
290
+ close_lists()
291
+ out.append("<h2>%s</h2>" % inline_md(s[3:]))
292
+ i += 1
293
+ continue
294
+ if s.startswith("# "):
295
+ close_lists()
296
+ out.append('<h1 class="no-paste">%s</h1>' % inline_md(s[2:]))
297
+ i += 1
298
+ continue
299
+
300
+ if s in ("---", "***"):
301
+ close_lists()
302
+ out.append("<hr>")
303
+ i += 1
304
+ continue
305
+
306
+ m = re.match(r"^(\d+)\.\s+(.*)$", s)
307
+ if m:
308
+ if in_ul:
309
+ out.append("</ul>")
310
+ in_ul = False
311
+ if not in_ol:
312
+ out.append("<ol>")
313
+ in_ol = True
314
+ out.append("<li>%s</li>" % inline_md(m.group(2)))
315
+ i += 1
316
+ continue
317
+
318
+ if s.startswith("- "):
319
+ if in_ol:
320
+ out.append("</ol>")
321
+ in_ol = False
322
+ if not in_ul:
323
+ out.append("<ul>")
324
+ in_ul = True
325
+ out.append("<li>%s</li>" % inline_md(s[2:]))
326
+ i += 1
327
+ continue
328
+
329
+ if not s:
330
+ close_lists()
331
+ i += 1
332
+ continue
333
+
334
+ close_lists()
335
+ out.append("<p>%s</p>" % inline_md(s))
336
+ i += 1
337
+
338
+ close_lists()
339
+ flush_table()
340
+
341
+ body = "\n".join(out)
342
+
343
+ cover_html = ""
344
+ if cover_uri:
345
+ cover_html = (
346
+ '<div class="cover-note no-paste" id="coverbox">'
347
+ '<img src="%s" alt="封面预览">'
348
+ '<div>封面图预览(1200×675)。X 文章有独立封面上传入口,'
349
+ '请单独上传,不要随正文粘贴。<br>'
350
+ '<button class="btn" data-fname="cover-1200x675.png" '
351
+ 'onclick="dlCover(this)">下载封面图</button></div></div>'
352
+ ) % cover_uri
353
+ return cover_html + "\n" + body
354
+
355
+
356
+ def main():
357
+ ap = argparse.ArgumentParser(description="Markdown 原稿 → X 长文 HTML(一键复制)")
358
+ ap.add_argument("md", help="原稿 markdown 路径")
359
+ ap.add_argument("--out", required=True, help="输出 HTML 路径")
360
+ ap.add_argument("--images", nargs="*", default=[], help="[图N] 对应的图片路径(按顺序)")
361
+ ap.add_argument("--cover", default=None, help="封面图路径(1200×675)")
362
+ args = ap.parse_args()
363
+
364
+ if not os.path.isfile(args.md):
365
+ print("输入文件不存在:%s" % args.md, file=sys.stderr)
366
+ sys.exit(1)
367
+
368
+ with open(args.md, encoding="utf-8") as f:
369
+ md_text = f.read()
370
+
371
+ m = re.search(r"^#\s+(.+)$", md_text, re.M)
372
+ title = m.group(1).strip() if m else "X长文"
373
+
374
+ body = md_to_html(md_text, args.images, args.cover)
375
+ page = HTML_TMPL.replace("{title}", html.escape(title), 1) \
376
+ .replace("{css}", CSS, 1) \
377
+ .replace("{body}", body, 1) \
378
+ .replace("{js}", JS, 1)
379
+
380
+ out_dir = os.path.dirname(os.path.abspath(args.out))
381
+ os.makedirs(out_dir, exist_ok=True)
382
+ tmp = args.out + ".tmp"
383
+ try:
384
+ with open(tmp, "w", encoding="utf-8") as f:
385
+ f.write(page)
386
+ os.replace(tmp, args.out)
387
+ except OSError as e:
388
+ print("写入失败:%s" % e, file=sys.stderr)
389
+ sys.exit(1)
390
+ print("已生成:%s" % args.out)
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()
@@ -193,6 +193,7 @@ def convert(md: str):
193
193
  defs = {}
194
194
  out_lines = []
195
195
  in_code = False
196
+ fence_len = 0
196
197
 
197
198
  md, esc_store = _protect_escapes(md)
198
199
  md = _strip_frontmatter(md)
@@ -209,9 +210,23 @@ def convert(md: str):
209
210
  for raw in body:
210
211
  line = raw.rstrip()
211
212
 
212
- # 代码块:先处理围栏,块内行原样缩进(分割线规则不进代码块)
213
- if line.strip().startswith("```"):
214
- in_code = not in_code
213
+ # 代码块:先处理围栏,块内行原样缩进(分割线规则不进代码块)。
214
+ # 围栏反引号数 >= 3:```` 开栏只能由 >= 4 反引号闭合(CommonMark),
215
+ # 否则 markdown 示例里的内层 ``` 会把外层栏提前"闭合",造成静默错排。
216
+ stripped = line.strip()
217
+ if stripped.startswith("```"):
218
+ k = 0
219
+ while k < len(stripped) and stripped[k] == "`":
220
+ k += 1
221
+ if not in_code:
222
+ in_code = True
223
+ fence_len = k
224
+ elif k >= fence_len and not stripped[k:].strip():
225
+ in_code = False
226
+ fence_len = 0
227
+ else:
228
+ # 栏内误写的短栏 / 带信息串的栏是代码内容,不是闭合栏
229
+ out_lines.append(" " + line)
215
230
  continue
216
231
  if in_code:
217
232
  out_lines.append(" " + line)
@@ -64,6 +64,23 @@ def main() -> None:
64
64
  out = (td / "o6.txt").read_text(encoding="utf-8") if (td / "o6.txt").exists() else ""
65
65
  check("正常输入含配图清单", r.returncode == 0 and "[图1]" in out)
66
66
 
67
+ # 7. 嵌套围栏:```` 开栏不被内层 ``` 提前闭合(内层栏是代码内容)
68
+ (td / "nest.md").write_text(
69
+ "# t\n\n````markdown\n外层\n```python\nprint(1)\n```\n结束\n````\n",
70
+ encoding="utf-8")
71
+ r = run([str(td / "nest.md"), "--out", str(td / "o7.txt")])
72
+ out = (td / "o7.txt").read_text(encoding="utf-8") if (td / "o7.txt").exists() else ""
73
+ check("嵌套围栏不提前闭合",
74
+ r.returncode == 0 and " ```python" in out and "print(1)" in out)
75
+
76
+ # 8. 括号 URL 链接不断裂(与 md2wechat 对齐的守卫)
77
+ (td / "paren.md").write_text(
78
+ "# t\n\n[维基](https://zh.wikipedia.org/wiki/猫_(动物))\n", encoding="utf-8")
79
+ r = run([str(td / "paren.md"), "--out", str(td / "o8.txt")])
80
+ out = (td / "o8.txt").read_text(encoding="utf-8") if (td / "o8.txt").exists() else ""
81
+ check("括号 URL 不截断",
82
+ r.returncode == 0 and "猫_(动物)" in out)
83
+
67
84
  if fails:
68
85
  print(f"\n[negative_tests] {len(fails)} 项失败")
69
86
  sys.exit(1)