@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.
- package/CHANGELOG.md +114 -0
- package/README.en.md +12 -9
- package/README.md +12 -9
- package/bin/tms-skills.js +144 -12
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +8 -7
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +16 -171
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +13 -3
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +16 -324
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +16 -917
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +99 -18
- package/top-ppt-html/assets/icons/index.json +557 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.png +0 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.svg +1 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.png +0 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.svg +1 -0
- package/top-ppt-html/assets/icons//344/272/221.png +0 -0
- package/top-ppt-html/assets/icons//344/272/221.svg +1 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.png +0 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.svg +1 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.png +0 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.svg +1 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.png +0 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.svg +1 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.png +0 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.png +0 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.png +0 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.png +0 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.svg +1 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.png +0 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.png +0 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.svg +1 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.png +0 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.svg +1 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.png +0 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.svg +1 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.png +0 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.png +0 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.png +0 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.png +0 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.png +0 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.png +0 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.svg +1 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.png +0 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.svg +1 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.png +0 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.svg +1 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.png +0 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.svg +1 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.png +0 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.svg +1 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.png +0 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.svg +1 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.png +0 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.png +0 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.png +0 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.svg +1 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.png +0 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.svg +1 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.png +0 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.svg +1 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.png +0 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.png +0 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.svg +1 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.png +0 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.png +0 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.png +0 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.svg +1 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.png +0 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.svg +1 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.png +0 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.svg +1 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.png +0 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.svg +1 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.png +0 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.svg +1 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.png +0 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.svg +1 -0
- package/top-ppt-html/assets/pptx-export.js +2 -2
- package/top-ppt-html/assets/style-gallery.html +1 -1
- package/top-ppt-html/assets/templates/architecture.html +15 -3
- package/top-ppt-html/assets/templates/engine.css +12 -0
- package/top-ppt-html/assets/templates/presentation.html +15 -3
- package/top-ppt-html/assets/templates/research.html +15 -3
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +2 -2
- package/top-ppt-html/references/icons.md +22 -19
- package/top-ppt-html/references/layout-variants.md +69 -0
- package/top-ppt-html/references/style-pack.md +40 -0
- package/top-ppt-html/scripts/build_pptx.js +93 -26
- package/top-ppt-html/scripts/check_icons.py +119 -0
- package/top-ppt-html/scripts/hydrate_charts.py +60 -5
- package/top-ppt-html/scripts/icon_lib.js +66 -37
- package/top-ppt-html/scripts/layout-constants.json +91 -4
- package/top-ppt-html/scripts/make_icon_pack.py +172 -0
- package/top-ppt-html/scripts/model-schema.json +4 -1
- package/top-ppt-html/scripts/render_from_model.py +116 -55
- package/top-ppt-html/scripts/validate_pptx.py +1 -0
- package/top-ppt-html/scripts/validate_report.py +153 -0
- package/topmind-cover/README.en.md +4 -4
- package/topmind-cover/README.md +4 -4
- package/topmind-cover/SKILL.md +36 -18
- package/topmind-cover/assets/examples/README.md +13 -7
- package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
- package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
- package/topmind-cover/assets/examples/bg-blur.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
- package/topmind-cover/assets/examples/paper-collage.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
- package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean.png +0 -0
- package/topmind-cover/package.json +2 -2
- package/topmind-cover/references/cover-prompts.md +436 -0
- package/topmind-cover/references/cover-styles.md +149 -14
- package/topmind-cover/references/impact-language.md +9 -6
- package/topmind-cover/references/ref-white-clean-1.png +0 -0
- package/topmind-cover/references/ref-white-clean-2.png +0 -0
- package/topmind-wechat-post/README.en.md +1 -1
- package/topmind-wechat-post/README.md +1 -1
- package/topmind-wechat-post/references/wechat-constraints.md +5 -0
- package/topmind-wechat-post/scripts/md2wechat.py +130 -27
- package/topmind-wechat-post/scripts/negative_tests.py +74 -0
- package/topmind-x-article/README.en.md +25 -5
- package/topmind-x-article/README.md +21 -8
- package/topmind-x-article/SKILL.md +37 -13
- package/topmind-x-article/package.json +1 -1
- package/topmind-x-article/references/publish-checklist.md +9 -2
- package/topmind-x-article/references/x-html-format.md +73 -0
- package/topmind-x-article/scripts/md2x-html.py +394 -0
- package/topmind-x-article/scripts/md2x.py +18 -3
- package/topmind-x-article/scripts/negative_tests.py +17 -0
|
@@ -159,15 +159,22 @@ function _normColor(c) {
|
|
|
159
159
|
if (/^[0-9a-f]{3}$/.test(v)) v = '#' + v;
|
|
160
160
|
return v;
|
|
161
161
|
}
|
|
162
|
-
/** 取图标 PNG data URI;无缓存时按名挑色兜底(accent → 首色 → null) */
|
|
162
|
+
/** 取图标 PNG data URI;无缓存时按名挑色兜底(accent → 首色 → 包内 512px PNG → null) */
|
|
163
163
|
function iconPng(name, colorHex) {
|
|
164
164
|
const n = name && ICON_ASSETS.icons[name] ? name : ICON_LIB.pickIconName(name, 0);
|
|
165
165
|
const bag = ICON_ASSETS.icons[n];
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
if (bag) {
|
|
167
|
+
const want = _normColor(colorHex);
|
|
168
|
+
if (bag[want]) return bag[want];
|
|
169
|
+
const keys = Object.keys(bag);
|
|
170
|
+
if (keys.length) return bag[keys[0]];
|
|
171
|
+
}
|
|
172
|
+
// 无 sharp / 无预生成资产时的可靠回落:包内 512px 预渲染 PNG(python-pptx/pptxgenjs 均可直接嵌入)
|
|
173
|
+
try {
|
|
174
|
+
const p = ICON_LIB.iconPngPath(n);
|
|
175
|
+
if (p) return 'data:image/png;base64,' + fs.readFileSync(p).toString('base64');
|
|
176
|
+
} catch (e) { /* 继续回落 accent 方块 */ }
|
|
177
|
+
return null;
|
|
171
178
|
}
|
|
172
179
|
/** 在指定盒内嵌图标(真导出);无图标资产时回落 accent 小方块(可扫读路标) */
|
|
173
180
|
function addIcon(s, name, x, y, size, colorHex) {
|
|
@@ -275,6 +282,30 @@ const FS_POLICY = (function () {
|
|
|
275
282
|
}
|
|
276
283
|
})();
|
|
277
284
|
const FONT_LADDER = FS_POLICY.ladder;
|
|
285
|
+
/* 弹性文本单源(containers.adaptiveText · 0.2.0):长标题/长列表/长表格单元格的降级规则。
|
|
286
|
+
调用处只读 AT_* 与下列 helper,禁止手写字号/行距/阈值 magic number。 */
|
|
287
|
+
const AT = (LC.containers && LC.containers.adaptiveText) || {};
|
|
288
|
+
const AT_TITLE = AT.title || {}, AT_LIST = AT.list || {}, AT_TABLE = AT.table || {};
|
|
289
|
+
/* 标题字号上限:按字符数(CJK=1,拉丁/数字≈0.5,与 estLines 同口径)分档 */
|
|
290
|
+
function titleMaxFor(text) {
|
|
291
|
+
const s = String(text == null ? '' : text);
|
|
292
|
+
let units = 0;
|
|
293
|
+
for (const ch of s) units += /[\x00-\xff]/.test(ch) ? 0.5 : 1;
|
|
294
|
+
if (units >= (AT_TITLE.xlongChars != null ? AT_TITLE.xlongChars : 44)) return AT_TITLE.xlongMax || 20;
|
|
295
|
+
if (units >= (AT_TITLE.longChars != null ? AT_TITLE.longChars : 28)) return AT_TITLE.longMax || 24;
|
|
296
|
+
return AT_TITLE.base || 30;
|
|
297
|
+
}
|
|
298
|
+
/* 列表密集度:条目数达标 → 收紧字号上限/段间距/行距 */
|
|
299
|
+
function listFitOpts(n) {
|
|
300
|
+
const dense = n >= (AT_LIST.denseItems != null ? AT_LIST.denseItems : 6);
|
|
301
|
+
return {
|
|
302
|
+
dense,
|
|
303
|
+
max: dense ? (AT_LIST.denseMax || 12) : (AT_LIST.max || 14),
|
|
304
|
+
gapFactor: dense ? (AT_LIST.denseGapFactor != null ? AT_LIST.denseGapFactor : 0.25)
|
|
305
|
+
: (AT_LIST.gapFactor != null ? AT_LIST.gapFactor : 0.5),
|
|
306
|
+
lineFactor: dense ? (AT_LIST.denseLineFactor || 1.3) : (AT_LIST.lineFactor || 1.45),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
278
309
|
/* 缩字号触底记录:触底 = 内容量超出该版式承载力,是「该拆页/换组合」的信号,不得无声无息 */
|
|
279
310
|
const SHRINK_FLOOR_HITS = [];
|
|
280
311
|
function modeFloorPt() {
|
|
@@ -470,9 +501,10 @@ function head(s, eyebrow, title, lead) {
|
|
|
470
501
|
};
|
|
471
502
|
if (eyebrow) s.addText(eyebrow, { x: H.x, y: H.y, w: H.w, h: 0.4, fontFace: STYLE.font,
|
|
472
503
|
fontSize: sz(13), bold: true, color: STYLE.accent, charSpacing: 2 });
|
|
473
|
-
/*
|
|
504
|
+
/* 主标题自适应(adaptiveText.title 单源):先按字符数分档定上限,再按高度验算降一档,避免压到分隔线 */
|
|
474
505
|
const titleH = eyebrow ? (H.ruleY - H.titleY - 0.06) : 1.6 - 0.6;
|
|
475
|
-
const
|
|
506
|
+
const tMax = titleMaxFor(title);
|
|
507
|
+
const tSize = sz(estTextH(title, CW, sz(tMax), 1.25) > titleH ? Math.min(tMax, AT_TITLE.step1 || 24) : tMax);
|
|
476
508
|
s.addText(title, { x: H.x, y: eyebrow ? H.titleY : 0.6, w: H.w, h: Math.max(0.6, titleH),
|
|
477
509
|
fontFace: STYLE.fontDisplay, fontSize: tSize, bold: true, color: STYLE.ink, fit: 'shrink' });
|
|
478
510
|
s.addShape('rect', { x: H.x, y: eyebrow ? H.ruleY : 1.6, w: 0.9, h: 0.045, fill: { color: STYLE.accent }, line: { type: 'none' } });
|
|
@@ -515,7 +547,7 @@ function isRec(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
|
|
|
515
547
|
pointKV: [k,v] | {k|t, v|d} | str(对齐 pts_list)
|
|
516
548
|
metricKV: [v,k] | {v|value, k}(对齐 metrics_row)
|
|
517
549
|
bulletKV: [k,a,t] | {k,a,t}(对齐 r_bullet;缺省保持 undefined 以便调用方判空)
|
|
518
|
-
phaseKV: [label,name,desc,state] | {label,name,d,s}(对齐 r_timeline
|
|
550
|
+
phaseKV: [label,name,desc,state,icon?] | {label,name,d,s,icon?}(对齐 r_timeline;icon 为变体 timeline 节点图标)
|
|
519
551
|
paraKV: [title,text] | str(对齐 r_twocol;str 为无加粗标题的正文段) */
|
|
520
552
|
function pointKV(p) {
|
|
521
553
|
if (Array.isArray(p)) return [String(p[0] == null ? '' : p[0]), String(p[1] == null ? '' : p[1])];
|
|
@@ -533,9 +565,10 @@ function bulletKV(it) {
|
|
|
533
565
|
return [String(it == null ? '' : it), undefined, undefined];
|
|
534
566
|
}
|
|
535
567
|
function phaseKV(ph) {
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
568
|
+
// 第 5 元 = icon(变体 timeline 的节点图标;缺省 '',渲染回落圆点)
|
|
569
|
+
if (Array.isArray(ph)) return [ph[0] || '', ph[1] || '', ph[2] || '', ph[3] || '', ph[4] || ''];
|
|
570
|
+
if (isRec(ph)) return [ph.label || '', ph.name || '', ph.d || '', ph.s || '', ph.icon || ''];
|
|
571
|
+
return [String(ph == null ? '' : ph), '', '', '', ''];
|
|
539
572
|
}
|
|
540
573
|
function paraKV(p) {
|
|
541
574
|
if (Array.isArray(p)) return [p[0] || '', p[1] || ''];
|
|
@@ -1836,7 +1869,7 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1836
1869
|
const minRowH = opts.minRowH != null ? opts.minRowH : PT.table.rowHMin;
|
|
1837
1870
|
const headRowH = opts.headRowH != null ? opts.headRowH : (PT.table.headRowH || minRowH);
|
|
1838
1871
|
/* 行数超过容量时优先压行高到 hardFloor,仍放不下则整体缩放到 availH(禁溢出) */
|
|
1839
|
-
const hardFloor = Math.min(minRowH, 0.22);
|
|
1872
|
+
const hardFloor = Math.min(minRowH, AT_TABLE.rowFloorIn != null ? AT_TABLE.rowFloorIn : 0.22);
|
|
1840
1873
|
const ideal = availH / Math.max(1, n);
|
|
1841
1874
|
let rowH = Math.min(maxRowH, Math.max(hardFloor, ideal));
|
|
1842
1875
|
if (n * rowH > availH + 0.01) {
|
|
@@ -1847,7 +1880,8 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1847
1880
|
}
|
|
1848
1881
|
/* 行多则同步收字号(research 密排允许更小;句子单元格仍走语义字号门禁) */
|
|
1849
1882
|
const dense = rowH < 0.42;
|
|
1850
|
-
const headSz = sz(dense ? 11 : 13
|
|
1883
|
+
const headSz = sz(dense ? 11 : Math.min(13, AT_TABLE.headMax || 12));
|
|
1884
|
+
const bodySz = sz(dense ? 10 : Math.min(12.5, AT_TABLE.cellMax || 13));
|
|
1851
1885
|
const hRowH = Math.min(headRowH, Math.max(hardFloor, rowH * 1.15));
|
|
1852
1886
|
/* 内容高度预估:逐格用 estTextH 估算,超出行高则全表收字号 ——
|
|
1853
1887
|
禁依赖 fit:'shrink' 或渲染引擎自动撑高(PowerPoint/LibreOffice 会按内容增高行,压住脚注) */
|
|
@@ -1855,9 +1889,11 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1855
1889
|
const effColW = (Array.isArray(tbl.colW) && tbl.colW.length >= ncolsT) ? tbl.colW.slice(0, ncolsT)
|
|
1856
1890
|
: Array.from({ length: ncolsT }, () => w / ncolsT);
|
|
1857
1891
|
const cellPadIn = (dense ? 2 : 5) / 72;
|
|
1892
|
+
/* 长表格单元格适配(adaptiveText.table 单源):逐格估算,超出行高则全表收字号;
|
|
1893
|
+
floor 取模式下限与表格下限之大者——表格不得突破模式可读性底线 */
|
|
1858
1894
|
const fitCellSz = (text, cw, availHh, maxSz) => fitFont([String(text == null ? '' : text)],
|
|
1859
1895
|
Math.max(0.5, cw - cellPadIn * 2), Math.max(0.2, availHh - cellPadIn * 2),
|
|
1860
|
-
{ max: maxSz, maxShrinkSteps: 6 });
|
|
1896
|
+
{ max: maxSz, maxShrinkSteps: 6, floor: Math.max(modeFloorPt(), AT_TABLE.cellFloor || 0) });
|
|
1861
1897
|
let bodySzF = bodySz, headSzF = headSz;
|
|
1862
1898
|
rowsIn.forEach(r => r.forEach((c, j) => {
|
|
1863
1899
|
const f = fitCellSz(c, effColW[j] != null ? effColW[j] : effColW[0], rowH, bodySz);
|
|
@@ -2408,8 +2444,14 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2408
2444
|
ps.forEach((p, i) => {
|
|
2409
2445
|
const x = treg.x + 0.2 + i * stepW;
|
|
2410
2446
|
const on = p[3] === 'done' || p[3] === 'now';
|
|
2411
|
-
|
|
2412
|
-
|
|
2447
|
+
// 变体 timeline:节点图标(icon:<name>);无图标回落圆点
|
|
2448
|
+
const wantIcon = (sec.variant === 'timeline') && p[4];
|
|
2449
|
+
if (wantIcon) {
|
|
2450
|
+
addIcon(s, p[4], x - 0.13, T.dotY - 0.06, 0.26, on ? STYLE.accent : STYLE.body);
|
|
2451
|
+
} else {
|
|
2452
|
+
s.addShape('ellipse', { x: x - T.dotR / 2, y: T.dotY, w: T.dotR, h: T.dotR,
|
|
2453
|
+
fill: { color: on ? STYLE.accent : STYLE.line }, line: { type: 'none' } });
|
|
2454
|
+
}
|
|
2413
2455
|
s.addText(p[0], { x: x - 0.1, y: T.labelY, w: stepW - 0.2, h: 0.4, fontFace: STYLE.font,
|
|
2414
2456
|
fontSize: sz(12), bold: true, color: STYLE.accent, charSpacing: 1 });
|
|
2415
2457
|
s.addText(p[1], { x: x - 0.1, y: T.nameY, w: stepW - 0.2, h: 0.5, fontFace: STYLE.fontDisplay,
|
|
@@ -2465,11 +2507,25 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2465
2507
|
const fz = fitFont(items.length ? items : ['x'.repeat(longest)], g.w, availH, { max: 13.5, gapFactor: 1.2 });
|
|
2466
2508
|
groups.forEach((col, ci) => {
|
|
2467
2509
|
if (!col.length) return;
|
|
2510
|
+
const colX = treg.x + ci * g.step;
|
|
2511
|
+
// 变体 2-col-feature:首栏特性栏(accent 左线 + 标题放大一档)
|
|
2512
|
+
const isFeature = (sec.variant === '2-col-feature') && ci === 0 && nCol === 2;
|
|
2513
|
+
const fx = isFeature ? colX + 0.14 : colX;
|
|
2514
|
+
const fw = isFeature ? g.w - 0.14 : g.w;
|
|
2515
|
+
if (isFeature) {
|
|
2516
|
+
// 特性栏 accent 左线:高度按首段实际文本估算(不用整栏 availH,避免无墨水区压进注释带触发 severe 门禁)
|
|
2517
|
+
const p0 = col[0] || ['', ''];
|
|
2518
|
+
const barH = Math.min(availH,
|
|
2519
|
+
estTextH(p0[0] + ' ', fw, sz(fz + 1), 1.5) +
|
|
2520
|
+
estTextH(p0[1] || '', fw, sz(fz - 1), 1.5) + sz(7) / 72 + 0.06);
|
|
2521
|
+
s.addShape('rect', { x: colX, y: treg.y, w: 0.045, h: Math.max(0.2, barH),
|
|
2522
|
+
fill: { color: STYLE.accent }, line: { type: 'none' }, objectName: 'feature:bar' });
|
|
2523
|
+
}
|
|
2468
2524
|
const runs = col.map(p => ([
|
|
2469
|
-
{ text: p[0] + ' ', options: { fontSize: sz(fz), bold: true, color: STYLE.ink } },
|
|
2525
|
+
{ text: p[0] + ' ', options: { fontSize: sz(isFeature ? fz + 1 : fz), bold: true, color: STYLE.ink } },
|
|
2470
2526
|
{ text: p[1] + '\n', options: { fontSize: sz(fz - 1), color: STYLE.body, breakLine: true } },
|
|
2471
2527
|
])).flat();
|
|
2472
|
-
s.addText(runs, { x:
|
|
2528
|
+
s.addText(runs, { x: fx, y: treg.y, w: fw, h: availH, fontFace: STYLE.font,
|
|
2473
2529
|
valign: 'top', lineSpacing: sz(fz * 1.5), paraSpaceBefore: sz(7) });
|
|
2474
2530
|
});
|
|
2475
2531
|
} else if (type === 'halftable') {
|
|
@@ -2563,8 +2619,10 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2563
2619
|
: (Array.isArray(cd)
|
|
2564
2620
|
? { title: cd[0] || '', points: cd.length > 1 ? [['', cd[1]]] : [] }
|
|
2565
2621
|
: { title: String(cd == null ? '' : cd), points: [] }));
|
|
2566
|
-
const
|
|
2567
|
-
const
|
|
2622
|
+
const isBento = (sec.variant === 'bento-grid') && cds.length >= 3;
|
|
2623
|
+
const c = isBento ? 3 : Math.min(sec.columns || 3, cds.length || 1);
|
|
2624
|
+
// bento-grid:首卡占 2 个槽位(跨 2 列),其余顺排;槽位数 = 卡数 + 1
|
|
2625
|
+
const r = isBento ? Math.ceil((cds.length + 1) / c) : Math.ceil((cds.length || 1) / c);
|
|
2568
2626
|
const gw = (creg.w - (c - 1) * cdC.gap) / c;
|
|
2569
2627
|
const gh = fitRowH(creg.h - (r - 1) * cdC.gap, r, cdC.maxH, 0.6);
|
|
2570
2628
|
/* points 文本归一化(scaffold v9 契约 {title, points:[[k,v]]} 兼容三种形态) */
|
|
@@ -2589,21 +2647,28 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2589
2647
|
{ max: 12, lineFactor: 1.5, gapFactor: 0 }))) : 12;
|
|
2590
2648
|
const ptsFz = sz(ptsFzB);
|
|
2591
2649
|
cds.forEach((cd, i) => {
|
|
2592
|
-
|
|
2650
|
+
let col = i % c, row = Math.floor(i / c), span = 1;
|
|
2651
|
+
if (isBento) {
|
|
2652
|
+
// 首卡跨 2 列:槽位 = i===0 ? [0,1] : i+1
|
|
2653
|
+
const slot = i === 0 ? 0 : i + 1;
|
|
2654
|
+
if (i === 0) span = 2;
|
|
2655
|
+
col = slot % c; row = Math.floor(slot / c);
|
|
2656
|
+
}
|
|
2657
|
+
const cw = span * gw + (span - 1) * cdC.gap;
|
|
2593
2658
|
const x = creg.x + col * (gw + cdC.gap), y = creg.y + row * (gh + cdC.gap);
|
|
2594
|
-
s.addShape('roundRect', { x, y, w:
|
|
2659
|
+
s.addShape('roundRect', { x, y, w: cw, h: gh, rectRadius: 0.08,
|
|
2595
2660
|
fill: { color: STYLE.surface }, line: { color: STYLE.line, width: 0.75 } });
|
|
2596
2661
|
/* 卡片头路标:真导出语义图标(SVG→PNG),与 HTML .card__ico 同源;无资产时回落 accent 方块 */
|
|
2597
2662
|
const ico = 0.18;
|
|
2598
2663
|
const icoName = cd.icon || ICON_LIB.pickIconName(cd.title || '', i);
|
|
2599
2664
|
addIcon(s, icoName, x + 0.18, y + 0.2, ico, STYLE.accent);
|
|
2600
|
-
s.addText(cd.title || '', { x: x + 0.18 + ico + 0.1, y: y + 0.14, w:
|
|
2665
|
+
s.addText(cd.title || '', { x: x + 0.18 + ico + 0.1, y: y + 0.14, w: cw - 0.36 - ico - 0.1, h: cdC.titleH, fontFace: STYLE.font,
|
|
2601
2666
|
fontSize: sz(15), bold: true, color: STYLE.ink, fit: 'shrink' });
|
|
2602
2667
|
/* points 兼容:[[k,v]…] | [str…] | [{t,d}…](scaffold v9 契约 {title, points:[[k,v]]}) */
|
|
2603
2668
|
const ptLines = (cd.points || []).map(cardPtText);
|
|
2604
2669
|
s.addText(ptLines.map(t => ({ text: t + '\n',
|
|
2605
2670
|
options: { fontSize: ptsFz, color: STYLE.body, fontFace: STYLE.font, breakLine: true } })),
|
|
2606
|
-
{ x: x + 0.18, y: y + 0.14 + cdC.titleH + 0.06, w:
|
|
2671
|
+
{ x: x + 0.18, y: y + 0.14 + cdC.titleH + 0.06, w: cw - 0.36, h: ptsBoxH,
|
|
2607
2672
|
fontFace: STYLE.font, valign: 'top', lineSpacing: ptsFz * 1.5 });
|
|
2608
2673
|
});
|
|
2609
2674
|
} else if (type === 'split') {
|
|
@@ -2792,8 +2857,10 @@ CONTENT.sections.forEach((sec) => {
|
|
|
2792
2857
|
: CW - 0.4;
|
|
2793
2858
|
const avail = bodyBottom - bodyY;
|
|
2794
2859
|
const rowH = fitRowH(avail, pts.length, P.rowH, 0.3);
|
|
2860
|
+
/* 长列表收紧(adaptiveText.list 单源):条目多时降字号上限/段间距/行距,仍装不下由 fitFont 阶梯下探 */
|
|
2861
|
+
const _lo = listFitOpts(pts.length);
|
|
2795
2862
|
const fzB = fitFont(pts.map(p => (p[0] || '') + (p[1] || '')), textW - 0.32, avail,
|
|
2796
|
-
{ max: 15, gapFactor:
|
|
2863
|
+
{ max: Math.min(15, _lo.max), gapFactor: _lo.gapFactor, lineFactor: _lo.lineFactor });
|
|
2797
2864
|
const fz = sz(fzB), fzSm = sz(snapFont(fzB - 1));
|
|
2798
2865
|
pts.forEach((p, i) => {
|
|
2799
2866
|
const y = bodyY + i * rowH;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 图标包校验(assets/icons/)
|
|
4
|
+
|
|
5
|
+
检查:
|
|
6
|
+
1. 每个 *.svg 合法 XML;根为 svg;viewBox 恰为 "0 0 24 24"
|
|
7
|
+
2. 描边风格统一:fill="none"、stroke="currentColor"、stroke-width="1.8"、
|
|
8
|
+
stroke-linecap/linejoin="round"(同家族铁律)
|
|
9
|
+
3. 只含允许元素(path/circle/rect/ellipse/line/polyline/polygon),无 <text>/<image>,
|
|
10
|
+
无 fill="#..." 彩色填充(禁三方彩色标),无外链
|
|
11
|
+
4. index.json 一致性:icons 键 ↔ <name>.svg 文件一一对应;defaults 5~8 个且都在包内;
|
|
12
|
+
keywords 非空且含自身名
|
|
13
|
+
5. 每个图标有同名 512px PNG(PPTX 通道预渲染)
|
|
14
|
+
|
|
15
|
+
用法: python scripts/check_icons.py [--strict]
|
|
16
|
+
退出码:0=通过;1=有 FAIL;2=仅 WARN(--strict 下也按 1 处理)
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
import xml.etree.ElementTree as ET
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
26
|
+
ICON_DIR = ROOT / 'assets' / 'icons'
|
|
27
|
+
ALLOWED = {'path', 'circle', 'rect', 'ellipse', 'line', 'polyline', 'polygon'}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> int:
|
|
31
|
+
strict = '--strict' in sys.argv
|
|
32
|
+
fails: list[str] = []
|
|
33
|
+
warns: list[str] = []
|
|
34
|
+
|
|
35
|
+
idx_path = ICON_DIR / 'index.json'
|
|
36
|
+
if not idx_path.exists():
|
|
37
|
+
print('FAIL: assets/icons/index.json 缺失')
|
|
38
|
+
return 1
|
|
39
|
+
index = json.loads(idx_path.read_text(encoding='utf-8'))
|
|
40
|
+
icons_meta = index.get('icons') or {}
|
|
41
|
+
if index.get('count') != len(icons_meta):
|
|
42
|
+
fails.append(f"index.json count={index.get('count')} 与实际条目 {len(icons_meta)} 不一致")
|
|
43
|
+
|
|
44
|
+
svg_names = {p.stem for p in ICON_DIR.glob('*.svg')}
|
|
45
|
+
meta_names = set(icons_meta)
|
|
46
|
+
for n in sorted(meta_names - svg_names):
|
|
47
|
+
fails.append(f'index.json 有条目但缺文件: {n}.svg')
|
|
48
|
+
for n in sorted(svg_names - meta_names):
|
|
49
|
+
warns.append(f'有 SVG 文件但 index.json 未登记: {n}.svg')
|
|
50
|
+
|
|
51
|
+
defaults = index.get('defaults') or []
|
|
52
|
+
if not 5 <= len(defaults) <= 8:
|
|
53
|
+
fails.append(f'defaults 应 5~8 个,实际 {len(defaults)} 个')
|
|
54
|
+
for d in defaults:
|
|
55
|
+
if d not in icons_meta:
|
|
56
|
+
fails.append(f'default 图标不在包内: {d}')
|
|
57
|
+
|
|
58
|
+
for name, meta in icons_meta.items():
|
|
59
|
+
kws = meta.get('keywords') or []
|
|
60
|
+
if not kws:
|
|
61
|
+
warns.append(f'{name}: keywords 为空')
|
|
62
|
+
elif name not in kws:
|
|
63
|
+
warns.append(f'{name}: keywords 未含自身名')
|
|
64
|
+
png = ICON_DIR / f'{name}.png'
|
|
65
|
+
if not png.exists():
|
|
66
|
+
fails.append(f'缺预渲染 PNG: {name}.png')
|
|
67
|
+
elif png.stat().st_size < 1024:
|
|
68
|
+
warns.append(f'{name}.png 疑似过小({png.stat().st_size}B)')
|
|
69
|
+
|
|
70
|
+
for name in sorted(svg_names):
|
|
71
|
+
p = ICON_DIR / f'{name}.svg'
|
|
72
|
+
try:
|
|
73
|
+
root = ET.fromstring(p.read_text(encoding='utf-8'))
|
|
74
|
+
except ET.ParseError as e:
|
|
75
|
+
fails.append(f'{name}.svg XML 非法: {e}')
|
|
76
|
+
continue
|
|
77
|
+
tag = root.tag.split('}')[-1]
|
|
78
|
+
if tag != 'svg':
|
|
79
|
+
fails.append(f'{name}.svg 根元素不是 svg: {tag}')
|
|
80
|
+
continue
|
|
81
|
+
if root.get('viewBox') != '0 0 24 24':
|
|
82
|
+
fails.append(f"{name}.svg viewBox={root.get('viewBox')!r}(必须 '0 0 24 24')")
|
|
83
|
+
style = {
|
|
84
|
+
'fill': root.get('fill'), 'stroke': root.get('stroke'),
|
|
85
|
+
'stroke-width': root.get('stroke-width'),
|
|
86
|
+
'stroke-linecap': root.get('stroke-linecap'),
|
|
87
|
+
'stroke-linejoin': root.get('stroke-linejoin'),
|
|
88
|
+
}
|
|
89
|
+
want = {'fill': 'none', 'stroke': 'currentColor', 'stroke-width': '1.8',
|
|
90
|
+
'stroke-linecap': 'round', 'stroke-linejoin': 'round'}
|
|
91
|
+
if style != want:
|
|
92
|
+
fails.append(f'{name}.svg 风格属性偏离家族规范: {style}')
|
|
93
|
+
for el in root.iter():
|
|
94
|
+
t = el.tag.split('}')[-1]
|
|
95
|
+
if t == 'svg':
|
|
96
|
+
continue
|
|
97
|
+
if t not in ALLOWED:
|
|
98
|
+
fails.append(f'{name}.svg 含非法元素 <{t}>(禁 text/image/彩色填充)')
|
|
99
|
+
for k, v in el.attrib.items():
|
|
100
|
+
if k == 'fill' and v not in ('none',):
|
|
101
|
+
fails.append(f'{name}.svg <{t}> 有彩色填充 fill={v}')
|
|
102
|
+
if isinstance(v, str) and v.startswith('http'):
|
|
103
|
+
fails.append(f'{name}.svg 含外链: {v[:60]}')
|
|
104
|
+
|
|
105
|
+
for w in warns:
|
|
106
|
+
print(f'WARN: {w}')
|
|
107
|
+
for f in fails:
|
|
108
|
+
print(f'FAIL: {f}')
|
|
109
|
+
print(f'[check_icons] {len(svg_names)} svg / {len(icons_meta)} index / '
|
|
110
|
+
f'{len(defaults)} defaults: {"OK" if not fails else "FAIL"}')
|
|
111
|
+
if fails:
|
|
112
|
+
return 1
|
|
113
|
+
if warns and strict:
|
|
114
|
+
return 1
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == '__main__':
|
|
119
|
+
raise SystemExit(main())
|
|
@@ -13,6 +13,46 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
from render_from_model import extract_model_span
|
|
15
15
|
|
|
16
|
+
_ICON_DIR = Path(__file__).resolve().parent.parent / 'assets' / 'icons'
|
|
17
|
+
_ICON_CACHE: dict[str, str] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _icon_svg(name: str) -> str | None:
|
|
21
|
+
"""取图标包 SVG(内联用);缺图标返回 None(调用方保留原标记,由 validate WARN)。"""
|
|
22
|
+
if name in _ICON_CACHE:
|
|
23
|
+
return _ICON_CACHE[name]
|
|
24
|
+
p = _ICON_DIR / f'{name}.svg'
|
|
25
|
+
try:
|
|
26
|
+
svg = p.read_text(encoding='utf-8').strip()
|
|
27
|
+
except OSError:
|
|
28
|
+
return None
|
|
29
|
+
_ICON_CACHE[name] = svg
|
|
30
|
+
return svg
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# 手写 HTML 模板里的图标占位:<i data-icon="增长"></i> → 内联 SVG(currentColor 随主题变色)
|
|
34
|
+
# 未知图标名保留原标记(validate_report 报 WARN,不静默吞掉)
|
|
35
|
+
ICON_MARK_RE = re.compile(r'<i\s+data-icon="([^"]+)"\s*(?:/>|>\s*</i\s*>)')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def hydrate_icons(html: str) -> tuple[str, int, list[str]]:
|
|
39
|
+
"""内联 icon 标记。返回 (new_html, 成功替换数, 未知图标名列表)。"""
|
|
40
|
+
unknown: list[str] = []
|
|
41
|
+
replaced = [0]
|
|
42
|
+
|
|
43
|
+
def repl(m):
|
|
44
|
+
name = m.group(1)
|
|
45
|
+
svg = _icon_svg(name)
|
|
46
|
+
if svg is None:
|
|
47
|
+
if name not in unknown:
|
|
48
|
+
unknown.append(name)
|
|
49
|
+
return m.group(0)
|
|
50
|
+
replaced[0] += 1 # 只计真正替换的;未知名保留原标记,不计入
|
|
51
|
+
return svg
|
|
52
|
+
|
|
53
|
+
new_html, _ = ICON_MARK_RE.subn(repl, html)
|
|
54
|
+
return new_html, replaced[0], unknown
|
|
55
|
+
|
|
16
56
|
PLACEHOLDER_SVG_RE = re.compile(
|
|
17
57
|
r'<svg class="chart" data-chart="([^"]+)" viewBox="0 0 560 220">\s*'
|
|
18
58
|
r'<!--[\s\S]*?-->\s*'
|
|
@@ -336,20 +376,26 @@ def iter_chart_payloads(model: dict):
|
|
|
336
376
|
|
|
337
377
|
|
|
338
378
|
def hydrate(html: str, model: dict) -> str:
|
|
379
|
+
"""占位 SVG → 真实图表 SVG。
|
|
380
|
+
弹性(0.2.0):单图数据非法只留占位并记入 errors,不中断整份报告其余图的水合。"""
|
|
339
381
|
payloads = list(iter_chart_payloads(model))
|
|
340
382
|
idx = 0
|
|
383
|
+
errors: list = []
|
|
341
384
|
|
|
342
385
|
def repl(m):
|
|
343
386
|
nonlocal idx
|
|
344
387
|
if idx >= len(payloads):
|
|
345
388
|
return m.group(0)
|
|
346
389
|
ch, ctx = payloads[idx]
|
|
347
|
-
svg = build_svg(ch, ctx)
|
|
348
390
|
idx += 1
|
|
349
|
-
|
|
391
|
+
try:
|
|
392
|
+
return build_svg(ch, ctx)
|
|
393
|
+
except ChartDataError as e:
|
|
394
|
+
errors.append(str(e))
|
|
395
|
+
return m.group(0)
|
|
350
396
|
|
|
351
397
|
new_html, n = PLACEHOLDER_SVG_RE.subn(repl, html)
|
|
352
|
-
return new_html, n, idx, len(payloads)
|
|
398
|
+
return new_html, n, idx, len(payloads), errors
|
|
353
399
|
|
|
354
400
|
|
|
355
401
|
def main() -> int:
|
|
@@ -382,13 +428,22 @@ def main() -> int:
|
|
|
382
428
|
print(f'FAIL: REPORT_MODEL 根节点必须是对象(得到 {type(model).__name__})')
|
|
383
429
|
return 1
|
|
384
430
|
try:
|
|
385
|
-
new_html, n_ph, used, total = hydrate(html, model)
|
|
431
|
+
new_html, n_ph, used, total, chart_errors = hydrate(html, model)
|
|
386
432
|
except ChartDataError as e:
|
|
433
|
+
# 防御:hydrate 内部已按图捕获,此处仅兜底批量抽取阶段的非法数据
|
|
387
434
|
print(f'FAIL: 图表数据非法——{e}')
|
|
388
435
|
return 1
|
|
436
|
+
for e in chart_errors:
|
|
437
|
+
print(f'FAIL: 图表数据非法——{e}(该图保留占位,其余图已水合)')
|
|
438
|
+
rc = 1 if chart_errors else 0
|
|
439
|
+
new_html, n_icon, unknown_icons = hydrate_icons(new_html)
|
|
440
|
+
if unknown_icons:
|
|
441
|
+
print(f'WARN: 未知图标名(已保留原标记): {", ".join(unknown_icons)}')
|
|
389
442
|
path.write_text(new_html, encoding='utf-8')
|
|
390
443
|
left = new_html.count('见模型数据')
|
|
391
|
-
print(f'hydrated placeholders={n_ph} used_payloads={used}/{total} remaining_placeholder={left}')
|
|
444
|
+
print(f'hydrated placeholders={n_ph} icons={n_icon} used_payloads={used}/{total} remaining_placeholder={left}')
|
|
445
|
+
if rc:
|
|
446
|
+
return rc # 有图表数据非法:文件已落盘(坏图留占位),但以非零码报告
|
|
392
447
|
return 0 if left == 0 else 1
|
|
393
448
|
|
|
394
449
|
|
|
@@ -1,53 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TopPPT HTML · 语义图标库(
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* TopPPT HTML · 语义图标库(48 · 24×24 · stroke 1.8 圆角线帽 · currentColor)
|
|
3
|
+
* 单源:assets/icons/<name>.svg + assets/icons/index.json(关键词映射 + 默认表)。
|
|
4
|
+
* 由 scripts/make_icon_pack.py 生成。HTML 内联 / build_pptx.js PNG 真导出共用。
|
|
5
|
+
* 图标图 objectName 统一 `icon:` 前缀,validate_pptx 单独计数,不进 pictures 内容图门禁。
|
|
6
6
|
*/
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const ICON_DIR = path.join(__dirname, '..', 'assets', 'icons');
|
|
9
13
|
const SVG_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">';
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
let ICONS = {};
|
|
16
|
+
let ORDER = [];
|
|
17
|
+
let INDEX = { defaults: [], icons: {} };
|
|
18
|
+
|
|
19
|
+
function _load() {
|
|
20
|
+
try {
|
|
21
|
+
INDEX = JSON.parse(fs.readFileSync(path.join(ICON_DIR, 'index.json'), 'utf-8'));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
INDEX = { defaults: [], icons: {} };
|
|
24
|
+
}
|
|
25
|
+
const names = Object.keys(INDEX.icons || {});
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
try {
|
|
28
|
+
ICONS[name] = fs.readFileSync(path.join(ICON_DIR, `${name}.svg`), 'utf-8').trim();
|
|
29
|
+
} catch (e) { /* 缺文件则跳过,validate 会 WARN */ }
|
|
30
|
+
}
|
|
31
|
+
ORDER = Object.keys(ICONS);
|
|
32
|
+
if (!ORDER.length) {
|
|
33
|
+
// 极端回落:包不可读时给一个空壳,避免调用方崩溃
|
|
34
|
+
ICONS = { '信息': SVG_OPEN + '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.5h.01"/></svg>' };
|
|
35
|
+
ORDER = ['信息'];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
_load();
|
|
39
|
+
|
|
40
|
+
/** 默认图标表(index.json defaults;选不出语义图标时轮换) */
|
|
41
|
+
function defaultNames() {
|
|
42
|
+
const ds = (INDEX.defaults || []).filter((n) => ICONS[n]);
|
|
43
|
+
return ds.length ? ds : ORDER;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 按标题语义挑图标名:关键词命中 → 默认表轮换(不盲目轮换全量 48)。
|
|
48
|
+
* 与 render_from_model.pick_icon_name 同策略(同读 index.json)。
|
|
49
|
+
*/
|
|
38
50
|
function pickIconName(title, idx) {
|
|
39
51
|
const t = String(title || '');
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
const meta = INDEX.icons || {};
|
|
53
|
+
for (const name of ORDER) {
|
|
54
|
+
const kws = (meta[name] && meta[name].keywords) || [name];
|
|
55
|
+
for (const kw of kws) {
|
|
56
|
+
if (kw && t.includes(kw)) return name;
|
|
57
|
+
}
|
|
42
58
|
}
|
|
43
|
-
|
|
59
|
+
const ds = defaultNames();
|
|
60
|
+
return ds[(idx || 0) % ds.length];
|
|
44
61
|
}
|
|
45
62
|
|
|
46
|
-
/** 渲染为带指定描边色的 SVG
|
|
63
|
+
/** 渲染为带指定描边色的 SVG 字符串(currentColor → 目标色) */
|
|
47
64
|
function iconSvg(name, color) {
|
|
48
|
-
const raw = ICONS[name] || ICONS[ORDER[0]];
|
|
65
|
+
const raw = ICONS[name] || ICONS[defaultNames()[0]] || ICONS[ORDER[0]];
|
|
49
66
|
const c = color || '#1a56a8';
|
|
50
67
|
return raw.replace('stroke="currentColor"', `stroke="${c}"`);
|
|
51
68
|
}
|
|
52
69
|
|
|
53
|
-
|
|
70
|
+
/** 预渲染 PNG(512px)文件路径;无 sharp 环境下 PPTX 通道的可靠回落 */
|
|
71
|
+
function iconPngPath(name) {
|
|
72
|
+
const n = ICONS[name] ? name : pickIconName(name, 0);
|
|
73
|
+
const p = path.join(ICON_DIR, `${n}.png`);
|
|
74
|
+
try {
|
|
75
|
+
fs.accessSync(p, fs.constants.R_OK);
|
|
76
|
+
return p;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { ICONS, ORDER, INDEX, pickIconName, iconSvg, iconPngPath, SVG_OPEN, defaultNames };
|