@topmindspace/tms-skills 0.5.0 → 0.7.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.
- package/CHANGELOG.md +64 -0
- package/README.en.md +6 -5
- package/README.md +14 -5
- package/package.json +2 -1
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/topmind-briefs/CHANGELOG.md +22 -0
- package/topmind-briefs/README.en.md +40 -0
- package/topmind-briefs/README.md +37 -0
- package/topmind-briefs/SKILL.md +101 -0
- package/topmind-briefs/dist/topmind-briefs.manifest.json +21 -0
- package/topmind-briefs/dist/topmind-briefs.zip +0 -0
- package/topmind-briefs/package-lock.json +13 -0
- package/topmind-briefs/package.json +1 -0
- package/topmind-briefs/references/examples.md +127 -0
- package/topmind-briefs/references/platform-specs.md +60 -0
- package/topmind-briefs/references/style-guide.md +55 -0
- package/topmind-briefs/scripts/audit_css.py +39 -0
- package/topmind-briefs/scripts/audit_docs.py +80 -0
- package/topmind-briefs/scripts/audit_skill.py +60 -0
- package/topmind-briefs/scripts/audit_styles.py +54 -0
- package/topmind-briefs/scripts/negative_tests.py +88 -0
- package/topmind-briefs/scripts/package_skill.py +188 -0
- package/topmind-cover/README.en.md +4 -4
- package/topmind-cover/README.md +4 -4
- package/topmind-cover/SKILL.md +5 -5
- package/topmind-cover/assets/examples/README.md +2 -0
- package/topmind-cover/package.json +1 -1
- package/topmind-cover/references/cover-prompts.md +19 -19
- package/topmind-cover/references/cover-styles.md +27 -26
- package/topmind-cover/scripts/crop-cover.py +6 -6
- package/topmind-cover/scripts/negative_tests.py +1 -1
- package/topmind-wechat-post/README.en.md +1 -1
- package/topmind-wechat-post/README.md +1 -1
- package/topmind-wechat-post/SKILL.md +74 -38
- package/topmind-wechat-post/package.json +1 -1
- package/topmind-wechat-post/references/element-spec.md +3 -2
- package/topmind-wechat-post/scripts/md2wechat.py +2 -7
- package/topmind-x-article/README.en.md +6 -2
- package/topmind-x-article/README.md +4 -2
- package/topmind-x-article/SKILL.md +59 -4
- package/topmind-x-article/package.json +1 -1
- package/topmind-x-article/references/publish-checklist.md +1 -1
- package/topmind-x-article/references/x-html-format.md +13 -1
- package/topmind-x-article/scripts/md2x-html.py +179 -51
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
用法:
|
|
5
5
|
python3 md2x-html.py 原稿.md --out X长文.html \\
|
|
6
|
-
--images 样张1.png 样张2.png ... [--cover cover-
|
|
6
|
+
--images 样张1.png 样张2.png ... [--cover cover-1500x600.png]
|
|
7
7
|
|
|
8
8
|
约定(与 md2x.py 对齐):
|
|
9
|
-
- `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure
|
|
9
|
+
- `[图N]` 独占一行 → 按顺序取 --images[N-1],转成内嵌 data-URI 的 <figure>;
|
|
10
|
+
点击图片弹出 lightbox 放大查看原图(右键可另存/截图,不拦截右键菜单)
|
|
10
11
|
- ``` 围栏代码块(直出提示词)→ <blockquote> + 「复制提示词」按钮。
|
|
11
12
|
原因:X 文章编辑器没有"粘贴 <pre> 即代码块"的识别,原生代码块只能走
|
|
12
|
-
Insert 菜单;<blockquote> 粘贴后即 X
|
|
13
|
+
Insert 菜单;<blockquote> 粘贴后即 X 原生引用块,格式不丢、读者全选即拷。
|
|
14
|
+
支持嵌套围栏(```` 开栏不被内层 ``` 提前闭合);未闭合围栏在文末自动收尾
|
|
13
15
|
- `#` → h1(X 标题栏专用,复制全文时自动剔除);`##`/`###` → h2/h3
|
|
14
16
|
(对应 X 的"标题/副标题"两级)
|
|
15
17
|
- `- ` / `1. ` → ul/ol;`**x**` → strong;`` `x` `` → code(X 粘贴后变纯文本);
|
|
@@ -22,7 +24,8 @@
|
|
|
22
24
|
复制前自动剔除按钮/UI 与 .no-paste 块(标题、封面预览),只剩 X 可识别的
|
|
23
25
|
语义标签:h2/h3/p/ul/ol/blockquote/a/hr/img
|
|
24
26
|
- 图片:data-URI 内嵌(X 若支持则随粘贴带入);每张图带 [图N] 编号 + 「下载图片」
|
|
25
|
-
|
|
27
|
+
按钮保底,图片没跟过去时按编号下载再上传,不用找文件、不用对顺序;
|
|
28
|
+
点击图片 lightbox 放大看原图,方便核对、截图或右键另存
|
|
26
29
|
- 封面:X 有独立封面上传入口,始终单独文件;HTML 顶部仅作预览 + 下载链接
|
|
27
30
|
|
|
28
31
|
只用 Python 标准库。
|
|
@@ -66,10 +69,11 @@ body { margin: 0; background: #f7f9fa; color: #0f1419;
|
|
|
66
69
|
border-radius: 8px; padding: 8px 12px; margin-bottom: 8px; }
|
|
67
70
|
.cover-note { background: #f7f9fa; border: 1px dashed #cfd9de; border-radius: 12px;
|
|
68
71
|
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;
|
|
72
|
+
.cover-note img { width: 100%; border-radius: 8px; display: block; margin-bottom: 10px;
|
|
73
|
+
cursor: zoom-in; }
|
|
70
74
|
figure.ximg { margin: 22px 0; }
|
|
71
75
|
figure.ximg img { width: 100%; border-radius: 10px; display: block;
|
|
72
|
-
border: 1px solid #eff3f4; }
|
|
76
|
+
border: 1px solid #eff3f4; cursor: zoom-in; }
|
|
73
77
|
figure.ximg figcaption { display: flex; justify-content: space-between; align-items: center;
|
|
74
78
|
margin-top: 8px; font-size: 13px; color: #536471; }
|
|
75
79
|
.prompt { position: relative; margin: 14px 0 20px; }
|
|
@@ -82,10 +86,22 @@ figure.ximg figcaption { display: flex; justify-content: space-between; align-it
|
|
|
82
86
|
font-size: 12px; }
|
|
83
87
|
.toast { position: fixed; left: 50%; bottom: 40px; transform: translateX(-50%);
|
|
84
88
|
background: #0f1419; color: #fff; font-size: 14px; padding: 10px 20px;
|
|
85
|
-
border-radius: 9999px; opacity: 0; transition: opacity .25s; pointer-events: none;
|
|
89
|
+
border-radius: 9999px; opacity: 0; transition: opacity .25s; pointer-events: none;
|
|
90
|
+
z-index: 200; }
|
|
91
|
+
/* 图片 lightbox:点击放大看原图;不拦截右键,可右键另存/截图 */
|
|
92
|
+
.lightbox { position: fixed; inset: 0; z-index: 100; background: rgba(10,14,20,.93);
|
|
93
|
+
display: none; align-items: center; justify-content: center; padding: 24px; }
|
|
94
|
+
.lightbox.open { display: flex; }
|
|
95
|
+
.lb-inner { max-width: 96vw; max-height: 94vh; display: flex; flex-direction: column;
|
|
96
|
+
gap: 10px; align-items: center; }
|
|
97
|
+
.lb-inner img { max-width: 94vw; max-height: 76vh; border-radius: 8px; background: #fff; }
|
|
98
|
+
.lb-bar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
|
|
99
|
+
.lb-cap { color: #cfd9de; font-size: 13px; }
|
|
100
|
+
.lb-tip { color: #8a93a6; font-size: 12px; }
|
|
86
101
|
"""
|
|
87
102
|
|
|
88
103
|
JS = """
|
|
104
|
+
var lbFname = '';
|
|
89
105
|
function toast(msg){ var t=document.getElementById('toast'); t.textContent=msg;
|
|
90
106
|
t.style.opacity=1; setTimeout(function(){ t.style.opacity=0; }, 1800); }
|
|
91
107
|
function cleanClone(){
|
|
@@ -116,19 +132,48 @@ async function copyPrompt(btn){
|
|
|
116
132
|
catch(e){ toast('复制失败,请手动复制'); }
|
|
117
133
|
}
|
|
118
134
|
function dlImg(btn){
|
|
119
|
-
var
|
|
135
|
+
var fig=btn.closest('figure');
|
|
136
|
+
var img=fig ? fig.querySelector('img') : document.querySelector('#coverbox img');
|
|
137
|
+
if(!img){ toast('找不到图片'); return; }
|
|
120
138
|
var a=document.createElement('a');
|
|
121
|
-
a.href=img.src; a.download=btn.getAttribute('data-fname');
|
|
139
|
+
a.href=img.src; a.download=btn.getAttribute('data-fname') || 'image.png';
|
|
122
140
|
document.body.appendChild(a); a.click(); a.remove();
|
|
123
|
-
toast('开始下载 '+btn.getAttribute('data-fname'));
|
|
141
|
+
toast('开始下载 '+(btn.getAttribute('data-fname')||'图片'));
|
|
124
142
|
}
|
|
125
|
-
|
|
126
|
-
|
|
143
|
+
/* lightbox:点击图片放大看原图。刻意不拦截右键,放大后可右键另存 / 直接截图。 */
|
|
144
|
+
function openLightbox(img){
|
|
145
|
+
var lb=document.getElementById('lightbox');
|
|
146
|
+
document.getElementById('lbimg').src=img.src;
|
|
147
|
+
document.getElementById('lbimg').alt=img.alt||'原图';
|
|
148
|
+
lbFname=img.getAttribute('data-fname')||'image.png';
|
|
149
|
+
document.getElementById('lbcap').textContent=(img.alt||'原图')+' · '+lbFname;
|
|
150
|
+
lb.classList.add('open');
|
|
151
|
+
document.body.style.overflow='hidden';
|
|
152
|
+
}
|
|
153
|
+
function closeLightbox(){
|
|
154
|
+
document.getElementById('lightbox').classList.remove('open');
|
|
155
|
+
document.body.style.overflow='';
|
|
156
|
+
}
|
|
157
|
+
function openFullsize(){
|
|
158
|
+
var src=document.getElementById('lbimg').src;
|
|
159
|
+
var parts=src.split(','), mime='image/png';
|
|
160
|
+
var m=parts[0].match(/:(.*?);/); if(m){ mime=m[1]; }
|
|
161
|
+
try{
|
|
162
|
+
var bin=atob(parts[1]), arr=new Uint8Array(bin.length);
|
|
163
|
+
for(var i=0;i<bin.length;i++){ arr[i]=bin.charCodeAt(i); }
|
|
164
|
+
var url=URL.createObjectURL(new Blob([arr],{type:mime}));
|
|
165
|
+
window.open(url,'_blank');
|
|
166
|
+
}catch(e){ toast('打开失败,可右键图片另存'); }
|
|
167
|
+
}
|
|
168
|
+
function dlLightbox(){
|
|
127
169
|
var a=document.createElement('a');
|
|
128
|
-
a.href=
|
|
170
|
+
a.href=document.getElementById('lbimg').src; a.download=lbFname;
|
|
129
171
|
document.body.appendChild(a); a.click(); a.remove();
|
|
130
|
-
toast('
|
|
172
|
+
toast('开始下载 '+lbFname);
|
|
131
173
|
}
|
|
174
|
+
document.addEventListener('keydown', function(e){
|
|
175
|
+
if(e.key==='Escape'){ closeLightbox(); }
|
|
176
|
+
});
|
|
132
177
|
"""
|
|
133
178
|
|
|
134
179
|
HTML_TMPL = """<!DOCTYPE html>
|
|
@@ -136,28 +181,46 @@ HTML_TMPL = """<!DOCTYPE html>
|
|
|
136
181
|
<head>
|
|
137
182
|
<meta charset="utf-8">
|
|
138
183
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
139
|
-
<title
|
|
140
|
-
<style
|
|
184
|
+
<title>@@TITLE@@</title>
|
|
185
|
+
<style>@@CSS@@</style>
|
|
141
186
|
</head>
|
|
142
187
|
<body>
|
|
143
188
|
<div class="toolbar">
|
|
144
189
|
<span class="steps">1. 标题填入 X 标题栏 → 2. 一键复制全文 → 3. 粘贴到 X 正文</span>
|
|
145
190
|
<button class="btn primary" onclick="copyArticle()">一键复制全文</button>
|
|
146
|
-
<span class="hint">图片随粘贴带入则直接用;没带入就按 [图N]
|
|
191
|
+
<span class="hint">图片随粘贴带入则直接用;没带入就按 [图N] 下载上传。封面始终单独上传。点击图片可放大查看原图。</span>
|
|
147
192
|
</div>
|
|
148
193
|
<article class="article" id="article">
|
|
149
|
-
|
|
194
|
+
@@BODY@@
|
|
150
195
|
</article>
|
|
196
|
+
<div class="lightbox" id="lightbox" onclick="if(event.target===this)closeLightbox()">
|
|
197
|
+
<div class="lb-inner">
|
|
198
|
+
<img id="lbimg" alt="原图">
|
|
199
|
+
<div class="lb-bar">
|
|
200
|
+
<span class="lb-cap" id="lbcap"></span>
|
|
201
|
+
<button class="btn" onclick="openFullsize()">新标签页打开原图</button>
|
|
202
|
+
<button class="btn" onclick="dlLightbox()">下载原图</button>
|
|
203
|
+
<button class="btn" onclick="closeLightbox()">关闭</button>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="lb-tip">可右键图片另存 / 直接截图 · Esc 关闭</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
151
208
|
<div class="toast" id="toast"></div>
|
|
152
|
-
<script
|
|
209
|
+
<script>@@JS@@</script>
|
|
153
210
|
</body>
|
|
154
211
|
</html>
|
|
155
212
|
"""
|
|
156
213
|
|
|
214
|
+
COVER_FNAME = "cover-1500x600.png"
|
|
215
|
+
|
|
157
216
|
|
|
158
217
|
def data_uri(path):
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
try:
|
|
219
|
+
with open(path, "rb") as f:
|
|
220
|
+
raw = f.read()
|
|
221
|
+
except OSError:
|
|
222
|
+
print("图片读取失败:%s" % path, file=sys.stderr)
|
|
223
|
+
sys.exit(1)
|
|
161
224
|
ext = os.path.splitext(path)[1].lower()
|
|
162
225
|
mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
|
|
163
226
|
"webp": "image/webp", "gif": "image/gif"}.get(ext.lstrip("."), "image/png")
|
|
@@ -177,7 +240,6 @@ def inline_md(s):
|
|
|
177
240
|
def md_to_html(md_text, images, cover):
|
|
178
241
|
lines = md_text.split("\n")
|
|
179
242
|
out = []
|
|
180
|
-
img_idx = 0
|
|
181
243
|
cover_uri = data_uri(cover) if cover else None
|
|
182
244
|
|
|
183
245
|
i = 0
|
|
@@ -187,7 +249,7 @@ def md_to_html(md_text, images, cover):
|
|
|
187
249
|
i += 1
|
|
188
250
|
i += 1
|
|
189
251
|
|
|
190
|
-
|
|
252
|
+
in_fence = None # 开栏的反引号串(如 '```');None 表示不在围栏内
|
|
191
253
|
code_buf = []
|
|
192
254
|
in_ul = False
|
|
193
255
|
in_ol = False
|
|
@@ -224,28 +286,38 @@ def md_to_html(md_text, images, cover):
|
|
|
224
286
|
in_table = False
|
|
225
287
|
table_buf = []
|
|
226
288
|
|
|
289
|
+
def flush_prompt():
|
|
290
|
+
body = "\n".join(code_buf)
|
|
291
|
+
esc = html.escape(body).replace("\n", "<br>")
|
|
292
|
+
out.append(
|
|
293
|
+
'<div class="prompt"><blockquote><code>%s</code></blockquote>'
|
|
294
|
+
'<button class="btn" onclick="copyPrompt(this)">复制提示词</button>'
|
|
295
|
+
"</div>" % esc
|
|
296
|
+
)
|
|
297
|
+
|
|
227
298
|
while i < len(lines):
|
|
228
299
|
line = lines[i]
|
|
229
300
|
s = line.strip()
|
|
230
301
|
|
|
231
|
-
|
|
232
|
-
|
|
302
|
+
fm = re.match(r"^(`{3,})", s)
|
|
303
|
+
if fm:
|
|
304
|
+
fence = fm.group(1)
|
|
305
|
+
if in_fence is None:
|
|
306
|
+
# 开栏
|
|
233
307
|
close_lists()
|
|
234
308
|
flush_table()
|
|
235
|
-
|
|
309
|
+
in_fence = fence
|
|
310
|
+
code_buf = []
|
|
311
|
+
elif len(fence) >= len(in_fence) and s.strip("`") == "":
|
|
312
|
+
# 闭栏:纯反引号行且长度不短于开栏(内层短围栏视为代码内容)
|
|
313
|
+
flush_prompt()
|
|
314
|
+
in_fence = None
|
|
236
315
|
code_buf = []
|
|
237
316
|
else:
|
|
238
|
-
|
|
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
|
-
)
|
|
317
|
+
code_buf.append(line)
|
|
246
318
|
i += 1
|
|
247
319
|
continue
|
|
248
|
-
if
|
|
320
|
+
if in_fence is not None:
|
|
249
321
|
code_buf.append(line)
|
|
250
322
|
i += 1
|
|
251
323
|
continue
|
|
@@ -269,15 +341,16 @@ def md_to_html(md_text, images, cover):
|
|
|
269
341
|
uri = data_uri(images[n - 1])
|
|
270
342
|
fname = "图%d-%s" % (n, os.path.basename(images[n - 1]))
|
|
271
343
|
out.append(
|
|
272
|
-
'<figure class="ximg"><img src="%s" alt="图%d"
|
|
344
|
+
'<figure class="ximg"><img src="%s" alt="图%d" data-fname="%s" '
|
|
345
|
+
'onclick="openLightbox(this)">'
|
|
273
346
|
'<figcaption><span class="cap">[图%d]</span>'
|
|
274
347
|
'<button class="btn" data-fname="%s" '
|
|
275
348
|
'onclick="dlImg(this)">下载图片</button></figcaption></figure>'
|
|
276
|
-
% (uri, n,
|
|
349
|
+
% (uri, n, html.escape(fname, quote=True), n,
|
|
350
|
+
html.escape(fname, quote=True))
|
|
277
351
|
)
|
|
278
352
|
else:
|
|
279
353
|
out.append("<p>[图%d](图片缺失)</p>" % n)
|
|
280
|
-
img_idx = max(img_idx, n)
|
|
281
354
|
i += 1
|
|
282
355
|
continue
|
|
283
356
|
|
|
@@ -337,6 +410,9 @@ def md_to_html(md_text, images, cover):
|
|
|
337
410
|
|
|
338
411
|
close_lists()
|
|
339
412
|
flush_table()
|
|
413
|
+
if in_fence is not None:
|
|
414
|
+
# 未闭合围栏:文末自动收尾,不丢内容
|
|
415
|
+
flush_prompt()
|
|
340
416
|
|
|
341
417
|
body = "\n".join(out)
|
|
342
418
|
|
|
@@ -344,12 +420,12 @@ def md_to_html(md_text, images, cover):
|
|
|
344
420
|
if cover_uri:
|
|
345
421
|
cover_html = (
|
|
346
422
|
'<div class="cover-note no-paste" id="coverbox">'
|
|
347
|
-
'<img src="%s" alt="
|
|
348
|
-
'<div>封面图预览(
|
|
423
|
+
'<img src="%s" alt="封面预览(点击放大)" data-fname="%s" onclick="openLightbox(this)">'
|
|
424
|
+
'<div>封面图预览(1500×600 · 5:2)。X 文章有独立封面上传入口,'
|
|
349
425
|
'请单独上传,不要随正文粘贴。<br>'
|
|
350
|
-
'<button class="btn" data-fname="
|
|
351
|
-
'onclick="
|
|
352
|
-
) % cover_uri
|
|
426
|
+
'<button class="btn" data-fname="%s" '
|
|
427
|
+
'onclick="dlImg(this)">下载封面图</button></div></div>'
|
|
428
|
+
) % (cover_uri, COVER_FNAME, COVER_FNAME)
|
|
353
429
|
return cover_html + "\n" + body
|
|
354
430
|
|
|
355
431
|
|
|
@@ -358,24 +434,76 @@ def main():
|
|
|
358
434
|
ap.add_argument("md", help="原稿 markdown 路径")
|
|
359
435
|
ap.add_argument("--out", required=True, help="输出 HTML 路径")
|
|
360
436
|
ap.add_argument("--images", nargs="*", default=[], help="[图N] 对应的图片路径(按顺序)")
|
|
361
|
-
ap.add_argument("--
|
|
437
|
+
ap.add_argument("--images-from", default=None, metavar="WECHAT_MD",
|
|
438
|
+
help="从指定的 markdown(如公众号稿.md)中按文档顺序提取 ![]() 本地图片路径,"
|
|
439
|
+
"作为 [图N] 的图片来源。X 稿必须从公众号稿派生时用此项,"
|
|
440
|
+
"禁止手工拼 --images 顺序(曾因此出现配图错位)")
|
|
441
|
+
ap.add_argument("--cover", default=None, help="封面图路径(1500×600,5:2)")
|
|
362
442
|
args = ap.parse_args()
|
|
363
443
|
|
|
364
444
|
if not os.path.isfile(args.md):
|
|
365
445
|
print("输入文件不存在:%s" % args.md, file=sys.stderr)
|
|
366
446
|
sys.exit(1)
|
|
367
|
-
|
|
368
447
|
with open(args.md, encoding="utf-8") as f:
|
|
369
448
|
md_text = f.read()
|
|
370
449
|
|
|
371
|
-
|
|
450
|
+
# 图片来源:优先 --images-from(从公众号稿等源 md 按文档顺序派生),杜绝手工排序错位
|
|
451
|
+
images = list(args.images)
|
|
452
|
+
if args.images_from:
|
|
453
|
+
if not os.path.isfile(args.images_from):
|
|
454
|
+
print("图片来源文件不存在:%s" % args.images_from, file=sys.stderr)
|
|
455
|
+
sys.exit(1)
|
|
456
|
+
with open(args.images_from, encoding="utf-8") as f:
|
|
457
|
+
src_md = f.read()
|
|
458
|
+
base = os.path.dirname(os.path.abspath(args.images_from))
|
|
459
|
+
found = []
|
|
460
|
+
for q in re.findall(r"!\[[^\]]*\]\(([^)\s]+)\)", src_md):
|
|
461
|
+
if re.match(r"https?://", q):
|
|
462
|
+
print("警告:跳过远程图片(无法内嵌):%s" % q, file=sys.stderr)
|
|
463
|
+
continue
|
|
464
|
+
fp = q if os.path.isabs(q) else os.path.normpath(os.path.join(base, q))
|
|
465
|
+
found.append(fp)
|
|
466
|
+
if args.images:
|
|
467
|
+
print("警告:同时给了 --images 和 --images-from,采用 --images-from 派生顺序",
|
|
468
|
+
file=sys.stderr)
|
|
469
|
+
images = found
|
|
470
|
+
print("从 %s 派生图片顺序:%d 张" % (args.images_from, len(images)), file=sys.stderr)
|
|
471
|
+
|
|
472
|
+
for p in images:
|
|
473
|
+
if not os.path.isfile(p):
|
|
474
|
+
print("图片不存在:%s" % p, file=sys.stderr)
|
|
475
|
+
sys.exit(1)
|
|
476
|
+
|
|
477
|
+
# 校验:[图N] 标记数量必须与图片数量一致,且编号连续 1..N
|
|
478
|
+
markers = sorted({int(n) for line in md_text.split("\n")
|
|
479
|
+
for n in re.findall(r"^\[图(\d+)\]$", line.strip())})
|
|
480
|
+
if markers:
|
|
481
|
+
if markers != list(range(1, len(markers) + 1)):
|
|
482
|
+
print("图编号不连续:发现 %s,应为 1..%d" % (markers, len(markers)), file=sys.stderr)
|
|
483
|
+
sys.exit(1)
|
|
484
|
+
if len(images) != len(markers):
|
|
485
|
+
print("图片数量不匹配:[图N] 标记 %d 个,图片 %d 张" % (len(markers), len(images)),
|
|
486
|
+
file=sys.stderr)
|
|
487
|
+
sys.exit(1)
|
|
488
|
+
if args.cover and not os.path.isfile(args.cover):
|
|
489
|
+
print("封面图不存在:%s" % args.cover, file=sys.stderr)
|
|
490
|
+
sys.exit(1)
|
|
491
|
+
|
|
492
|
+
# 标题取 frontmatter 之后的第一个 #(frontmatter 里可能有 # 开头的行)
|
|
493
|
+
body_lines = md_text.split("\n")
|
|
494
|
+
if body_lines and body_lines[0].strip() == "---":
|
|
495
|
+
j = 1
|
|
496
|
+
while j < len(body_lines) and body_lines[j].strip() != "---":
|
|
497
|
+
j += 1
|
|
498
|
+
body_lines = body_lines[j + 1:]
|
|
499
|
+
m = re.search(r"^#\s+(.+)$", "\n".join(body_lines), re.M)
|
|
372
500
|
title = m.group(1).strip() if m else "X长文"
|
|
373
501
|
|
|
374
|
-
body = md_to_html(md_text,
|
|
375
|
-
page = HTML_TMPL.replace("
|
|
376
|
-
.replace("
|
|
377
|
-
.replace("
|
|
378
|
-
.replace("
|
|
502
|
+
body = md_to_html(md_text, images, args.cover)
|
|
503
|
+
page = HTML_TMPL.replace("@@TITLE@@", html.escape(title)) \
|
|
504
|
+
.replace("@@CSS@@", CSS) \
|
|
505
|
+
.replace("@@BODY@@", body) \
|
|
506
|
+
.replace("@@JS@@", JS)
|
|
379
507
|
|
|
380
508
|
out_dir = os.path.dirname(os.path.abspath(args.out))
|
|
381
509
|
os.makedirs(out_dir, exist_ok=True)
|