@lark-apaas/coding-steering 0.1.18-dev.e6a2787 → 0.1.18-dev.ee5404f
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/package.json +1 -1
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +3 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +33 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +261 -66
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +38 -4
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +3 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +58 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +62 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +98 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_rounded_contract.py +112 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +168 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +12 -6
- package/steering/design-html/skills/slide-deck/SKILL.md +15 -20
- package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
(package.py 见 TODO 即 FAIL),由 L 层改掉。
|
|
13
13
|
"""
|
|
14
14
|
import argparse
|
|
15
|
+
import copy
|
|
15
16
|
import json
|
|
16
17
|
import os
|
|
17
18
|
import re
|
|
@@ -264,9 +265,9 @@ OFFICE_DEFAULT_FONTS_NORM = {norm(x) for x in OFFICE_DEFAULT_FONTS}
|
|
|
264
265
|
|
|
265
266
|
|
|
266
267
|
def cover_slot_colors(tokens, archetypes, rows, cusage):
|
|
267
|
-
"""slot 里出现的每个色值都必须在色板里有名字。
|
|
268
|
+
"""slot CSS 里出现的每个色值都必须在色板里有名字。
|
|
268
269
|
|
|
269
|
-
Hard Rules 写「颜色只用 colors 里的 token」,而 slot 的 color 是从模板直读的,
|
|
270
|
+
Hard Rules 写「颜色只用 colors 里的 token」,而 slot CSS 的 color 是从模板直读的,
|
|
270
271
|
两者不对齐就等于产物自己违反自己的规则——slot 的色值直读自模板,未必都已进
|
|
271
272
|
色板。这里把缺的补进色板,按用法归族命名。
|
|
272
273
|
"""
|
|
@@ -286,7 +287,7 @@ def cover_slot_colors(tokens, archetypes, rows, cusage):
|
|
|
286
287
|
added = []
|
|
287
288
|
for a in archetypes:
|
|
288
289
|
for s in a['slots']:
|
|
289
|
-
h = (s.get('
|
|
290
|
+
h = (s.get('_color') or '').upper()
|
|
290
291
|
if not h.startswith('#') or h in have:
|
|
291
292
|
continue
|
|
292
293
|
have.add(h)
|
|
@@ -500,7 +501,8 @@ def draft_anchors(d, tokens, fonts, roles, assets, archetypes):
|
|
|
500
501
|
# 6. 层级:字号跨度 + 字重是否单一(字重真单一才敢说「不靠字重」)
|
|
501
502
|
disp, body = roles.get('display'), roles.get('body')
|
|
502
503
|
if disp and body and disp['sz_px'] > body['sz_px']:
|
|
503
|
-
ws = {s.get('
|
|
504
|
+
ws = {s.get('_font_weight') for a in archetypes for s in a['slots']
|
|
505
|
+
if s.get('_font_weight')}
|
|
504
506
|
tail = (',字重只用 %s 一档' % list(ws)[0]) if len(ws) == 1 else ''
|
|
505
507
|
A.append(('size-driven-hierarchy', 'pattern',
|
|
506
508
|
'最大字号档与正文档相差 %.1f 倍(见 typography)%s'
|
|
@@ -930,11 +932,23 @@ def slot_overlaps(slots):
|
|
|
930
932
|
return out
|
|
931
933
|
|
|
932
934
|
|
|
935
|
+
def css_number(value, digits=3):
|
|
936
|
+
"""CSS 数值稳定格式:整数不带小数,其余去掉无意义尾零。"""
|
|
937
|
+
number = round(float(value), digits)
|
|
938
|
+
if number == int(number):
|
|
939
|
+
return str(int(number))
|
|
940
|
+
return ('%.*f' % (digits, number)).rstrip('0').rstrip('.')
|
|
941
|
+
|
|
942
|
+
|
|
933
943
|
def slot_style(s):
|
|
934
|
-
"""
|
|
944
|
+
"""占位符自带的排版样式,统一转成可直接写进 HTML style 的 CSS 声明串。
|
|
935
945
|
|
|
936
946
|
样式可能在三层:lstStyle.lvl1pPr(版式占位符常用)、段落 defRPr(Mac Office
|
|
937
947
|
导出把大量属性写在这一层)、段落 pPr(对齐)。逐层兜底,缺一层就往下取。
|
|
948
|
+
|
|
949
|
+
`box` 是布局几何,继续由 slot 独立承载;其余渲染属性不再泄漏成 size / color /
|
|
950
|
+
align / insets_px 等 PPTX 中间字段。下划线开头的键仅供 draft 内部统计,emit_layouts
|
|
951
|
+
不会写进消费者产物。
|
|
938
952
|
"""
|
|
939
953
|
txt = s.get('text') or {}
|
|
940
954
|
ls = dict((txt.get('lstStyle') or {}).get('lvl1pPr') or {})
|
|
@@ -955,12 +969,41 @@ def slot_style(s):
|
|
|
955
969
|
anysz = shape_sz(s)
|
|
956
970
|
if anysz:
|
|
957
971
|
ls['sz_px'] = anysz
|
|
972
|
+
body = txt.get('bodyPr') or {}
|
|
973
|
+
css = []
|
|
958
974
|
out = {}
|
|
975
|
+
insets = body.get('insets_px') or {}
|
|
976
|
+
if insets:
|
|
977
|
+
css.append('box-sizing: border-box')
|
|
978
|
+
css.append('padding: %spx %spx %spx %spx' % (
|
|
979
|
+
css_number(insets.get('tIns', 0) or 0),
|
|
980
|
+
css_number(insets.get('rIns', 0) or 0),
|
|
981
|
+
css_number(insets.get('bIns', 0) or 0),
|
|
982
|
+
css_number(insets.get('lIns', 0) or 0),
|
|
983
|
+
))
|
|
959
984
|
if ls.get('sz_px'):
|
|
960
|
-
|
|
985
|
+
size = round(ls['sz_px'])
|
|
986
|
+
css.append('font-size: %dpx' % size)
|
|
987
|
+
out['_font_size'] = size
|
|
988
|
+
weight = ls.get('weight') or (700 if ls.get('bold') else None)
|
|
989
|
+
if weight:
|
|
990
|
+
css.append('font-weight: %s' % weight)
|
|
991
|
+
out['_font_weight'] = weight
|
|
992
|
+
if ls.get('italic'):
|
|
993
|
+
css.append('font-style: italic')
|
|
994
|
+
decorations = []
|
|
995
|
+
if ls.get('underline'):
|
|
996
|
+
decorations.append('underline')
|
|
997
|
+
if ls.get('strike'):
|
|
998
|
+
decorations.append('line-through')
|
|
999
|
+
if decorations:
|
|
1000
|
+
css.append('text-decoration: %s' % ' '.join(decorations))
|
|
1001
|
+
if ls.get('spc_px') is not None:
|
|
1002
|
+
css.append('letter-spacing: %spx' % css_number(ls['spc_px']))
|
|
961
1003
|
col = (ls.get('color') or {}).get('resolved')
|
|
962
1004
|
if col:
|
|
963
|
-
|
|
1005
|
+
css.append('color: %s' % col)
|
|
1006
|
+
out['_color'] = col
|
|
964
1007
|
else:
|
|
965
1008
|
# 占位符的字色也可以是 gradFill(章节页的大号序号常这么做)。解析层已经把
|
|
966
1009
|
# stops 和角度记全了,这里只取单色就会整条丢掉,消费端只能自己编一个平色。
|
|
@@ -969,17 +1012,30 @@ def slot_style(s):
|
|
|
969
1012
|
if f.get('type') == 'gradient':
|
|
970
1013
|
g = _load_query()._css_gradient(f)
|
|
971
1014
|
if g:
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1015
|
+
css += ['background-image: %s' % g, '-webkit-background-clip: text',
|
|
1016
|
+
'background-clip: text', 'color: transparent']
|
|
1017
|
+
align = ls.get('algn')
|
|
1018
|
+
if align:
|
|
1019
|
+
css.append('text-align: %s' % {
|
|
1020
|
+
'l': 'left', 'ctr': 'center', 'r': 'right', 'just': 'justify',
|
|
1021
|
+
}.get(align, align))
|
|
1022
|
+
line_spacing = ls.get('lnSpc') or {}
|
|
1023
|
+
if line_spacing.get('mult'):
|
|
1024
|
+
css.append('line-height: %s' % css_number(line_spacing['mult'] * 1.2))
|
|
1025
|
+
elif line_spacing.get('px'):
|
|
1026
|
+
css.append('line-height: %spx' % css_number(line_spacing['px']))
|
|
1027
|
+
anchor = body.get('anchor')
|
|
981
1028
|
if anchor in ('ctr', 'b'):
|
|
982
|
-
|
|
1029
|
+
css += ['display: flex', 'flex-direction: column',
|
|
1030
|
+
'justify-content: %s' % {'ctr': 'center', 'b': 'flex-end'}[anchor]]
|
|
1031
|
+
if body.get('rot'):
|
|
1032
|
+
try:
|
|
1033
|
+
degrees = float(body['rot']) / 60000.0
|
|
1034
|
+
css.append('rotate: %sdeg' % css_number(degrees))
|
|
1035
|
+
except (TypeError, ValueError):
|
|
1036
|
+
pass
|
|
1037
|
+
if css:
|
|
1038
|
+
out['css'] = '; '.join(css)
|
|
983
1039
|
return out
|
|
984
1040
|
|
|
985
1041
|
|
|
@@ -1021,7 +1077,8 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1021
1077
|
"""
|
|
1022
1078
|
by_part = defaultdict(list)
|
|
1023
1079
|
for s in shapes:
|
|
1024
|
-
if s.get('layer') == 'layout' and s.get('
|
|
1080
|
+
if (s.get('layer') == 'layout' and s.get('kind') == 'sp'
|
|
1081
|
+
and (s.get('box') or {}).get('w') and (s.get('ph') or shape_text(s))):
|
|
1025
1082
|
by_part[s['part']].append(s)
|
|
1026
1083
|
bg_of_layout = {}
|
|
1027
1084
|
composites = d.get('background_composites') or {}
|
|
@@ -1050,10 +1107,10 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1050
1107
|
slots, seen_kind = [], set()
|
|
1051
1108
|
for s in phs:
|
|
1052
1109
|
t = PH_TO_TYPE.get((s['ph'] or {}).get('type'), 'body')
|
|
1053
|
-
if t in ('slide-number', 'footer'):
|
|
1054
|
-
continue #
|
|
1110
|
+
if t in ('slide-number', 'footer') and not shape_text(s):
|
|
1111
|
+
continue # 空 chrome 占位符不是实际元素
|
|
1055
1112
|
b = s['box']
|
|
1056
|
-
role = t if t in ('title', 'subtitle') else 'body'
|
|
1113
|
+
role = t if t in ('title', 'subtitle', 'footer', 'slide-number') else 'body'
|
|
1057
1114
|
if t == 'title' and 'title' in seen_kind:
|
|
1058
1115
|
role, t = 'subtitle', 'subtitle'
|
|
1059
1116
|
seen_kind.add(t)
|
|
@@ -1062,6 +1119,14 @@ def layouts_from_template(d, shapes, cW, cH):
|
|
|
1062
1119
|
round(b.get('w', 0)), round(b.get('h', 0))],
|
|
1063
1120
|
'txt': shape_text(s) or (s.get('name') or '')[:24]}
|
|
1064
1121
|
row.update(slot_style(s))
|
|
1122
|
+
if t == 'body':
|
|
1123
|
+
ph = s.get('ph') or {}
|
|
1124
|
+
row.update({
|
|
1125
|
+
'_needs_role': True,
|
|
1126
|
+
'_source_layer': 'layout',
|
|
1127
|
+
'_placeholder': '%s/%s' % (
|
|
1128
|
+
ph.get('type') or '-', ph.get('idx') or '-'),
|
|
1129
|
+
})
|
|
1065
1130
|
slots.append(row)
|
|
1066
1131
|
# 非满屏的图片元素(logo / 联名标 / 装饰)——它们逐版式换位置换尺寸,
|
|
1067
1132
|
# 必须按版式落进 slots,压成一条全局「固定位」规则就会撞标题。
|
|
@@ -1181,17 +1246,76 @@ def collect_decor(shapes, part, taken_boxes, canvas, limit=10):
|
|
|
1181
1246
|
return out[:limit] # 同款不同位置都要留,位置本身是版式信息
|
|
1182
1247
|
|
|
1183
1248
|
|
|
1249
|
+
def placeholder_key(shape):
|
|
1250
|
+
ph = shape.get('ph') or {}
|
|
1251
|
+
if not ph:
|
|
1252
|
+
return None
|
|
1253
|
+
return (ph.get('type') or 'body', str(ph.get('idx') or ''))
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def merge_dict(base, override):
|
|
1257
|
+
"""把实例页的非空声明叠到版式声明上;空实例占位符继续继承版式事实。"""
|
|
1258
|
+
out = copy.deepcopy(base or {})
|
|
1259
|
+
for key, value in (override or {}).items():
|
|
1260
|
+
if value is None or value == []:
|
|
1261
|
+
continue
|
|
1262
|
+
if isinstance(value, dict) and isinstance(out.get(key), dict):
|
|
1263
|
+
out[key] = merge_dict(out[key], value)
|
|
1264
|
+
else:
|
|
1265
|
+
out[key] = copy.deepcopy(value)
|
|
1266
|
+
return out
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def inherited_text_shapes(layout_shapes, slide_shapes):
|
|
1270
|
+
"""返回实例页可用的文字形状,并补齐其引用版式中的占位符几何与样式。"""
|
|
1271
|
+
layout_text = []
|
|
1272
|
+
for shape in layout_shapes:
|
|
1273
|
+
if shape.get('kind') != 'sp' or not (shape.get('box') or {}).get('w'):
|
|
1274
|
+
continue
|
|
1275
|
+
ph = shape.get('ph') or {}
|
|
1276
|
+
ph_type = ph.get('type')
|
|
1277
|
+
if shape_text(shape) or (ph and ph_type not in ('ftr', 'dt', 'sldNum')):
|
|
1278
|
+
layout_text.append(shape)
|
|
1279
|
+
by_placeholder = {placeholder_key(s): s for s in layout_text if placeholder_key(s)}
|
|
1280
|
+
used = set()
|
|
1281
|
+
out = []
|
|
1282
|
+
for shape in slide_shapes:
|
|
1283
|
+
if shape.get('kind') != 'sp':
|
|
1284
|
+
continue
|
|
1285
|
+
key = placeholder_key(shape)
|
|
1286
|
+
base = by_placeholder.get(key)
|
|
1287
|
+
if base:
|
|
1288
|
+
merged = merge_dict(base, shape)
|
|
1289
|
+
merged['text'] = merge_dict(base.get('text'), shape.get('text'))
|
|
1290
|
+
if not shape_text(shape):
|
|
1291
|
+
merged['text']['paragraphs'] = copy.deepcopy(
|
|
1292
|
+
(base.get('text') or {}).get('paragraphs') or [])
|
|
1293
|
+
used.add(key)
|
|
1294
|
+
out.append((merged, 'slide+layout'))
|
|
1295
|
+
elif (shape.get('box') or {}).get('w') and shape_text(shape):
|
|
1296
|
+
out.append((shape, 'slide'))
|
|
1297
|
+
for shape in layout_text:
|
|
1298
|
+
key = placeholder_key(shape)
|
|
1299
|
+
if key not in used:
|
|
1300
|
+
out.append((shape, 'layout'))
|
|
1301
|
+
return out
|
|
1302
|
+
|
|
1303
|
+
|
|
1184
1304
|
def draft_layouts(d, outdir):
|
|
1185
|
-
|
|
1305
|
+
with open(os.path.join(outdir, 'ref', 'shapes.json'), encoding='utf-8') as stream:
|
|
1306
|
+
shapes = json.load(stream)['shapes']
|
|
1186
1307
|
cW, cH = d['canvas']['px']
|
|
1187
1308
|
if (d.get('form_hint') or {}).get('form') == 3:
|
|
1188
1309
|
arch = layouts_from_template(d, shapes, cW, cH)
|
|
1189
1310
|
if len(arch) >= 3:
|
|
1190
1311
|
return arch, [], []
|
|
1191
1312
|
by_slide = defaultdict(list)
|
|
1313
|
+
by_layout = defaultdict(list)
|
|
1192
1314
|
for s in shapes:
|
|
1193
1315
|
if s.get('layer') == 'slide':
|
|
1194
1316
|
by_slide[s['part']].append(s)
|
|
1317
|
+
elif s.get('layer') == 'layout':
|
|
1318
|
+
by_layout[s['part']].append(s)
|
|
1195
1319
|
|
|
1196
1320
|
bg_of_slide, layout_of_slide = {}, {}
|
|
1197
1321
|
for s in d.get('slides', []):
|
|
@@ -1207,32 +1331,43 @@ def draft_layouts(d, outdir):
|
|
|
1207
1331
|
|
|
1208
1332
|
pages = []
|
|
1209
1333
|
for part, sh in sorted(by_slide.items(), key=lambda kv: slide_no(kv[0])):
|
|
1334
|
+
layout_part = layout_of_slide.get(part)
|
|
1335
|
+
layout_shapes = by_layout.get(layout_part) or []
|
|
1210
1336
|
bg_media = top_bleed_media(sh)
|
|
1211
1337
|
if bg_media is None:
|
|
1212
|
-
bg_media = bg_of_layout.get(
|
|
1338
|
+
bg_media = bg_of_layout.get(layout_part)
|
|
1213
1339
|
rendered_bg = (composites.get(part)
|
|
1214
|
-
or composites.get(
|
|
1340
|
+
or composites.get(layout_part)
|
|
1215
1341
|
or bg_media)
|
|
1216
1342
|
texts = []
|
|
1217
|
-
for s in sh:
|
|
1218
|
-
|
|
1219
|
-
continue
|
|
1220
|
-
txt = shape_text(s)
|
|
1221
|
-
if not txt:
|
|
1222
|
-
continue
|
|
1343
|
+
for s, source_layer in inherited_text_shapes(layout_shapes, sh):
|
|
1344
|
+
txt = shape_text(s) or (s.get('name') or '')[:24]
|
|
1223
1345
|
b = s.get('box') or {}
|
|
1224
1346
|
if b.get('w', 0) < DECOR_MIN or b.get('h', 0) < 16:
|
|
1225
1347
|
continue
|
|
1226
|
-
|
|
1348
|
+
ph = s.get('ph') or {}
|
|
1349
|
+
ph_type = ph.get('type')
|
|
1350
|
+
direct_type = PH_TO_TYPE.get(ph_type, 'body')
|
|
1351
|
+
texts.append({
|
|
1352
|
+
'sz': shape_sz(s),
|
|
1353
|
+
'box': b,
|
|
1354
|
+
'txt': txt,
|
|
1355
|
+
'style': slot_style(s),
|
|
1356
|
+
'direct_type': direct_type,
|
|
1357
|
+
'needs_role': direct_type == 'body',
|
|
1358
|
+
'source_layer': source_layer,
|
|
1359
|
+
'placeholder': '%s/%s' % (ph_type or '-', ph.get('idx') or '-'),
|
|
1360
|
+
})
|
|
1227
1361
|
texts.sort(key=lambda t: (-t['sz'], t['box'].get('y', 0)))
|
|
1228
|
-
|
|
1362
|
+
visible_shapes = layout_shapes + sh
|
|
1363
|
+
pics = [s for s in visible_shapes if s.get('kind') == 'pic' and s.get('w_pct', 0) < 95]
|
|
1229
1364
|
# 小图元素(logo / 角标 / 装饰)逐页记位置,供 archetype 落 slots
|
|
1230
1365
|
marks = [{'media': s['media'], 'box': s['box']} for s in pics
|
|
1231
1366
|
if s.get('media') and (s.get('box') or {}).get('w') and s.get('w_pct', 0) < 30]
|
|
1232
1367
|
pages.append({'part': part, 'no': slide_no(part), 'bg_media': bg_media,
|
|
1233
1368
|
'rendered_bg': rendered_bg,
|
|
1234
1369
|
'bg_color': bg_of_slide.get(part), 'texts': texts, 'pic_n': len(pics),
|
|
1235
|
-
'marks': marks, 'shape_n': len(
|
|
1370
|
+
'marks': marks, 'shape_n': len(visible_shapes), 'layout': layout_part})
|
|
1236
1371
|
|
|
1237
1372
|
# 页型的**角色**(封面 / 章节页 / 内容页……)不在这里判:那是看图才能下的结论,
|
|
1238
1373
|
# 交给读得到重建图的模型。脚本只做客观归并——同一张底图 + 文字块数量相近的页
|
|
@@ -1291,7 +1426,11 @@ def draft_layouts(d, outdir):
|
|
|
1291
1426
|
'——包里的坐标是模板量出来的,自创网格等于放弃这套版式', name)
|
|
1292
1427
|
for i, t in enumerate(ordered[:6]):
|
|
1293
1428
|
b = t['box']
|
|
1294
|
-
if t
|
|
1429
|
+
if t.get('needs_role'):
|
|
1430
|
+
role = typ = 'body'
|
|
1431
|
+
elif t.get('direct_type') in ('title', 'subtitle', 'footer', 'slide-number'):
|
|
1432
|
+
role = typ = t['direct_type']
|
|
1433
|
+
elif t is title:
|
|
1295
1434
|
role = typ = 'title'
|
|
1296
1435
|
elif (title and i == 1
|
|
1297
1436
|
# 副标题 = 紧跟在标题下方、与标题左对齐的那一块。三个量都相对标题
|
|
@@ -1307,6 +1446,12 @@ def draft_layouts(d, outdir):
|
|
|
1307
1446
|
round(b.get('w', 0)), round(b.get('h', 0))],
|
|
1308
1447
|
'type': typ, 'sz': t['sz'], 'txt': t['txt']}
|
|
1309
1448
|
row.update(t.get('style') or {})
|
|
1449
|
+
if t.get('needs_role'):
|
|
1450
|
+
row.update({
|
|
1451
|
+
'_needs_role': True,
|
|
1452
|
+
'_source_layer': t.get('source_layer'),
|
|
1453
|
+
'_placeholder': t.get('placeholder'),
|
|
1454
|
+
})
|
|
1310
1455
|
slots.append(row)
|
|
1311
1456
|
# 代表页上的小图元素按位置去重后落 slots(同一 logo 在不同页型位置不同)
|
|
1312
1457
|
seen_mark = set()
|
|
@@ -1320,7 +1465,15 @@ def draft_layouts(d, outdir):
|
|
|
1320
1465
|
'media': mk['media'],
|
|
1321
1466
|
'box': [round(b.get('x', 0)), round(b.get('y', 0)),
|
|
1322
1467
|
round(b.get('w', 0)), round(b.get('h', 0))]})
|
|
1323
|
-
|
|
1468
|
+
taken = {tuple(s['box']) for s in slots}
|
|
1469
|
+
decor = []
|
|
1470
|
+
seen_decor = set()
|
|
1471
|
+
for source_part in (rep.get('layout'), rep['part']):
|
|
1472
|
+
for item in collect_decor(shapes, source_part, taken, (cW, cH)):
|
|
1473
|
+
key = (tuple(item['box']), item['geom'], item['css'])
|
|
1474
|
+
if key not in seen_decor:
|
|
1475
|
+
seen_decor.add(key)
|
|
1476
|
+
decor.append(item)
|
|
1324
1477
|
archetypes.append({'name': name, 'bg': None, 'bg_raw': bg_raw, 'slots': slots,
|
|
1325
1478
|
'decor': decor,
|
|
1326
1479
|
'pages': sorted(p['no'] for p in ps), 'rep': rep['no'],
|
|
@@ -1470,10 +1623,11 @@ def emit_frontmatter(d, tokens, fonts, roles, anchors, gaps, ldir):
|
|
|
1470
1623
|
L.append('spacing:')
|
|
1471
1624
|
L.append(' page-padding: {top: %s, right: %s, bottom: %s, left: %s}'
|
|
1472
1625
|
% (edge['top'], edge['right'], edge['bottom'], edge['left']))
|
|
1473
|
-
#
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1626
|
+
# rounded.card 是全局 token,只能表达全档共同的一档圆角。多个非零档位或零/非零
|
|
1627
|
+
# 混用时,圆角属于 layouts.md 里的局部形状事实,压成一个值会把直角容器也圆角化。
|
|
1628
|
+
radii = d.get('radii_census') or []
|
|
1629
|
+
if len(radii) == 1 and radii[0]['px'] >= 1:
|
|
1630
|
+
top = radii[0]
|
|
1477
1631
|
L.append('rounded:')
|
|
1478
1632
|
L.append(' card: %dpx' % round(top['px']))
|
|
1479
1633
|
if edges_full:
|
|
@@ -1601,8 +1755,7 @@ def structure_facts(archetypes, d, shapes):
|
|
|
1601
1755
|
'fill': fill, 'line': line, 'fx': fx, 'shapes': set()})
|
|
1602
1756
|
g['n'] += 1
|
|
1603
1757
|
g['parts'].add(s.get('part'))
|
|
1604
|
-
|
|
1605
|
-
g['radii'].append(s['radius_px'])
|
|
1758
|
+
g['radii'].append(s.get('radius_px') or 0)
|
|
1606
1759
|
g['shapes'].add(id(s))
|
|
1607
1760
|
ranked = sorted(groups.values(), key=lambda g: -g['n'])
|
|
1608
1761
|
recipe_id = {}
|
|
@@ -1645,7 +1798,8 @@ def structure_facts(archetypes, d, shapes):
|
|
|
1645
1798
|
def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
1646
1799
|
prefilled = sum(1 for a in archetypes if a.get('zh'))
|
|
1647
1800
|
L = ['# 判断单草案 —— package.py 读它产出 layouts.md,deck 的版式坐标从 layouts.md 读。',
|
|
1648
|
-
'# 只改 names / roles /
|
|
1801
|
+
'# 只改 names / roles / text_roles / bg_rules 四段(都是扁平键值,'
|
|
1802
|
+
'改完 package.py 自动并回各页型)。',
|
|
1649
1803
|
'# 下面 layouts 段是普查数值,一个字都不要动——改它容易连带删掉 slots/confidence。']
|
|
1650
1804
|
if prefilled:
|
|
1651
1805
|
L.append('# names 已按模板自带的版式名填好 %d 条,读一遍确认表意即可,通常不用改。' % prefilled)
|
|
@@ -1674,6 +1828,27 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1674
1828
|
len([s for s in a['slots'] if not s.get('asset')]),
|
|
1675
1829
|
'/'.join(str(x) for x in szs[:5]) or '未声明',
|
|
1676
1830
|
a.get('pic_n') or 0, ';有满屏底图' if a.get('bg_raw') else ''))
|
|
1831
|
+
text_role_ids = {}
|
|
1832
|
+
for a in archetypes:
|
|
1833
|
+
index = 0
|
|
1834
|
+
for slot in a.get('slots') or []:
|
|
1835
|
+
if not slot.get('_needs_role'):
|
|
1836
|
+
continue
|
|
1837
|
+
index += 1
|
|
1838
|
+
text_role_ids[id(slot)] = '%s-text-%d' % (a['name'], index)
|
|
1839
|
+
if text_role_ids:
|
|
1840
|
+
L.append('text_roles: # 取值 title|subtitle|header|footer|body;只改角色,不删槽')
|
|
1841
|
+
for a in archetypes:
|
|
1842
|
+
for slot in a.get('slots') or []:
|
|
1843
|
+
role_id = text_role_ids.get(id(slot))
|
|
1844
|
+
if not role_id:
|
|
1845
|
+
continue
|
|
1846
|
+
L.append(' %s: TODO文本角色 # 来源 %s;占位符 %s;样例 %s;'
|
|
1847
|
+
'box %s;字号 %s;css %s'
|
|
1848
|
+
% (role_id, slot.get('_source_layer') or '-',
|
|
1849
|
+
slot.get('_placeholder') or '-', q(slot.get('txt') or ''),
|
|
1850
|
+
slot.get('box'), round(slot.get('sz') or 0),
|
|
1851
|
+
q(slot.get('css') or '未声明')))
|
|
1677
1852
|
# 禁放区是**背景图**的属性,不是页型的属性——按背景资产分组,页型再多也不涨
|
|
1678
1853
|
bgs = []
|
|
1679
1854
|
for a in archetypes:
|
|
@@ -1759,6 +1934,9 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1759
1934
|
L.append(' gap: %d' % r['gap'])
|
|
1760
1935
|
L.append(' items:')
|
|
1761
1936
|
for s in r['items']:
|
|
1937
|
+
role_id = text_role_ids.get(id(s))
|
|
1938
|
+
if role_id:
|
|
1939
|
+
L.append(' # text-role: %s' % role_id)
|
|
1762
1940
|
# free 区带按坐标摆,而 slots 会被删掉,所以坐标必须写在这里
|
|
1763
1941
|
bx = ', box: %s' % s['box'] if r['kind'] == 'free' else ''
|
|
1764
1942
|
if s.get('type') == 'decor':
|
|
@@ -1766,25 +1944,22 @@ def emit_layouts(archetypes, ldir, busy_hints=None, facts=None, recipes=None):
|
|
|
1766
1944
|
% (bx, (s.get('css') or '').replace('"', "'")))
|
|
1767
1945
|
continue
|
|
1768
1946
|
extra = bx
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
extra += ', %s: %s' % (
|
|
1773
|
-
k, '"%s"' % str(s[k]).replace('"', "'")
|
|
1774
|
-
if k in ('color', 'css') else s[k])
|
|
1947
|
+
if s.get('css') is not None:
|
|
1948
|
+
# CSS 串一律加引号:里面的逗号/冒号在 flow map 里是分隔符
|
|
1949
|
+
extra += ', css: "%s"' % str(s['css']).replace('"', "'")
|
|
1775
1950
|
if s.get('asset'):
|
|
1776
1951
|
extra += ', asset: %s' % s['asset']
|
|
1777
1952
|
L.append(' - {role: %s, type: %s%s}' % (s['role'], s['type'], extra))
|
|
1778
1953
|
L.append(' slots:')
|
|
1779
1954
|
for s in a['slots']:
|
|
1955
|
+
role_id = text_role_ids.get(id(s))
|
|
1956
|
+
if role_id:
|
|
1957
|
+
L.append(' # text-role: %s' % role_id)
|
|
1780
1958
|
extra = ''
|
|
1781
1959
|
if s.get('asset'):
|
|
1782
1960
|
extra += ', asset: %s' % s['asset']
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
v = s[k]
|
|
1786
|
-
extra += ', %s: %s' % (
|
|
1787
|
-
k, '"%s"' % str(v).replace('"', "'") if k in ('color', 'css') else v)
|
|
1961
|
+
if s.get('css') is not None:
|
|
1962
|
+
extra += ', css: "%s"' % str(s['css']).replace('"', "'")
|
|
1788
1963
|
L.append(' - {role: %s, box: %s, type: %s%s}'
|
|
1789
1964
|
% (s['role'], s['box'], s['type'], extra))
|
|
1790
1965
|
if a.get('decor'):
|
|
@@ -1811,7 +1986,7 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1811
1986
|
|
|
1812
1987
|
L = ['## Overview', '',
|
|
1813
1988
|
'TODO: 两三句话讲清这套模板的性格与适用场景——看过联系表和页面重建图之后再写。', '']
|
|
1814
|
-
L.append(('模板自带 %d
|
|
1989
|
+
L.append(('模板自带 %d 种版式,页型、坐标和 CSS 样式都直读自版式层。'
|
|
1815
1990
|
% len(archetypes)) if (d.get('form_hint') or {}).get('form') == 3 else
|
|
1816
1991
|
('%d 页样张归纳出 %d 种页型。' % (d['counts']['slides'], len(archetypes))))
|
|
1817
1992
|
L += ['', '## Usage', '',
|
|
@@ -1831,22 +2006,34 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1831
2006
|
'内容决定,不要写死高度**——上面的区带内容变多时,下面的自然被推下去,这正是'
|
|
1832
2007
|
'这套表达要解决的事。区带内部:`kind: grid` 用 `grid-template-columns: repeat(cols, 1fr)` '
|
|
1833
2008
|
'配 `gap: [行间距, 列间距]`;`kind: stack` 用纵向 flex 配 `gap`;`kind: free` '
|
|
1834
|
-
'按该页型 `slots`
|
|
1835
|
-
'
|
|
2009
|
+
'按该页型 `slots` 里的坐标绝对定位。每个 `role: container` 的项是容器,把它的 '
|
|
2010
|
+
'`css` 逐项原样写进 style,内容放进去;其中没有 `border-radius` 就按 `0`,'
|
|
2011
|
+
'不得自行补圆角。',
|
|
1836
2012
|
'4. **按 slot 落元素(页型给的是 slots 时)** —— 每个 slot 渲染成一个绝对定位元素:`box` 是 '
|
|
1837
|
-
'`[x, y, w, h]`(%dx%d
|
|
1838
|
-
'
|
|
2013
|
+
'`[x, y, w, h]`(%dx%d 画布上的绝对像素),机械展开成 `left/top/width/height`;'
|
|
2014
|
+
'slot 的 `css` 是模板排版属性已转译好的声明串,原样写进 style,不要另选字号、'
|
|
2015
|
+
'内边距、颜色或对齐。'
|
|
1839
2016
|
'带 `asset` 的 slot 是图片元素(logo、角标),把该资产放在它自己的 `box` 里;'
|
|
1840
2017
|
'这个页型没有 `asset` 槽,这一页就不出现该资产。' % (canvas[0], canvas[1]),
|
|
1841
2018
|
'5. **铺装饰几何** —— 页型的 `decor` 是这一页的图形骨架(图标托底的圆、'
|
|
1842
|
-
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css`
|
|
1843
|
-
'`geom: ellipse` 另加
|
|
2019
|
+
'卡片、分隔线):每条渲染成一个绝对定位空元素,`box` 给位置,`css` 逐项原样写进 '
|
|
2020
|
+
'style;没有 `border-radius` 就按 `0`。只有 `geom: ellipse` 另加 '
|
|
2021
|
+
'`border-radius: 50%`。它们压在背景之上、slot 之下,'
|
|
1844
2022
|
'落在 slot 上的图标正是靠它们托住。',
|
|
1845
|
-
'6.
|
|
2023
|
+
'6. **落实全局设计** —— `design.md` frontmatter 的 `colors`、`typography`、'
|
|
2024
|
+
'`spacing`、`rounded`、`components` 是全局 token;用 CSS variables、类名或内联'
|
|
2025
|
+
'样式承载。局部 slot / decor 的 `css` 优先,不能再解释成另一套视觉系统。'
|
|
2026
|
+
'字体使用 Typography 的完整栈与降级,不在运行时安装字体或依赖。',
|
|
2027
|
+
'7. **保持标题结构** —— 有合适页型可参考时,沿用该页型已有的标题层级与局部 '
|
|
2028
|
+
'`css`;只渲染该页型已有的文字槽,背景中已经可见的固定标题不再创建文本,'
|
|
2029
|
+
'页型没有 `subtitle` 槽就不新增副标题。没有合适参考时,按本包整体视觉组织标题。']
|
|
1846
2030
|
if assets:
|
|
1847
2031
|
L += ['', '资产文件(背景由页型的 `background` 字段指定,'
|
|
1848
2032
|
'图片资产的位置由该页型 `slots` 里带 `asset` 的槽给出):', '',
|
|
1849
|
-
'{{ASSET_TABLE}}'
|
|
2033
|
+
'{{ASSET_TABLE}}', '',
|
|
2034
|
+
'将包内 `assets/` 复制到项目内相对目录,再引用复制后的路径;最终 HTML 不引用'
|
|
2035
|
+
'抽取工作目录或本机绝对路径。附件只提供 `assetRoot` / `assetPaths` 时,把'
|
|
2036
|
+
'`assetRoot` 当作不透明前缀,只拼接清单中声明的相对路径。']
|
|
1850
2037
|
L += ['', '文字与容器的外接矩形落在该页型 `background` 对应的 `text_safe` 内,'
|
|
1851
2038
|
'避开 `avoid` 列出的区域(两者都在 %s 的 `backgrounds` 段)。内容装不下时换页型或拆页。'
|
|
1852
2039
|
% sidecar, '',
|
|
@@ -1861,7 +2048,8 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1861
2048
|
',源为商业/内部字体无 web 分发源,按气质降级到 %s' % f['stack'][1]
|
|
1862
2049
|
if len(f['stack']) > 1 else ''))
|
|
1863
2050
|
L += ['', '字号轴:' + '、'.join('%s %dpx' % (k, round(v['sz_px'])) for k, v in roles.items())
|
|
1864
|
-
+ '。slot 自带 `size`
|
|
2051
|
+
+ '。slot 自带 `css` 时以其中的 `font-size` 为准;没有 slot CSS 的新增层级,'
|
|
2052
|
+
'复用轴上最接近的一档。', '',
|
|
1865
2053
|
'字体加载(**HARD REQUIREMENT:下面这行 @import 原样写入全局样式首行,禁止替换为 '
|
|
1866
2054
|
'fonts.googleapis.com 或其他域**):', '', '```', imp, '```', '',
|
|
1867
2055
|
'镜像只保证 wght 400 一档,更粗的字重由浏览器合成,字重不能作为唯一区分手段;'
|
|
@@ -1884,7 +2072,14 @@ def emit_body(d, tokens, fonts, roles, assets, archetypes, exceptions, cusage, l
|
|
|
1884
2072
|
% (logo['id'], '、'.join('`%s`' % x for x in with_logo) or '(无)'))
|
|
1885
2073
|
L += ['- 坐标、字号、色值、资产位置以 %s 为准;本文件的 Colors / Typography 是可用值的清单。'
|
|
1886
2074
|
% sidecar,
|
|
1887
|
-
'-
|
|
2075
|
+
'- 强调色族以 Colors 和 %s 的 slot CSS 为准,不得自行新增第二强调色。'
|
|
2076
|
+
% sidecar,
|
|
2077
|
+
'- 允许新增中性色、低彩度辅助色或局部语义色来表达正负、风险、警告、状态、图表序列,'
|
|
2078
|
+
'但必须保持辅助层级;只要新色通过高饱和、高对比、大面积或跨页重复获得主视觉权重,'
|
|
2079
|
+
'或被用于标题、关键数字、图表主序列、卡片底色或渐变,就属于新的强调色,改用模板'
|
|
2080
|
+
'强调色族的深浅、透明度,或改用线型、纹理、标签区分。',
|
|
2081
|
+
'- 交付前逐页检查:色板、字体、版式、背景、资产和本段规则均来自本风格包;'
|
|
2082
|
+
'页面无资源加载失败、内容溢出或画幅裁切。',
|
|
1888
2083
|
'- 本包里的数值就是普查结果,照用即可,无需重新统计颜色、字体或版式。',
|
|
1889
2084
|
'- 风格包以文本形式(zip 摘要等)到手时,直接用摘要里 design.md / layouts.md 的文本。',
|
|
1890
2085
|
'', '## Exceptions', '']
|
|
@@ -1974,7 +2169,7 @@ def emit_brief(d, ctx, ldir):
|
|
|
1974
2169
|
if leftover:
|
|
1975
2170
|
L += ['', '未归入 archetype 的页:%s —— 都是单页孤例,需要就自己补一个 archetype。'
|
|
1976
2171
|
% ', '.join(map(str, leftover))]
|
|
1977
|
-
L += ['', '各 archetype 的 slot
|
|
2172
|
+
L += ['', '各 archetype 的 slot 原文(据此起中文页型名,并在 text_roles 判断文本角色):', '']
|
|
1978
2173
|
for a in archetypes:
|
|
1979
2174
|
L.append('- `%s`(第 %s 页,覆盖 %s)' % (a['name'], a['rep'], a['pages']))
|
|
1980
2175
|
for s in a['slots']:
|
|
@@ -2124,7 +2319,7 @@ def main(argv=None):
|
|
|
2124
2319
|
gaps.append('源字体 %s 不在 font-fallback 表里,字体栈只有原名,消费端很可能装不上;'
|
|
2125
2320
|
'按气质挑一个有 web 分发源的近似体补进栈,不要照抄原名。' % f['names'][0])
|
|
2126
2321
|
nosize = [(a['name'], s['box']) for a in archetypes for s in a['slots']
|
|
2127
|
-
if not s.get('asset') and not s.get('
|
|
2322
|
+
if not s.get('asset') and not s.get('_font_size')]
|
|
2128
2323
|
if nosize:
|
|
2129
2324
|
gaps.append('这些文字槽在源文件任何层级都没有字号声明(都不是占位符,是普通文本框,'
|
|
2130
2325
|
'继承源是 presentation.xml 的 defaultTextStyle,本抽取按约定不解继承链):'
|