@lark-apaas/coding-steering 0.1.18-dev.ee5404f → 0.1.18-dev.fb5e270
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/charts/SKILL.md +4 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +8 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +548 -108
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +18 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +235 -26
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +161 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_color_contract.py +3 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +1 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +468 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +407 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +40 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +4 -3
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +10 -0
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
- package/steering/nestjs-react-fullstack/skills_local/plugin-guide/SKILL.md +4 -0
- package/steering/vite-react/skills/plugin-guide/SKILL.md +3 -1
- package/steering/vite-react/skills/react-three-fiber/SKILL.md +4 -0
|
@@ -519,8 +519,25 @@ def read_txbody(tx, ctx, kind='txBody'):
|
|
|
519
519
|
ins[k] = ctx.units.px(bp.get(k))
|
|
520
520
|
if ins:
|
|
521
521
|
b['insets_px'] = ins
|
|
522
|
-
|
|
522
|
+
na = bp.find('a:normAutofit', NS)
|
|
523
|
+
if na is not None:
|
|
523
524
|
b['autofit'] = 'norm'
|
|
525
|
+
# normAutofit 用 fontScale / lnSpcReduction(单位 1/1000 %)把大字缩进小框——
|
|
526
|
+
# 章节大号数字就靠它让 160px 的字装进 144px 的框。只记 autofit 存在、丢掉
|
|
527
|
+
# fontScale,消费端就拿到未缩放字号 + 原始框高,字比框高,渐变裁切把溢出的
|
|
528
|
+
# 底部切成透明。缺省即 100%(无缩放)。
|
|
529
|
+
fs = na.get('fontScale')
|
|
530
|
+
if fs is not None:
|
|
531
|
+
try:
|
|
532
|
+
b['font_scale'] = round(int(fs) / 100000.0, 4)
|
|
533
|
+
except (TypeError, ValueError):
|
|
534
|
+
pass
|
|
535
|
+
lsr = na.get('lnSpcReduction')
|
|
536
|
+
if lsr is not None:
|
|
537
|
+
try:
|
|
538
|
+
b['ln_spc_reduction'] = round(int(lsr) / 100000.0, 4)
|
|
539
|
+
except (TypeError, ValueError):
|
|
540
|
+
pass
|
|
524
541
|
elif bp.find('a:spAutoFit', NS) is not None:
|
|
525
542
|
b['autofit'] = 'shape'
|
|
526
543
|
if b:
|
|
@@ -235,11 +235,13 @@ def parse_item_list(lines):
|
|
|
235
235
|
|
|
236
236
|
def read_manifest(path):
|
|
237
237
|
blocks = split_top_blocks(open(path, encoding='utf-8').read())
|
|
238
|
-
man, assets, raw = {}, [], {}
|
|
238
|
+
man, assets, asset_decisions, raw = {}, [], [], {}
|
|
239
239
|
derived = []
|
|
240
240
|
for key, inline, lines in blocks:
|
|
241
241
|
if key == 'assets':
|
|
242
242
|
assets = parse_item_list(lines)
|
|
243
|
+
elif key == 'asset_decisions':
|
|
244
|
+
asset_decisions = parse_item_list(lines)
|
|
243
245
|
elif key == 'derived':
|
|
244
246
|
derived = parse_item_list(lines)
|
|
245
247
|
elif inline.startswith('['):
|
|
@@ -253,6 +255,7 @@ def read_manifest(path):
|
|
|
253
255
|
# 不把作者的折行改成一条长行。
|
|
254
256
|
raw[key] = (inline, lines)
|
|
255
257
|
man['assets'] = assets
|
|
258
|
+
man['asset_decisions'] = asset_decisions
|
|
256
259
|
man['derived'] = derived
|
|
257
260
|
man['_raw'] = raw
|
|
258
261
|
return man
|
|
@@ -291,6 +294,68 @@ def strip_kind_prefix(aid, kind):
|
|
|
291
294
|
return aid
|
|
292
295
|
|
|
293
296
|
|
|
297
|
+
def apply_asset_decisions(manifest, bound_sources=None):
|
|
298
|
+
"""把临时图片判断并入正式 assets;content 只用于删除候选,不进入消费产物。"""
|
|
299
|
+
allowed = {'content', 'texture', 'logo', 'icon', 'slogan'}
|
|
300
|
+
decisions = {}
|
|
301
|
+
for item in manifest.get('asset_decisions') or []:
|
|
302
|
+
source = unquote(item.get('source_media') or '')
|
|
303
|
+
decision = unquote(item.get('decision') or '')
|
|
304
|
+
if not source or decision not in allowed:
|
|
305
|
+
raise Fail('asset_decisions 必须逐项填写 source_media 和 decision;'
|
|
306
|
+
'decision 只能是 %s' % '|'.join(sorted(allowed)))
|
|
307
|
+
decisions[source] = decision
|
|
308
|
+
if bound_sources is not None:
|
|
309
|
+
unbound = sorted(set(decisions) - set(bound_sources))
|
|
310
|
+
if unbound:
|
|
311
|
+
raise Fail('asset_decisions 中这些图片没有对应图片槽:%s'
|
|
312
|
+
% '、'.join(unbound))
|
|
313
|
+
|
|
314
|
+
assets = []
|
|
315
|
+
by_source = {}
|
|
316
|
+
serial = Counter()
|
|
317
|
+
used_ids = {unquote(item.get('id') or '')
|
|
318
|
+
for item in manifest.get('assets') or [] if item.get('id')}
|
|
319
|
+
for item in manifest.get('assets') or []:
|
|
320
|
+
source = unquote(item.get('source_media') or '')
|
|
321
|
+
if source and decisions.get(source) == 'content':
|
|
322
|
+
continue
|
|
323
|
+
row = dict(item)
|
|
324
|
+
decision = decisions.get(source)
|
|
325
|
+
old_kind = unquote(row.get('kind') or '')
|
|
326
|
+
if decision:
|
|
327
|
+
row['kind'] = decision
|
|
328
|
+
if decision != old_kind:
|
|
329
|
+
serial[decision] += 1
|
|
330
|
+
aid = '%s-%d' % (KIND_PREFIX[decision], serial[decision])
|
|
331
|
+
while aid in used_ids:
|
|
332
|
+
serial[decision] += 1
|
|
333
|
+
aid = '%s-%d' % (KIND_PREFIX[decision], serial[decision])
|
|
334
|
+
row['id'] = aid
|
|
335
|
+
for field in ('role', 'theme', 'on-bg', 'mark'):
|
|
336
|
+
row.pop(field, None)
|
|
337
|
+
used_ids.add(unquote(row.get('id') or ''))
|
|
338
|
+
assets.append(row)
|
|
339
|
+
if source:
|
|
340
|
+
by_source[source] = row
|
|
341
|
+
|
|
342
|
+
for source, decision in decisions.items():
|
|
343
|
+
if decision == 'content' or source in by_source:
|
|
344
|
+
continue
|
|
345
|
+
serial[decision] += 1
|
|
346
|
+
prefix = KIND_PREFIX[decision]
|
|
347
|
+
aid = '%s-%d' % (prefix, serial[decision])
|
|
348
|
+
while aid in used_ids:
|
|
349
|
+
serial[decision] += 1
|
|
350
|
+
aid = '%s-%d' % (prefix, serial[decision])
|
|
351
|
+
used_ids.add(aid)
|
|
352
|
+
row = {'id': aid, 'source_media': source, 'kind': decision}
|
|
353
|
+
assets.append(row)
|
|
354
|
+
by_source[source] = row
|
|
355
|
+
manifest['assets'] = assets
|
|
356
|
+
return decisions, {source: unquote(item['id']) for source, item in by_source.items()}
|
|
357
|
+
|
|
358
|
+
|
|
294
359
|
def media_row_of(extract, source_media):
|
|
295
360
|
for m in extract.get('media') or []:
|
|
296
361
|
if m.get('out') and os.path.basename(m['out']) == source_media:
|
|
@@ -475,6 +540,16 @@ def expand_placeholders(body, manifest, consumer, audit, extract, layouts_text):
|
|
|
475
540
|
f += '(原图 `%s`)' % e['full']
|
|
476
541
|
rows.append('| `%s` | %s | %s |' % (aid, f, when))
|
|
477
542
|
body = body.replace('{{ASSET_TABLE}}', '\n'.join(rows))
|
|
543
|
+
if '{{LOGO_RULES}}' in body:
|
|
544
|
+
rows = []
|
|
545
|
+
for aid, entry in consumer.items():
|
|
546
|
+
if entry.get('kind') != 'logo':
|
|
547
|
+
continue
|
|
548
|
+
owners = _archetypes_using(layouts_text, aid)
|
|
549
|
+
rows.append('- `%s` 只出现在这些页型上:%s;其余页型不放。位置取该页型 '
|
|
550
|
+
'`slots` 里 `role: logo` 那一项的 `box`,原样使用该文件、保持原比例。'
|
|
551
|
+
% (aid, '、'.join('`%s`' % owner for owner in owners) or '(无)'))
|
|
552
|
+
body = body.replace('{{LOGO_RULES}}', '\n'.join(rows))
|
|
478
553
|
if '{{LAYOUT_LIST}}' in body:
|
|
479
554
|
rows, sect, names = [], None, {}
|
|
480
555
|
for line in (layouts_text or '').split('\n'):
|
|
@@ -903,6 +978,64 @@ def build_design(manifest, l_frontmatter, consumer, body, has_sidecar, canvas):
|
|
|
903
978
|
return text
|
|
904
979
|
|
|
905
980
|
|
|
981
|
+
def layout_forms(lines):
|
|
982
|
+
"""每个页型草案里实际给了哪几种形态(flow / slots)。"""
|
|
983
|
+
out, cur = {}, None
|
|
984
|
+
for line in lines:
|
|
985
|
+
m = re.match(r'^ ([\w-]+):\s*$', line)
|
|
986
|
+
if m:
|
|
987
|
+
cur = m.group(1)
|
|
988
|
+
out[cur] = set()
|
|
989
|
+
continue
|
|
990
|
+
m = re.match(r'^ (flow|slots):\s*$', line)
|
|
991
|
+
if m and cur:
|
|
992
|
+
out[cur].add(m.group(1))
|
|
993
|
+
return out
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def layout_modes(lines):
|
|
997
|
+
"""读取扁平 `layout_modes:` 判断区。"""
|
|
998
|
+
out = {}
|
|
999
|
+
for line in lines:
|
|
1000
|
+
m = re.match(r'^\s{2}([\w-]+):\s*([^\s#]+)', line)
|
|
1001
|
+
if m:
|
|
1002
|
+
out[m.group(1)] = m.group(2).strip()
|
|
1003
|
+
return out
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def text_role_boxes(lines):
|
|
1007
|
+
"""从 slots 里的同名标记取回文本槽坐标,供 flow 固定锚点复用。"""
|
|
1008
|
+
out, pending = {}, None
|
|
1009
|
+
for line in lines:
|
|
1010
|
+
marker = re.match(r'^\s*#\s*text-role:\s*([\w-]+)\s*$', line)
|
|
1011
|
+
if marker:
|
|
1012
|
+
pending = marker.group(1)
|
|
1013
|
+
continue
|
|
1014
|
+
if pending:
|
|
1015
|
+
box = re.search(r'\bbox:\s*(\[[^\]]+\])', line)
|
|
1016
|
+
if box:
|
|
1017
|
+
out[pending] = box.group(1)
|
|
1018
|
+
pending = None
|
|
1019
|
+
return out
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def select_layout_forms(lines, modes):
|
|
1023
|
+
"""按 `layout_modes` 只保留每个页型选中的 flow 或 slots。"""
|
|
1024
|
+
out, layout, drop = [], None, False
|
|
1025
|
+
for line in lines:
|
|
1026
|
+
layout_match = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1027
|
+
form_match = re.match(r'^ (flow|slots):\s*$', line)
|
|
1028
|
+
if layout_match:
|
|
1029
|
+
layout, drop = layout_match.group(1), False
|
|
1030
|
+
elif form_match:
|
|
1031
|
+
drop = modes.get(layout) not in (None, form_match.group(1))
|
|
1032
|
+
elif drop and re.match(r'^ \S', line):
|
|
1033
|
+
drop = False
|
|
1034
|
+
if not drop:
|
|
1035
|
+
out.append(line)
|
|
1036
|
+
return out
|
|
1037
|
+
|
|
1038
|
+
|
|
906
1039
|
def shrink_safe_area(lines):
|
|
907
1040
|
"""text_safe 与 avoid 相交时把安全区收掉重叠的那部分。
|
|
908
1041
|
|
|
@@ -955,13 +1088,46 @@ def shrink_safe_area(lines):
|
|
|
955
1088
|
return lines, None
|
|
956
1089
|
|
|
957
1090
|
|
|
958
|
-
def
|
|
1091
|
+
def resolve_asset_candidate_lines(lines, decisions, asset_ids):
|
|
1092
|
+
"""把 source_media 临时字段转成资产引用;内容图保留通用 pic 槽。"""
|
|
1093
|
+
out = []
|
|
1094
|
+
content_slots = set()
|
|
1095
|
+
for line in lines:
|
|
1096
|
+
match = re.search(r'\bsource_media:\s*([^,}]+)', line)
|
|
1097
|
+
if not match:
|
|
1098
|
+
out.append(line)
|
|
1099
|
+
continue
|
|
1100
|
+
source = unquote(match.group(1).strip())
|
|
1101
|
+
decision = decisions.get(source)
|
|
1102
|
+
line = re.sub(r'\s*,?\s*source_media:\s*[^,}]+', '', line, count=1)
|
|
1103
|
+
if decision == 'content':
|
|
1104
|
+
line = re.sub(r'\s*,?\s*asset:\s*[^,}]+', '', line, count=1)
|
|
1105
|
+
line = re.sub(r'(\brole:\s*)[\w-]+', r'\g<1>pic', line, count=1)
|
|
1106
|
+
content_key = line.strip()
|
|
1107
|
+
if content_key in content_slots:
|
|
1108
|
+
continue
|
|
1109
|
+
content_slots.add(content_key)
|
|
1110
|
+
out.append(line)
|
|
1111
|
+
continue
|
|
1112
|
+
aid = asset_ids.get(source)
|
|
1113
|
+
if not aid:
|
|
1114
|
+
raise Fail('layouts.yaml 引用了未完成资产判断的 source_media: %s' % source)
|
|
1115
|
+
if re.search(r'\basset:\s*[^,}]+', line):
|
|
1116
|
+
line = re.sub(r'(\basset:\s*)[^,}]+', r'\g<1>%s' % aid, line, count=1)
|
|
1117
|
+
else:
|
|
1118
|
+
line = line.replace('}', ', asset: %s}' % aid, 1)
|
|
1119
|
+
line = re.sub(r'(\brole:\s*)[\w-]+', r'\g<1>%s' % decision, line, count=1)
|
|
1120
|
+
out.append(line)
|
|
1121
|
+
return out
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=None):
|
|
959
1125
|
blocks = {k: (inline, lines) for k, inline, lines in layouts_blocks}
|
|
960
1126
|
if 'canvas' in blocks:
|
|
961
1127
|
raise Fail('layouts.yaml 不要写 canvas —— 脚本从 extract.json 取')
|
|
962
1128
|
if 'layouts' not in blocks:
|
|
963
1129
|
raise Fail('layouts.yaml 缺顶层键 `layouts:`')
|
|
964
|
-
# `names:` / `roles:` / `text_roles:` / `bg_rules:` 是给 L 层集中填判断的
|
|
1130
|
+
# `names:` / `roles:` / `text_roles:` / `layout_modes:` / `bg_rules:` 是给 L 层集中填判断的
|
|
965
1131
|
# 扁平区——在这里并回各 archetype,本身不进产物。让 L 层只改扁平键值,别去动 layouts 里的
|
|
966
1132
|
# slots/confidence 结构(嵌套结构手改极易破坏缩进,进而静默改变语义)。
|
|
967
1133
|
names, roles = {}, {}
|
|
@@ -1002,12 +1168,33 @@ def build_layouts_md(layouts_blocks, canvas):
|
|
|
1002
1168
|
raise Fail('%s: %s' % (bg, err))
|
|
1003
1169
|
out.append(' %s:' % bg)
|
|
1004
1170
|
out += lines
|
|
1171
|
+
modes = layout_modes(blocks.get('layout_modes', ('', []))[1])
|
|
1005
1172
|
out.append('layouts:')
|
|
1006
1173
|
pending_text_role = None
|
|
1007
1174
|
used_text_roles = set()
|
|
1008
|
-
|
|
1175
|
+
source_layout_lines = resolve_asset_candidate_lines(
|
|
1176
|
+
blocks['layouts'][1], asset_decisions or {}, asset_ids or {})
|
|
1177
|
+
role_boxes = text_role_boxes(source_layout_lines)
|
|
1178
|
+
layout_lines = select_layout_forms(source_layout_lines, modes)
|
|
1179
|
+
cur, fixed_items = None, []
|
|
1180
|
+
|
|
1181
|
+
def flush_fixed_items():
|
|
1182
|
+
if not fixed_items:
|
|
1183
|
+
return
|
|
1184
|
+
out.append(' - kind: free')
|
|
1185
|
+
out.append(' items:')
|
|
1186
|
+
out.extend(fixed_items)
|
|
1187
|
+
fixed_items.clear()
|
|
1188
|
+
|
|
1189
|
+
for line in layout_lines:
|
|
1190
|
+
m = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1191
|
+
if m:
|
|
1192
|
+
flush_fixed_items()
|
|
1193
|
+
cur = m.group(1)
|
|
1194
|
+
elif fixed_items and re.match(r'^ \S', line):
|
|
1195
|
+
flush_fixed_items()
|
|
1009
1196
|
# 判断单里的结构事实(栅格、间距序列、样张字数、命中配方)是给 L 层判断用的,
|
|
1010
|
-
# 不进产物——消费端要的是结论,不是推导过程。
|
|
1197
|
+
# 不进产物——消费端要的是结论,不是推导过程。layout_mode 同理,它是判断的载体。
|
|
1011
1198
|
if line.lstrip().startswith('#'):
|
|
1012
1199
|
marker = re.match(r'^\s*#\s*text-role:\s*([\w-]+)\s*$', line)
|
|
1013
1200
|
if marker:
|
|
@@ -1017,21 +1204,32 @@ def build_layouts_md(layouts_blocks, canvas):
|
|
|
1017
1204
|
if pending_text_role not in text_roles:
|
|
1018
1205
|
raise Fail('text_roles 缺少 %s 的判断' % pending_text_role)
|
|
1019
1206
|
role = text_roles[pending_text_role]
|
|
1020
|
-
slot_type = role if role in ('title', 'subtitle', 'footer') else 'body'
|
|
1207
|
+
slot_type = role if role in ('title', 'subtitle', 'header', 'footer') else 'body'
|
|
1021
1208
|
line, role_n = re.subn(r'(\{\s*role:\s*)[\w-]+',
|
|
1022
1209
|
r'\g<1>%s' % role, line, count=1)
|
|
1023
1210
|
line, type_n = re.subn(r'(\btype:\s*)[\w-]+',
|
|
1024
1211
|
r'\g<1>%s' % slot_type, line, count=1)
|
|
1025
1212
|
if role_n != 1 or type_n != 1:
|
|
1026
1213
|
raise Fail('text_roles.%s 没有命中一个文本槽' % pending_text_role)
|
|
1214
|
+
if (modes.get(cur) == 'flow' and role in ('header', 'footer')
|
|
1215
|
+
and re.match(r'^\s{12}-\s*\{', line) and 'box:' not in line):
|
|
1216
|
+
box = role_boxes.get(pending_text_role)
|
|
1217
|
+
if not box:
|
|
1218
|
+
raise Fail('text_roles.%s 识别为 %s,但 slots 中没有坐标'
|
|
1219
|
+
% (pending_text_role, role))
|
|
1220
|
+
line = re.sub(r',\s*type:', ', box: %s, type:' % box, line, count=1)
|
|
1221
|
+
fixed_items.append(line)
|
|
1222
|
+
used_text_roles.add(pending_text_role)
|
|
1223
|
+
pending_text_role = None
|
|
1224
|
+
continue
|
|
1027
1225
|
used_text_roles.add(pending_text_role)
|
|
1028
1226
|
pending_text_role = None
|
|
1029
1227
|
out.append(line)
|
|
1030
|
-
m = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1031
1228
|
if m and m.group(1) in names:
|
|
1032
1229
|
out.append(' name: "%s"' % names.pop(m.group(1)))
|
|
1033
1230
|
if m and m.group(1) in roles:
|
|
1034
1231
|
out.append(' role: %s' % roles.pop(m.group(1)))
|
|
1232
|
+
flush_fixed_items()
|
|
1035
1233
|
if names:
|
|
1036
1234
|
raise Fail('names 里这些页型在 layouts 下找不到:%s' % ', '.join(sorted(names)))
|
|
1037
1235
|
if roles:
|
|
@@ -1045,23 +1243,22 @@ def build_layouts_md(layouts_blocks, canvas):
|
|
|
1045
1243
|
'\n'.join(out), re.M)]
|
|
1046
1244
|
if missing_role:
|
|
1047
1245
|
raise Fail('这些页型没有 role(在 layouts.yaml 的 roles 段填):%s' % ', '.join(missing_role))
|
|
1048
|
-
#
|
|
1049
|
-
#
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
raise Fail('这些页型同时留了 flow 和 slots,二选一删掉另一个:%s' % ', '.join(both))
|
|
1246
|
+
# `layout_modes.*: TODO` 由上面那道通用 TODO 扫描报(它连行内提示一起打出来),
|
|
1247
|
+
# 这里只管它管不到的两种:整行被删、以及填了 flow/slots 之外的值。
|
|
1248
|
+
forms = layout_forms(blocks['layouts'][1])
|
|
1249
|
+
bad = []
|
|
1250
|
+
for k, v in sorted(modes.items()):
|
|
1251
|
+
if len(forms.get(k) or ()) < 2:
|
|
1252
|
+
continue # 只有一份形态,无从选择
|
|
1253
|
+
if v not in ('flow', 'slots'):
|
|
1254
|
+
bad.append('%s 的 layout_modes 判断是 %r' % (k, v))
|
|
1255
|
+
elif v not in forms[k]:
|
|
1256
|
+
bad.append('%s 选了 %s 但该页型没有这一份' % (k, v))
|
|
1257
|
+
for k, forms_for_layout in sorted(forms.items()):
|
|
1258
|
+
if len(forms_for_layout) >= 2 and k not in modes:
|
|
1259
|
+
bad.append('%s 缺 layout_modes 判断' % k)
|
|
1260
|
+
if bad:
|
|
1261
|
+
raise Fail('layout_modes 只能填 flow 或 slots,一个页型一个词:%s' % ';'.join(bad))
|
|
1065
1262
|
declared = set(re.findall(r'^ background:\s*(\S+)\s*$',
|
|
1066
1263
|
'\n'.join(blocks['layouts'][1]), re.M))
|
|
1067
1264
|
stray = set(bg_rules) - declared
|
|
@@ -1144,12 +1341,24 @@ def main(argv=None):
|
|
|
1144
1341
|
lay_path = os.path.join(lout, 'layouts.yaml')
|
|
1145
1342
|
layouts_blocks = (split_top_blocks(open(lay_path, encoding='utf-8').read())
|
|
1146
1343
|
if os.path.exists(lay_path) else None)
|
|
1344
|
+
bound_sources = set()
|
|
1345
|
+
if layouts_blocks is not None:
|
|
1346
|
+
for _, _, lines in layouts_blocks:
|
|
1347
|
+
for line in lines:
|
|
1348
|
+
match = re.search(r'\bsource_media:\s*([^,}]+)', line)
|
|
1349
|
+
if match:
|
|
1350
|
+
bound_sources.add(unquote(match.group(1).strip()))
|
|
1351
|
+
asset_decisions, asset_ids = apply_asset_decisions(
|
|
1352
|
+
manifest, bound_sources=bound_sources if manifest.get('asset_decisions') else None)
|
|
1147
1353
|
canvas = extract['canvas']['px']
|
|
1354
|
+
final_layouts = (build_layouts_md(
|
|
1355
|
+
layouts_blocks, canvas, asset_decisions=asset_decisions, asset_ids=asset_ids)
|
|
1356
|
+
if layouts_blocks is not None else None)
|
|
1148
1357
|
|
|
1149
1358
|
os.makedirs(pack, exist_ok=True)
|
|
1150
1359
|
consumer, audit, copied = place_assets(manifest, extract, stage1, pack)
|
|
1151
1360
|
|
|
1152
|
-
lay_text =
|
|
1361
|
+
lay_text = final_layouts or ''
|
|
1153
1362
|
body = expand_placeholders(body, manifest, consumer, audit, extract, lay_text)
|
|
1154
1363
|
design = build_design(manifest, l_fm, consumer, body,
|
|
1155
1364
|
has_sidecar=layouts_blocks is not None, canvas=canvas)
|
|
@@ -1157,7 +1366,7 @@ def main(argv=None):
|
|
|
1157
1366
|
f.write(design)
|
|
1158
1367
|
if layouts_blocks is not None:
|
|
1159
1368
|
with open(os.path.join(pack, 'layouts.md'), 'w', encoding='utf-8') as f:
|
|
1160
|
-
f.write(
|
|
1369
|
+
f.write(final_layouts)
|
|
1161
1370
|
|
|
1162
1371
|
# 审计记录写回**抽取工作目录**,不进交付包:包是消费产物,`ref/` 里的东西
|
|
1163
1372
|
# 没有任何门禁读、消费模型也用不上,放进去只会让「别读我」和「几 MB 材料」同时下发。
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Integration regression tests for model-decided image roles."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
10
|
+
|
|
11
|
+
from check_v2 import Pack, RULES # noqa: E402
|
|
12
|
+
from package import main as package_main # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AssetJudgmentPackageTest(unittest.TestCase):
|
|
16
|
+
def test_package_strips_judgment_fields_and_preserves_content_slot(self):
|
|
17
|
+
try:
|
|
18
|
+
from PIL import Image
|
|
19
|
+
except ImportError:
|
|
20
|
+
self.skipTest('Pillow unavailable')
|
|
21
|
+
|
|
22
|
+
with tempfile.TemporaryDirectory() as root:
|
|
23
|
+
stage1 = os.path.join(root, 'stage1')
|
|
24
|
+
lout = os.path.join(stage1, 'l-out')
|
|
25
|
+
pack = os.path.join(root, 'pack')
|
|
26
|
+
media = os.path.join(stage1, 'media-out')
|
|
27
|
+
ref = os.path.join(stage1, 'ref')
|
|
28
|
+
os.makedirs(lout)
|
|
29
|
+
os.makedirs(media)
|
|
30
|
+
os.makedirs(ref)
|
|
31
|
+
|
|
32
|
+
Image.new('RGBA', (40, 20), (20, 40, 60, 255)).save(
|
|
33
|
+
os.path.join(media, 'partner-logo.png'))
|
|
34
|
+
Image.new('RGBA', (24, 24), (60, 40, 20, 255)).save(
|
|
35
|
+
os.path.join(media, 'ornament.png'))
|
|
36
|
+
|
|
37
|
+
box_rows = [
|
|
38
|
+
('ppt/media/image1.png', 'partner-logo.png', [100, 200, 400, 200]),
|
|
39
|
+
('ppt/media/image2.png', 'ornament.png', [40, 40, 120, 120]),
|
|
40
|
+
]
|
|
41
|
+
extract = {
|
|
42
|
+
'source': {'filename': 'fixture.pptx'},
|
|
43
|
+
'canvas': {
|
|
44
|
+
'px': [1920, 1080],
|
|
45
|
+
'source': {'cx': 12192000, 'cy': 6858000, 'unit': 'EMU'},
|
|
46
|
+
},
|
|
47
|
+
'media': [
|
|
48
|
+
{
|
|
49
|
+
'media': source,
|
|
50
|
+
'out': 'media-out/' + output,
|
|
51
|
+
'source_px': [40, 20] if output == 'partner-logo.png' else [24, 24],
|
|
52
|
+
}
|
|
53
|
+
for source, output, _ in box_rows
|
|
54
|
+
],
|
|
55
|
+
'images': [
|
|
56
|
+
{
|
|
57
|
+
'media': source,
|
|
58
|
+
'boxes': [{
|
|
59
|
+
'count': 1,
|
|
60
|
+
'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
|
|
61
|
+
'parts': ['ppt/slides/slide1.xml'],
|
|
62
|
+
}],
|
|
63
|
+
}
|
|
64
|
+
for source, _, box in box_rows
|
|
65
|
+
],
|
|
66
|
+
'color_freq': [],
|
|
67
|
+
'text_scale': [],
|
|
68
|
+
}
|
|
69
|
+
with open(os.path.join(stage1, 'extract.json'), 'w', encoding='utf-8') as stream:
|
|
70
|
+
json.dump(extract, stream)
|
|
71
|
+
with open(os.path.join(ref, 'shapes.json'), 'w', encoding='utf-8') as stream:
|
|
72
|
+
json.dump({
|
|
73
|
+
'shapes': [
|
|
74
|
+
{
|
|
75
|
+
'part': 'ppt/slides/slide1.xml',
|
|
76
|
+
'kind': 'pic',
|
|
77
|
+
'name': output,
|
|
78
|
+
'box': {'x': box[0], 'y': box[1], 'w': box[2], 'h': box[3]},
|
|
79
|
+
}
|
|
80
|
+
for _, output, box in box_rows
|
|
81
|
+
],
|
|
82
|
+
}, stream)
|
|
83
|
+
|
|
84
|
+
with open(os.path.join(lout, 'manifest.yaml'), 'w', encoding='utf-8') as stream:
|
|
85
|
+
stream.write(
|
|
86
|
+
'version: alpha\n'
|
|
87
|
+
'name: asset-judgment-fixture\n'
|
|
88
|
+
'description: >\n'
|
|
89
|
+
' A restrained fixture with explicit image-role judgments and traced layout slots.\n'
|
|
90
|
+
'assets:\n'
|
|
91
|
+
' - id: logo-primary\n'
|
|
92
|
+
' source_media: partner-logo.png\n'
|
|
93
|
+
' kind: logo\n'
|
|
94
|
+
' on-bg: light\n'
|
|
95
|
+
'asset_decisions:\n'
|
|
96
|
+
' - source_media: partner-logo.png\n'
|
|
97
|
+
' decision: content\n'
|
|
98
|
+
' - source_media: ornament.png\n'
|
|
99
|
+
' decision: texture\n'
|
|
100
|
+
)
|
|
101
|
+
with open(os.path.join(lout, 'frontmatter.yaml'), 'w', encoding='utf-8') as stream:
|
|
102
|
+
stream.write('')
|
|
103
|
+
with open(os.path.join(lout, 'layouts.yaml'), 'w', encoding='utf-8') as stream:
|
|
104
|
+
stream.write(
|
|
105
|
+
'layouts:\n'
|
|
106
|
+
' content:\n'
|
|
107
|
+
' name: "内容页"\n'
|
|
108
|
+
' role: content\n'
|
|
109
|
+
' slots:\n'
|
|
110
|
+
' - {role: logo, box: [100, 200, 400, 200], type: pic, '
|
|
111
|
+
'asset: logo-primary, source_media: partner-logo.png}\n'
|
|
112
|
+
' - {role: asset-candidate, box: [40, 40, 120, 120], type: pic, '
|
|
113
|
+
'source_media: ornament.png}\n'
|
|
114
|
+
' confidence: high\n'
|
|
115
|
+
)
|
|
116
|
+
with open(os.path.join(lout, 'body.md'), 'w', encoding='utf-8') as stream:
|
|
117
|
+
stream.write(
|
|
118
|
+
'## Usage\n\n'
|
|
119
|
+
'Read `layouts.md` before composing a slide.\n\n'
|
|
120
|
+
'## Assets\n\n'
|
|
121
|
+
'{{ASSET_TABLE}}\n\n'
|
|
122
|
+
'## Hard Rules\n\n'
|
|
123
|
+
'{{LOGO_RULES}}\n'
|
|
124
|
+
)
|
|
125
|
+
check_v1 = os.path.join(root, 'check_v1.py')
|
|
126
|
+
with open(check_v1, 'w', encoding='utf-8') as stream:
|
|
127
|
+
stream.write('raise SystemExit(0)\n')
|
|
128
|
+
|
|
129
|
+
rc = package_main([stage1, lout, pack, '--check-v1', check_v1])
|
|
130
|
+
|
|
131
|
+
self.assertEqual(rc, 0)
|
|
132
|
+
with open(os.path.join(pack, 'design.md'), encoding='utf-8') as stream:
|
|
133
|
+
design = stream.read()
|
|
134
|
+
with open(os.path.join(pack, 'layouts.md'), encoding='utf-8') as stream:
|
|
135
|
+
layouts = stream.read()
|
|
136
|
+
self.assertNotIn('asset_decisions', design)
|
|
137
|
+
self.assertNotIn('source_media', design)
|
|
138
|
+
self.assertNotIn('source_media', layouts)
|
|
139
|
+
self.assertNotIn('{{LOGO_RULES}}', design)
|
|
140
|
+
self.assertNotIn('logo-primary', design)
|
|
141
|
+
self.assertNotIn('logo-primary', layouts)
|
|
142
|
+
self.assertIn(
|
|
143
|
+
'- {role: pic, box: [100, 200, 400, 200], type: pic}',
|
|
144
|
+
layouts,
|
|
145
|
+
)
|
|
146
|
+
self.assertIn('texture-1:', design)
|
|
147
|
+
self.assertIn('kind: texture', design)
|
|
148
|
+
self.assertIn(
|
|
149
|
+
'- {role: texture, box: [40, 40, 120, 120], type: pic, asset: texture-1}',
|
|
150
|
+
layouts,
|
|
151
|
+
)
|
|
152
|
+
gate = Pack(pack)
|
|
153
|
+
self.assertFalse([
|
|
154
|
+
result
|
|
155
|
+
for result in (rule(gate) for rule in RULES)
|
|
156
|
+
if result.fails
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == '__main__':
|
|
161
|
+
unittest.main()
|
|
@@ -49,7 +49,9 @@ class ColorContractTest(unittest.TestCase):
|
|
|
49
49
|
|
|
50
50
|
self.assertIn('允许新增中性色、低彩度辅助色或局部语义色', body)
|
|
51
51
|
self.assertRegex(body, r'正负.*风险.*警告.*状态.*图表序列')
|
|
52
|
-
self.assertIn('
|
|
52
|
+
self.assertIn('必要时可以使用 Colors 之外的颜色', body)
|
|
53
|
+
self.assertIn('不能形成与模板主色竞争的第二强调色', body)
|
|
54
|
+
self.assertIn('色相、明度和饱和度关系', body)
|
|
53
55
|
self.assertIn('高饱和、高对比、大面积或跨页重复', body)
|
|
54
56
|
self.assertIn('标题、关键数字、图表主序列、卡片底色或渐变', body)
|
|
55
57
|
|
package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py
CHANGED
|
@@ -56,6 +56,7 @@ class DesignConsumerContractTest(unittest.TestCase):
|
|
|
56
56
|
self.assertIn('沿用该页型已有的标题层级与局部 `css`', body)
|
|
57
57
|
self.assertIn('背景中已经可见的固定标题不再创建文本', body)
|
|
58
58
|
self.assertIn('没有 `subtitle` 槽就不新增副标题', body)
|
|
59
|
+
self.assertIn('区带自带 `margin: [左, 右]` 时用它的', body)
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
if __name__ == '__main__':
|