@lark-apaas/coding-steering 0.1.38 → 0.1.39-alpha.20260824122813
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 +6 -6
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +19 -28
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +2 -8
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +190 -1181
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +10 -229
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +1 -18
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +40 -643
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +3 -19
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +2 -4
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +1 -308
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +0 -14
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +7 -157
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +2 -1598
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +4 -151
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +1 -1
- package/steering/design-html/skills/preflight/scripts/probe.sh +0 -0
- package/steering/nestjs-react-fullstack/skills/coding-guide/SKILL.md +0 -11
- package/steering/nestjs-react-fullstack/skills/contacts-service/SKILL.md +4 -67
- package/steering/nestjs-react-fullstack/skills/nestjs-cache/SKILL.md +1 -0
- package/steering/nestjs-react-fullstack/skills/openapi-guide/SKILL.md +0 -75
- package/steering/nestjs-react-fullstack/skills_local/coding-guide/SKILL.md +7 -5
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +0 -556
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +0 -479
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +0 -400
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +0 -12
|
@@ -52,15 +52,6 @@ L 层判断单 schema(<l-out-dir> 四个文件,这段就是填写说明书
|
|
|
52
52
|
role: content
|
|
53
53
|
color: "{colors.surface}"
|
|
54
54
|
|
|
55
|
-
asset_vision_groups: # 抽取草案专用,打包后不进入消费包
|
|
56
|
-
- id: vision-1
|
|
57
|
-
source_media: [mark.png, ornament.png]
|
|
58
|
-
visual_kind: decorative # FaaS 枚举;同组默认,可被 asset_decisions 覆盖
|
|
59
|
-
|
|
60
|
-
asset_decisions: # 可选:单图覆盖;旧版 decision 仍兼容
|
|
61
|
-
- source_media: chart.png
|
|
62
|
-
visual_kind: chart
|
|
63
|
-
|
|
64
55
|
说明:`boxes` / `aspect` / `canvas-source` **不要填**——脚本按 source_media 从
|
|
65
56
|
extract.json 的 images[] 直接取,写进 ref/audit.yaml。
|
|
66
57
|
|
|
@@ -121,7 +112,7 @@ L 层判断单 schema(<l-out-dir> 四个文件,这段就是填写说明书
|
|
|
121
112
|
Exceptions……)。脚本原样拼在 frontmatter 之后,不改一个字。
|
|
122
113
|
"""
|
|
123
114
|
import argparse
|
|
124
|
-
from collections import Counter
|
|
115
|
+
from collections import Counter
|
|
125
116
|
import hashlib
|
|
126
117
|
import json
|
|
127
118
|
import os
|
|
@@ -149,32 +140,6 @@ ASSET_CONSUMER_FIELDS = ['path', 'url', 'color', 'full', 'kind', 'role',
|
|
|
149
140
|
ASSET_AUDIT_FIELDS = ['boxes', 'aspect', 'mark', 'confidence']
|
|
150
141
|
KIND_PREFIX = {'background': 'bg', 'logo': 'logo', 'slogan': 'slogan',
|
|
151
142
|
'texture': 'texture', 'icon': 'icon'}
|
|
152
|
-
# 与 studio_server_faas 的 assetVisualKinds 对齐。它是抽取期视觉分类,不是 v2
|
|
153
|
-
# 消费包的 kind:后者仍只允许 KIND_PREFIX 中的五种资产类型。
|
|
154
|
-
ASSET_VISUAL_KINDS = {
|
|
155
|
-
'logo', 'slogan', 'background', 'texture', 'icon',
|
|
156
|
-
'decorative', 'illustration', 'photo', 'chart', 'screenshot',
|
|
157
|
-
'footer-copyright', 'page-number', 'watermark', 'content-image', 'unknown',
|
|
158
|
-
}
|
|
159
|
-
VISUAL_KIND_TO_DECISION = {
|
|
160
|
-
'logo': 'logo',
|
|
161
|
-
'slogan': 'slogan',
|
|
162
|
-
'background': 'background',
|
|
163
|
-
'texture': 'texture',
|
|
164
|
-
'icon': 'icon',
|
|
165
|
-
'decorative': 'texture',
|
|
166
|
-
'illustration': 'texture',
|
|
167
|
-
'photo': 'content',
|
|
168
|
-
'chart': 'content',
|
|
169
|
-
'screenshot': 'content',
|
|
170
|
-
'content-image': 'content',
|
|
171
|
-
'footer-copyright': 'omit',
|
|
172
|
-
'page-number': 'omit',
|
|
173
|
-
'watermark': 'omit',
|
|
174
|
-
# FaaS 也会把非内容的 unknown 局部图保留为 texture,避免丢掉未能命名的装饰。
|
|
175
|
-
'unknown': 'texture',
|
|
176
|
-
}
|
|
177
|
-
LEGACY_ASSET_DECISIONS = {'content', 'texture', 'logo', 'icon', 'slogan'}
|
|
178
143
|
# 交付包的 ref/ 只留 audit.yaml(人复核数值出处用,几 KB)。
|
|
179
144
|
# 频次原表、聚类原始数据、extract.json、重建图、logo 候选图全部留在 stage1 抽取目录,
|
|
180
145
|
# 不拷进包——规范原本指望「下发时链路剥离」,但链路上没有环节真的剥离,结果
|
|
@@ -187,46 +152,6 @@ class Fail(SystemExit):
|
|
|
187
152
|
super().__init__('package.py: %s' % msg)
|
|
188
153
|
|
|
189
154
|
|
|
190
|
-
class AssetDecisions(dict):
|
|
191
|
-
"""按素材的默认决定,以及只在对应 slot 生效的实例覆盖。"""
|
|
192
|
-
|
|
193
|
-
def __init__(self, defaults=None, overrides=None, roles=None,
|
|
194
|
-
default_overrides=None, explicit_overrides=None):
|
|
195
|
-
super().__init__(defaults or {})
|
|
196
|
-
self.overrides = overrides or {}
|
|
197
|
-
self.roles = roles or {}
|
|
198
|
-
self.default_overrides = default_overrides or set()
|
|
199
|
-
self.explicit_overrides = explicit_overrides or set()
|
|
200
|
-
|
|
201
|
-
def for_slot(self, source, line, layout_name=None):
|
|
202
|
-
box = source_box(line) or slot_box(line)
|
|
203
|
-
return self.for_scope(source, layout_name, box)
|
|
204
|
-
|
|
205
|
-
def for_scope(self, source, layout_name, box):
|
|
206
|
-
if box is not None:
|
|
207
|
-
for key in ((source, layout_name, box), (source, None, box)):
|
|
208
|
-
if key in self.explicit_overrides:
|
|
209
|
-
return self.overrides[key]
|
|
210
|
-
if source in self.default_overrides:
|
|
211
|
-
return self.get(source)
|
|
212
|
-
if box is not None:
|
|
213
|
-
for key in ((source, layout_name, box), (source, None, box)):
|
|
214
|
-
if key in self.overrides:
|
|
215
|
-
return self.overrides[key]
|
|
216
|
-
return self.get(source)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class AssetIds(dict):
|
|
220
|
-
"""同一原图因实例用途不同而落成多个资产时,按最终用途取对应 id。"""
|
|
221
|
-
|
|
222
|
-
def __init__(self, defaults=None, by_decision=None):
|
|
223
|
-
super().__init__(defaults or {})
|
|
224
|
-
self.by_decision = by_decision or {}
|
|
225
|
-
|
|
226
|
-
def for_decision(self, source, decision):
|
|
227
|
-
return self.by_decision.get((source, decision)) or self.get(source)
|
|
228
|
-
|
|
229
|
-
|
|
230
155
|
# ------------------------------------------------------------ 极简 YAML 读取
|
|
231
156
|
def split_top_blocks(text):
|
|
232
157
|
"""顶层 `key:` 切块。返回 [(key, inline_value, block_lines)],块内逐字节保留。"""
|
|
@@ -251,55 +176,6 @@ def split_top_blocks(text):
|
|
|
251
176
|
return out
|
|
252
177
|
|
|
253
178
|
|
|
254
|
-
LAYOUT_CONTROL_KEYS = {
|
|
255
|
-
'names', 'roles', 'text_roles', 'layout_modes', 'bg_rules',
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
def control_entry_keys(lines):
|
|
260
|
-
"""读取扁平判断段的一级条目,防止整段覆盖时漏掉旧结论。"""
|
|
261
|
-
return {
|
|
262
|
-
match.group(1)
|
|
263
|
-
for line in lines
|
|
264
|
-
for match in [re.match(r'^\s{2}([\w-]+):', line)]
|
|
265
|
-
if match
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def load_layout_blocks(lout):
|
|
270
|
-
"""读取版式坐标事实;新草案把可编辑判断区放在独立的小文件里。"""
|
|
271
|
-
lay_path = os.path.join(lout, 'layouts.yaml')
|
|
272
|
-
if not os.path.exists(lay_path):
|
|
273
|
-
return None
|
|
274
|
-
with open(lay_path, encoding='utf-8') as stream:
|
|
275
|
-
layouts = split_top_blocks(stream.read())
|
|
276
|
-
controls_path = os.path.join(lout, 'layout-controls.yaml')
|
|
277
|
-
if not os.path.exists(controls_path):
|
|
278
|
-
return layouts
|
|
279
|
-
with open(controls_path, encoding='utf-8') as stream:
|
|
280
|
-
controls = split_top_blocks(stream.read())
|
|
281
|
-
invalid = {key for key, _, _ in controls} - LAYOUT_CONTROL_KEYS
|
|
282
|
-
if invalid:
|
|
283
|
-
raise Fail('layout-controls.yaml 只能包含 %s,不能包含:%s'
|
|
284
|
-
% ('|'.join(sorted(LAYOUT_CONTROL_KEYS)), ', '.join(sorted(invalid))))
|
|
285
|
-
legacy_keys = {key for key, _, _ in layouts if key in LAYOUT_CONTROL_KEYS}
|
|
286
|
-
control_keys = {key for key, _, _ in controls}
|
|
287
|
-
missing = legacy_keys - control_keys
|
|
288
|
-
if missing:
|
|
289
|
-
raise Fail('layout-controls.yaml 必须覆盖 layouts.yaml 的全部判断段,缺:%s'
|
|
290
|
-
% ', '.join(sorted(missing)))
|
|
291
|
-
legacy_blocks = {key: lines for key, _, lines in layouts}
|
|
292
|
-
control_blocks = {key: lines for key, _, lines in controls}
|
|
293
|
-
for key in sorted(legacy_keys):
|
|
294
|
-
missing_entries = (control_entry_keys(legacy_blocks[key])
|
|
295
|
-
- control_entry_keys(control_blocks[key]))
|
|
296
|
-
if missing_entries:
|
|
297
|
-
raise Fail('layout-controls.yaml 的 %s 段缺少旧判断条目:%s'
|
|
298
|
-
% (key, ', '.join(sorted(missing_entries))))
|
|
299
|
-
base = [block for block in layouts if block[0] not in LAYOUT_CONTROL_KEYS]
|
|
300
|
-
return controls + base
|
|
301
|
-
|
|
302
|
-
|
|
303
179
|
def unquote(s):
|
|
304
180
|
s = s.strip()
|
|
305
181
|
if len(s) >= 2 and s[0] == s[-1] and s[0] in '"\'':
|
|
@@ -359,15 +235,11 @@ def parse_item_list(lines):
|
|
|
359
235
|
|
|
360
236
|
def read_manifest(path):
|
|
361
237
|
blocks = split_top_blocks(open(path, encoding='utf-8').read())
|
|
362
|
-
man, assets,
|
|
238
|
+
man, assets, raw = {}, [], {}
|
|
363
239
|
derived = []
|
|
364
240
|
for key, inline, lines in blocks:
|
|
365
241
|
if key == 'assets':
|
|
366
242
|
assets = parse_item_list(lines)
|
|
367
|
-
elif key == 'asset_vision_groups':
|
|
368
|
-
asset_vision_groups = parse_item_list(lines)
|
|
369
|
-
elif key == 'asset_decisions':
|
|
370
|
-
asset_decisions = parse_item_list(lines)
|
|
371
243
|
elif key == 'derived':
|
|
372
244
|
derived = parse_item_list(lines)
|
|
373
245
|
elif inline.startswith('['):
|
|
@@ -381,102 +253,11 @@ def read_manifest(path):
|
|
|
381
253
|
# 不把作者的折行改成一条长行。
|
|
382
254
|
raw[key] = (inline, lines)
|
|
383
255
|
man['assets'] = assets
|
|
384
|
-
man['asset_vision_groups'] = asset_vision_groups
|
|
385
|
-
man['asset_decisions'] = asset_decisions
|
|
386
256
|
man['derived'] = derived
|
|
387
257
|
man['_raw'] = raw
|
|
388
258
|
return man
|
|
389
259
|
|
|
390
260
|
|
|
391
|
-
def validate_asset_vision_groups(lout, manifest):
|
|
392
|
-
"""新草案的候选索引存在时,视觉判断必须逐项覆盖且不改写实例事实。"""
|
|
393
|
-
index_path = os.path.join(lout, 'asset-vision-groups.json')
|
|
394
|
-
if not os.path.exists(index_path):
|
|
395
|
-
return {}
|
|
396
|
-
try:
|
|
397
|
-
with open(index_path, encoding='utf-8') as stream:
|
|
398
|
-
index = json.load(stream)
|
|
399
|
-
except (OSError, ValueError) as exc:
|
|
400
|
-
raise Fail('asset-vision-groups.json 无法读取:%s' % exc)
|
|
401
|
-
if 'selected' not in index:
|
|
402
|
-
raise Fail('asset-vision-groups.json 缺少 selected 候选列表')
|
|
403
|
-
|
|
404
|
-
expected = {}
|
|
405
|
-
for group in index.get('selected') or []:
|
|
406
|
-
for candidate in group.get('candidates') or []:
|
|
407
|
-
candidate_id = unquote(str(candidate.get('id') or ''))
|
|
408
|
-
source = unquote(str(candidate.get('source_media') or candidate.get('file') or ''))
|
|
409
|
-
placements = candidate.get('placements') or []
|
|
410
|
-
placement = placements[0] if placements else {}
|
|
411
|
-
raw_box = placement.get('box')
|
|
412
|
-
box = tuple(raw_box) if isinstance(raw_box, list) and len(raw_box) == 4 else None
|
|
413
|
-
layout = unquote(str(candidate.get('layout') or placement.get('archetype') or ''))
|
|
414
|
-
if not candidate_id or not source or box is None:
|
|
415
|
-
raise Fail('asset-vision-groups.json 的候选缺少 id、source_media 或 box')
|
|
416
|
-
if candidate_id in expected:
|
|
417
|
-
raise Fail('asset-vision-groups.json 的候选 id 重复:%s' % candidate_id)
|
|
418
|
-
expected[candidate_id] = (source, layout or None, box)
|
|
419
|
-
|
|
420
|
-
actual = {}
|
|
421
|
-
for item in manifest.get('asset_vision_groups') or []:
|
|
422
|
-
candidate_id = unquote(item.get('id') or '')
|
|
423
|
-
sources = source_media_list(item.get('source_media') or '')
|
|
424
|
-
layout, box = decision_scope(item, 'asset_vision_groups.%s' % candidate_id)
|
|
425
|
-
if not candidate_id:
|
|
426
|
-
raise Fail('asset_vision_groups 必须逐项填写 id')
|
|
427
|
-
if candidate_id in actual:
|
|
428
|
-
raise Fail('asset_vision_groups 的候选 id 重复:%s' % candidate_id)
|
|
429
|
-
actual[candidate_id] = (sources, layout, box)
|
|
430
|
-
|
|
431
|
-
expected_ids = set(expected)
|
|
432
|
-
actual_ids = set(actual)
|
|
433
|
-
instance_format = index.get('version', 1) >= 2
|
|
434
|
-
if instance_format:
|
|
435
|
-
missing = sorted(expected_ids - actual_ids)
|
|
436
|
-
extra = sorted(actual_ids - expected_ids)
|
|
437
|
-
if missing or extra:
|
|
438
|
-
problems = []
|
|
439
|
-
if missing:
|
|
440
|
-
problems.append('缺少:%s' % ', '.join(missing))
|
|
441
|
-
if extra:
|
|
442
|
-
problems.append('出现未知项:%s' % ', '.join(extra))
|
|
443
|
-
raise Fail('asset_vision_groups 必须逐项保留抽取候选,%s'
|
|
444
|
-
% ';'.join(problems))
|
|
445
|
-
for candidate_id, expected_scope in expected.items():
|
|
446
|
-
sources, layout, box = actual[candidate_id]
|
|
447
|
-
expected_source, expected_layout, expected_box = expected_scope
|
|
448
|
-
if sources != [expected_source] or box != expected_box:
|
|
449
|
-
raise Fail('asset_vision_groups.%s 改写了抽取候选的图片或位置'
|
|
450
|
-
% candidate_id)
|
|
451
|
-
if layout and expected_layout and layout != expected_layout:
|
|
452
|
-
raise Fail('asset_vision_groups.%s 改写了抽取候选的页型'
|
|
453
|
-
% candidate_id)
|
|
454
|
-
return expected
|
|
455
|
-
|
|
456
|
-
# 旧草案用视觉组 id 和 source_media 列表表达默认判断,无法逐实例比对。
|
|
457
|
-
# 保留这种兼容输入,但仍要求每个选中的候选来源至少被一个组覆盖。
|
|
458
|
-
actual_sources = {
|
|
459
|
-
source for sources, _, _ in actual.values() for source in sources
|
|
460
|
-
}
|
|
461
|
-
missing_sources = sorted({
|
|
462
|
-
source for source, _, _ in expected.values()
|
|
463
|
-
} - actual_sources)
|
|
464
|
-
if missing_sources:
|
|
465
|
-
raise Fail('asset_vision_groups 必须覆盖抽取候选来源,缺少:%s'
|
|
466
|
-
% ', '.join(missing_sources))
|
|
467
|
-
return {}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
def bind_asset_vision_group_scopes(lout, manifest):
|
|
471
|
-
"""用 v2 候选 id 恢复实例页型,模型无需重复填写 layout。"""
|
|
472
|
-
expected = validate_asset_vision_groups(lout, manifest)
|
|
473
|
-
for item in manifest.get('asset_vision_groups') or []:
|
|
474
|
-
candidate_id = unquote(item.get('id') or '')
|
|
475
|
-
scope = expected.get(candidate_id)
|
|
476
|
-
if scope and scope[1]:
|
|
477
|
-
item['_resolved_layout'] = scope[1]
|
|
478
|
-
|
|
479
|
-
|
|
480
261
|
# ------------------------------------------------------------------- 组装
|
|
481
262
|
def yaml_scalar(v):
|
|
482
263
|
"""标量回写。已带引号 / 流式结构原样;含 YAML 危险序列才补引号。"""
|
|
@@ -510,284 +291,6 @@ def strip_kind_prefix(aid, kind):
|
|
|
510
291
|
return aid
|
|
511
292
|
|
|
512
293
|
|
|
513
|
-
def source_media_list(value):
|
|
514
|
-
"""`source_media: one.png` 与视觉组的 `[a.png, b.png]` 两种写法都接受。"""
|
|
515
|
-
raw = value.strip()
|
|
516
|
-
parsed = parse_flow_list(raw)
|
|
517
|
-
return parsed if parsed is not None else [unquote(raw)] if raw else []
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
def visual_kind_decision(value, label):
|
|
521
|
-
visual_kind = unquote(value or '')
|
|
522
|
-
if visual_kind not in ASSET_VISUAL_KINDS:
|
|
523
|
-
raise Fail('%s 的 visual_kind 只能是 %s'
|
|
524
|
-
% (label, '|'.join(sorted(ASSET_VISUAL_KINDS))))
|
|
525
|
-
return VISUAL_KIND_TO_DECISION[visual_kind]
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
def slot_box(line):
|
|
529
|
-
match = re.search(r'\bbox:\s*\[([^\]]+)\]', line)
|
|
530
|
-
return parse_box_match(match)
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
def source_box(line):
|
|
534
|
-
match = re.search(r'\bsource_box:\s*\[([^\]]+)\]', line)
|
|
535
|
-
return parse_box_match(match)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def parse_box_match(match):
|
|
539
|
-
if not match:
|
|
540
|
-
return None
|
|
541
|
-
try:
|
|
542
|
-
values = tuple(int(float(part.strip())) for part in match.group(1).split(','))
|
|
543
|
-
except ValueError:
|
|
544
|
-
return None
|
|
545
|
-
return values if len(values) == 4 else None
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
def decision_scope(item, label):
|
|
549
|
-
raw_box = item.get('box')
|
|
550
|
-
box = slot_box('box: %s' % raw_box) if raw_box else None
|
|
551
|
-
if raw_box and box is None:
|
|
552
|
-
raise Fail('%s 的 box 必须是四个数的 [x, y, w, h]' % label)
|
|
553
|
-
layout = unquote(item.get('_resolved_layout') or item.get('layout') or '')
|
|
554
|
-
if layout and box is None:
|
|
555
|
-
raise Fail('%s 的 layout 必须与 box 一起填写' % label)
|
|
556
|
-
return layout or None, box
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
def register_asset_decision(decisions, visual_kinds, key, decision, visual_kind):
|
|
560
|
-
"""合并相同实例的重复判断;显式 visual_kind 必须逐字一致。"""
|
|
561
|
-
if key not in decisions:
|
|
562
|
-
decisions[key] = decision
|
|
563
|
-
visual_kinds[key] = visual_kind
|
|
564
|
-
return
|
|
565
|
-
previous_kind = visual_kinds[key]
|
|
566
|
-
if ((previous_kind and visual_kind and previous_kind != visual_kind)
|
|
567
|
-
or (not previous_kind or not visual_kind)
|
|
568
|
-
and decisions[key] != decision):
|
|
569
|
-
label = 'source_media' if key[2] is None else '实例'
|
|
570
|
-
raise Fail('图片判断的 %s判断冲突: %s' % (label, key[0]))
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
def register_asset_role(roles, background_roles, source, layout, box, decision, role):
|
|
574
|
-
if not role:
|
|
575
|
-
return
|
|
576
|
-
if source in roles and roles[source] != role:
|
|
577
|
-
raise Fail('图片判断的 source_media role 冲突: %s' % source)
|
|
578
|
-
roles[source] = role
|
|
579
|
-
if decision != 'background' or box is None:
|
|
580
|
-
return
|
|
581
|
-
for (known_layout, known_box), (known_source, known_role) in background_roles.items():
|
|
582
|
-
if (known_box == box
|
|
583
|
-
and (known_layout == layout
|
|
584
|
-
or known_layout is None
|
|
585
|
-
or layout is None)
|
|
586
|
-
and known_role != role):
|
|
587
|
-
raise Fail('图片判断的背景实例 role 冲突: %s、%s'
|
|
588
|
-
% (known_source, source))
|
|
589
|
-
background_roles[(layout, box)] = (source, role)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def decision_overrides(manifest):
|
|
593
|
-
"""把视觉组默认值与单图/单页型实例覆盖展开为按 source_media 的内部决定。"""
|
|
594
|
-
scoped, visual_kinds, roles, background_roles = {}, {}, {}, {}
|
|
595
|
-
for group in manifest.get('asset_vision_groups') or []:
|
|
596
|
-
group_id = unquote(group.get('id') or '')
|
|
597
|
-
sources = source_media_list(group.get('source_media') or '')
|
|
598
|
-
if not group_id or not sources:
|
|
599
|
-
raise Fail('asset_vision_groups 必须逐项填写 id 与 source_media')
|
|
600
|
-
visual_kind = unquote(group.get('visual_kind') or '')
|
|
601
|
-
decision = visual_kind_decision(visual_kind,
|
|
602
|
-
'asset_vision_groups.%s' % group_id)
|
|
603
|
-
layout, box = decision_scope(
|
|
604
|
-
group, 'asset_vision_groups.%s' % group_id)
|
|
605
|
-
if box is not None and len(sources) != 1:
|
|
606
|
-
raise Fail('asset_vision_groups.%s 的实例判断只能填写一个 source_media'
|
|
607
|
-
% group_id)
|
|
608
|
-
role = unquote(group.get('role') or '')
|
|
609
|
-
for source in sources:
|
|
610
|
-
key = (source, layout, box)
|
|
611
|
-
register_asset_decision(scoped, visual_kinds, key, decision, visual_kind)
|
|
612
|
-
register_asset_role(
|
|
613
|
-
roles, background_roles, source, layout, box, decision, role)
|
|
614
|
-
|
|
615
|
-
explicit_defaults, explicit_default_kinds = {}, {}
|
|
616
|
-
explicit_overrides, explicit_override_kinds = {}, {}
|
|
617
|
-
explicit_roles = {}
|
|
618
|
-
for item in manifest.get('asset_decisions') or []:
|
|
619
|
-
source = unquote(item.get('source_media') or '')
|
|
620
|
-
visual_kind = item.get('visual_kind')
|
|
621
|
-
legacy = item.get('decision')
|
|
622
|
-
if not source or (visual_kind and legacy):
|
|
623
|
-
raise Fail('asset_decisions 每项只填 source_media 和 visual_kind(或旧 decision)')
|
|
624
|
-
if visual_kind:
|
|
625
|
-
decision = visual_kind_decision(visual_kind, 'asset_decisions.%s' % source)
|
|
626
|
-
else:
|
|
627
|
-
decision = unquote(legacy or '')
|
|
628
|
-
if decision not in LEGACY_ASSET_DECISIONS:
|
|
629
|
-
raise Fail('asset_decisions 的旧 decision 只能是 %s'
|
|
630
|
-
% '|'.join(sorted(LEGACY_ASSET_DECISIONS)))
|
|
631
|
-
layout, box = decision_scope(item, 'asset_decisions.%s' % source)
|
|
632
|
-
target = (explicit_defaults, explicit_default_kinds) if box is None else (
|
|
633
|
-
explicit_overrides, explicit_override_kinds)
|
|
634
|
-
register_asset_decision(
|
|
635
|
-
target[0], target[1], (source, layout, box), decision,
|
|
636
|
-
unquote(visual_kind) if visual_kind else None)
|
|
637
|
-
role = unquote(item.get('role') or '')
|
|
638
|
-
register_asset_role(
|
|
639
|
-
explicit_roles, background_roles, source, layout, box, decision, role)
|
|
640
|
-
|
|
641
|
-
scoped.update(explicit_defaults)
|
|
642
|
-
scoped.update(explicit_overrides)
|
|
643
|
-
for source, role in explicit_roles.items():
|
|
644
|
-
if source in roles and roles[source] != role:
|
|
645
|
-
raise Fail('图片判断的 source_media role 冲突: %s' % source)
|
|
646
|
-
roles[source] = role
|
|
647
|
-
|
|
648
|
-
defaults = {
|
|
649
|
-
source: decision
|
|
650
|
-
for (source, _, box), decision in scoped.items()
|
|
651
|
-
if box is None
|
|
652
|
-
}
|
|
653
|
-
overrides = {
|
|
654
|
-
(source, layout, box): decision
|
|
655
|
-
for (source, layout, box), decision in scoped.items()
|
|
656
|
-
if box is not None
|
|
657
|
-
}
|
|
658
|
-
return AssetDecisions(
|
|
659
|
-
defaults, overrides, roles,
|
|
660
|
-
{source for source, _, _ in explicit_defaults},
|
|
661
|
-
set(explicit_overrides),
|
|
662
|
-
)
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
def reusable_decisions_for_source(source, decisions):
|
|
666
|
-
"""无布局槽位时,收集该源图所有可能落盘的可复用用途。"""
|
|
667
|
-
reusable = {'background', 'texture', 'logo', 'icon', 'slogan'}
|
|
668
|
-
default = decisions.get(source)
|
|
669
|
-
overrides = {
|
|
670
|
-
decision for (override_source, layout, box), decision in decisions.overrides.items()
|
|
671
|
-
if override_source == source
|
|
672
|
-
and decision in reusable
|
|
673
|
-
and (source not in decisions.default_overrides
|
|
674
|
-
or (override_source, layout, box) in decisions.explicit_overrides)
|
|
675
|
-
}
|
|
676
|
-
return overrides | ({default} if default in reusable else set())
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def reusable_decisions_for_slots(source, decisions, source_slots=None):
|
|
680
|
-
"""按实际图片槽的最终决定保留资产,可让同图在不同实例按用途绑定。"""
|
|
681
|
-
reusable = {'background', 'texture', 'logo', 'icon', 'slogan'}
|
|
682
|
-
slots = (source_slots or {}).get(source)
|
|
683
|
-
if slots is None:
|
|
684
|
-
return reusable_decisions_for_source(source, decisions)
|
|
685
|
-
return {
|
|
686
|
-
decisions.for_scope(source, layout_name, box)
|
|
687
|
-
for layout_name, box in slots
|
|
688
|
-
} & reusable
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
def validate_instance_decisions(decisions, bound_slots):
|
|
692
|
-
"""实例判断必须指向已有图片槽,避免 layout 拼错后静默退化为通用图片。"""
|
|
693
|
-
if bound_slots is None:
|
|
694
|
-
return
|
|
695
|
-
for source, layout_name, box in decisions.overrides:
|
|
696
|
-
slots = bound_slots.get(source) or set()
|
|
697
|
-
matches = ((layout_name, box) in slots if layout_name is not None
|
|
698
|
-
else any(slot_box == box for _, slot_box in slots))
|
|
699
|
-
if not matches:
|
|
700
|
-
raise Fail('图片判断的实例没有对应图片槽: %s' % source)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
def apply_asset_decisions(manifest, bound_sources=None, bound_slots=None):
|
|
704
|
-
"""把抽取期图片判断并入正式 assets;内容图与噪声不进入消费产物。"""
|
|
705
|
-
decisions = decision_overrides(manifest)
|
|
706
|
-
if bound_sources is not None:
|
|
707
|
-
declared = set(decisions)
|
|
708
|
-
declared |= {source for source, _, _ in decisions.overrides}
|
|
709
|
-
unbound = sorted(declared - set(bound_sources))
|
|
710
|
-
if unbound:
|
|
711
|
-
raise Fail('图片判断中这些图片没有对应图片槽:%s'
|
|
712
|
-
% '、'.join(unbound))
|
|
713
|
-
validate_instance_decisions(decisions, bound_slots)
|
|
714
|
-
|
|
715
|
-
assets = []
|
|
716
|
-
by_source_kind = {}
|
|
717
|
-
serial = Counter()
|
|
718
|
-
used_ids = {unquote(item.get('id') or '')
|
|
719
|
-
for item in manifest.get('assets') or [] if item.get('id')}
|
|
720
|
-
decision_sources = list(decisions)
|
|
721
|
-
for source, _, _ in decisions.overrides:
|
|
722
|
-
if source not in decision_sources:
|
|
723
|
-
decision_sources.append(source)
|
|
724
|
-
desired_kinds = {
|
|
725
|
-
source: reusable_decisions_for_slots(source, decisions, bound_slots)
|
|
726
|
-
for source in decision_sources
|
|
727
|
-
}
|
|
728
|
-
emitted = set()
|
|
729
|
-
|
|
730
|
-
def next_asset_id(kind):
|
|
731
|
-
serial[kind] += 1
|
|
732
|
-
aid = '%s-%d' % (KIND_PREFIX[kind], serial[kind])
|
|
733
|
-
while aid in used_ids:
|
|
734
|
-
serial[kind] += 1
|
|
735
|
-
aid = '%s-%d' % (KIND_PREFIX[kind], serial[kind])
|
|
736
|
-
used_ids.add(aid)
|
|
737
|
-
return aid
|
|
738
|
-
|
|
739
|
-
def append_asset(item, source, decision):
|
|
740
|
-
old_kind = unquote(item.get('kind') or '')
|
|
741
|
-
pair = (source, decision if source in decision_sources else old_kind)
|
|
742
|
-
if source and pair in emitted:
|
|
743
|
-
return
|
|
744
|
-
row = dict(item)
|
|
745
|
-
if decision and decision != old_kind:
|
|
746
|
-
row['kind'] = decision
|
|
747
|
-
row['id'] = next_asset_id(decision)
|
|
748
|
-
for field in ('role', 'theme', 'on-bg', 'mark'):
|
|
749
|
-
row.pop(field, None)
|
|
750
|
-
if decision == 'background':
|
|
751
|
-
row['role'] = decisions.roles.get(source) or 'content'
|
|
752
|
-
if source:
|
|
753
|
-
emitted.add(pair)
|
|
754
|
-
by_source_kind[(source, row['kind'])] = row
|
|
755
|
-
used_ids.add(unquote(row.get('id') or ''))
|
|
756
|
-
assets.append(row)
|
|
757
|
-
|
|
758
|
-
for item in manifest.get('assets') or []:
|
|
759
|
-
source = unquote(item.get('source_media') or '')
|
|
760
|
-
if source in decision_sources:
|
|
761
|
-
for decision in sorted(desired_kinds[source]):
|
|
762
|
-
append_asset(item, source, decision)
|
|
763
|
-
else:
|
|
764
|
-
append_asset(item, source, None)
|
|
765
|
-
|
|
766
|
-
for source in decision_sources:
|
|
767
|
-
for decision in sorted(desired_kinds[source]):
|
|
768
|
-
if (source, decision) in emitted:
|
|
769
|
-
continue
|
|
770
|
-
append_asset({
|
|
771
|
-
'id': next_asset_id(decision),
|
|
772
|
-
'source_media': source,
|
|
773
|
-
'kind': decision,
|
|
774
|
-
}, source, decision)
|
|
775
|
-
manifest['assets'] = assets
|
|
776
|
-
source_kinds = defaultdict(set)
|
|
777
|
-
for source, kind in by_source_kind:
|
|
778
|
-
source_kinds[source].add(kind)
|
|
779
|
-
defaults = {
|
|
780
|
-
source: unquote(by_source_kind[(source, next(iter(kinds)))]['id'])
|
|
781
|
-
for source, kinds in source_kinds.items()
|
|
782
|
-
if len(kinds) == 1
|
|
783
|
-
}
|
|
784
|
-
scoped = {
|
|
785
|
-
(source, kind): unquote(item['id'])
|
|
786
|
-
for (source, kind), item in by_source_kind.items()
|
|
787
|
-
}
|
|
788
|
-
return decisions, AssetIds(defaults, scoped)
|
|
789
|
-
|
|
790
|
-
|
|
791
294
|
def media_row_of(extract, source_media):
|
|
792
295
|
for m in extract.get('media') or []:
|
|
793
296
|
if m.get('out') and os.path.basename(m['out']) == source_media:
|
|
@@ -972,16 +475,6 @@ def expand_placeholders(body, manifest, consumer, audit, extract, layouts_text):
|
|
|
972
475
|
f += '(原图 `%s`)' % e['full']
|
|
973
476
|
rows.append('| `%s` | %s | %s |' % (aid, f, when))
|
|
974
477
|
body = body.replace('{{ASSET_TABLE}}', '\n'.join(rows))
|
|
975
|
-
if '{{LOGO_RULES}}' in body:
|
|
976
|
-
rows = []
|
|
977
|
-
for aid, entry in consumer.items():
|
|
978
|
-
if entry.get('kind') != 'logo':
|
|
979
|
-
continue
|
|
980
|
-
owners = _archetypes_using(layouts_text, aid)
|
|
981
|
-
rows.append('- `%s` 只出现在这些页型上:%s;其余页型不放。位置取该页型 '
|
|
982
|
-
'`slots` 里 `role: logo` 那一项的 `box`,原样使用该文件、保持原比例。'
|
|
983
|
-
% (aid, '、'.join('`%s`' % owner for owner in owners) or '(无)'))
|
|
984
|
-
body = body.replace('{{LOGO_RULES}}', '\n'.join(rows))
|
|
985
478
|
if '{{LAYOUT_LIST}}' in body:
|
|
986
479
|
rows, sect, names = [], None, {}
|
|
987
480
|
for line in (layouts_text or '').split('\n'):
|
|
@@ -1453,7 +946,6 @@ def text_role_boxes(lines):
|
|
|
1453
946
|
|
|
1454
947
|
def select_layout_forms(lines, modes):
|
|
1455
948
|
"""按 `layout_modes` 只保留每个页型选中的 flow 或 slots。"""
|
|
1456
|
-
forms = layout_forms(lines)
|
|
1457
949
|
out, layout, drop = [], None, False
|
|
1458
950
|
for line in lines:
|
|
1459
951
|
layout_match = re.match(r'^ ([\w-]+):\s*$', line)
|
|
@@ -1461,10 +953,7 @@ def select_layout_forms(lines, modes):
|
|
|
1461
953
|
if layout_match:
|
|
1462
954
|
layout, drop = layout_match.group(1), False
|
|
1463
955
|
elif form_match:
|
|
1464
|
-
|
|
1465
|
-
if selected is None and len(forms.get(layout) or ()) >= 2:
|
|
1466
|
-
selected = 'slots'
|
|
1467
|
-
drop = selected is not None and selected != form_match.group(1)
|
|
956
|
+
drop = modes.get(layout) not in (None, form_match.group(1))
|
|
1468
957
|
elif drop and re.match(r'^ \S', line):
|
|
1469
958
|
drop = False
|
|
1470
959
|
if not drop:
|
|
@@ -1524,58 +1013,7 @@ def shrink_safe_area(lines):
|
|
|
1524
1013
|
return lines, None
|
|
1525
1014
|
|
|
1526
1015
|
|
|
1527
|
-
def
|
|
1528
|
-
"""把 source_media 临时字段转成资产引用;内容图保留通用 pic 槽。"""
|
|
1529
|
-
out = []
|
|
1530
|
-
content_slots = set()
|
|
1531
|
-
layout_name, layout_form = None, None
|
|
1532
|
-
for line in lines:
|
|
1533
|
-
layout_match = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1534
|
-
form_match = re.match(r'^ (flow|slots):\s*$', line)
|
|
1535
|
-
if layout_match:
|
|
1536
|
-
layout_name, layout_form = layout_match.group(1), None
|
|
1537
|
-
elif form_match:
|
|
1538
|
-
layout_form = form_match.group(1)
|
|
1539
|
-
match = re.search(r'\bsource_media:\s*([^,}]+)', line)
|
|
1540
|
-
if not match:
|
|
1541
|
-
out.append(line)
|
|
1542
|
-
continue
|
|
1543
|
-
source = unquote(match.group(1).strip())
|
|
1544
|
-
candidate_box = source_box(line) or slot_box(line)
|
|
1545
|
-
decision = (decisions.for_slot(source, line, layout_name)
|
|
1546
|
-
if isinstance(decisions, AssetDecisions)
|
|
1547
|
-
else decisions.get(source))
|
|
1548
|
-
line = re.sub(r'\s*,?\s*source_media:\s*[^,}]+', '', line, count=1)
|
|
1549
|
-
line = re.sub(r'\s*,?\s*source_box:\s*\[[^\]]+\]', '', line, count=1)
|
|
1550
|
-
# 预算裁掉、没有进入视觉组的素材不凭空判成风格资产;保留原位置通用图片槽,
|
|
1551
|
-
# 让生成侧可放这一页的内容图。`omit` 是页码/水印等纯噪声,直接去掉槽。
|
|
1552
|
-
if decision == 'omit':
|
|
1553
|
-
continue
|
|
1554
|
-
if decision in (None, 'content'):
|
|
1555
|
-
line = re.sub(r'\s*,?\s*asset:\s*[^,}]+', '', line, count=1)
|
|
1556
|
-
line = re.sub(r'(\brole:\s*)[\w-]+', r'\g<1>pic', line, count=1)
|
|
1557
|
-
# 内容图不进风格资产,但同一位置只能留一个通用图片槽。不同图片源在同一
|
|
1558
|
-
# box 的叠加是原 PPT 的内容选择,不应被转换成两个生成时必然重叠的 pic 槽。
|
|
1559
|
-
content_key = (layout_name, layout_form, candidate_box or line.strip())
|
|
1560
|
-
if content_key in content_slots:
|
|
1561
|
-
continue
|
|
1562
|
-
content_slots.add(content_key)
|
|
1563
|
-
out.append(line)
|
|
1564
|
-
continue
|
|
1565
|
-
aid = (asset_ids.for_decision(source, decision)
|
|
1566
|
-
if isinstance(asset_ids, AssetIds) else asset_ids.get(source))
|
|
1567
|
-
if not aid:
|
|
1568
|
-
raise Fail('layouts.yaml 引用了未完成资产判断的 source_media: %s' % source)
|
|
1569
|
-
if re.search(r'\basset:\s*[^,}]+', line):
|
|
1570
|
-
line = re.sub(r'(\basset:\s*)[^,}]+', r'\g<1>%s' % aid, line, count=1)
|
|
1571
|
-
else:
|
|
1572
|
-
line = line.replace('}', ', asset: %s}' % aid, 1)
|
|
1573
|
-
line = re.sub(r'(\brole:\s*)[\w-]+', r'\g<1>%s' % decision, line, count=1)
|
|
1574
|
-
out.append(line)
|
|
1575
|
-
return out
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=None):
|
|
1016
|
+
def build_layouts_md(layouts_blocks, canvas):
|
|
1579
1017
|
blocks = {k: (inline, lines) for k, inline, lines in layouts_blocks}
|
|
1580
1018
|
if 'canvas' in blocks:
|
|
1581
1019
|
raise Fail('layouts.yaml 不要写 canvas —— 脚本从 extract.json 取')
|
|
@@ -1623,18 +1061,13 @@ def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=Non
|
|
|
1623
1061
|
out.append(' %s:' % bg)
|
|
1624
1062
|
out += lines
|
|
1625
1063
|
modes = layout_modes(blocks.get('layout_modes', ('', []))[1])
|
|
1626
|
-
decided_layout_roles = set(roles)
|
|
1627
1064
|
out.append('layouts:')
|
|
1628
1065
|
pending_text_role = None
|
|
1629
1066
|
used_text_roles = set()
|
|
1630
|
-
|
|
1631
|
-
resolved_asset_ids = asset_ids if asset_ids is not None else {}
|
|
1632
|
-
source_layout_lines = resolve_asset_candidate_lines(
|
|
1633
|
-
blocks['layouts'][1], decisions, resolved_asset_ids)
|
|
1067
|
+
source_layout_lines = blocks['layouts'][1]
|
|
1634
1068
|
role_boxes = text_role_boxes(source_layout_lines)
|
|
1635
1069
|
layout_lines = select_layout_forms(source_layout_lines, modes)
|
|
1636
1070
|
cur, fixed_items = None, []
|
|
1637
|
-
emitted_roles = set()
|
|
1638
1071
|
|
|
1639
1072
|
def flush_fixed_items():
|
|
1640
1073
|
if not fixed_items:
|
|
@@ -1651,10 +1084,6 @@ def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=Non
|
|
|
1651
1084
|
cur = m.group(1)
|
|
1652
1085
|
elif fixed_items and re.match(r'^ \S', line):
|
|
1653
1086
|
flush_fixed_items()
|
|
1654
|
-
if cur in decided_layout_roles and re.match(r'^ role:\s*', line):
|
|
1655
|
-
# 页型草案中的 role 只是初始值;最终语义只由扁平 roles 判断单决定。
|
|
1656
|
-
# 否则两者同时写入会产生重复 YAML 键,并让后面的草案默认值覆盖模型判断。
|
|
1657
|
-
continue
|
|
1658
1087
|
# 判断单里的结构事实(栅格、间距序列、样张字数、命中配方)是给 L 层判断用的,
|
|
1659
1088
|
# 不进产物——消费端要的是结论,不是推导过程。layout_mode 同理,它是判断的载体。
|
|
1660
1089
|
if line.lstrip().startswith('#'):
|
|
@@ -1663,38 +1092,34 @@ def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=Non
|
|
|
1663
1092
|
pending_text_role = marker.group(1)
|
|
1664
1093
|
continue
|
|
1665
1094
|
if pending_text_role:
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
fixed_items.append(line)
|
|
1685
|
-
used_text_roles.add(pending_text_role)
|
|
1686
|
-
pending_text_role = None
|
|
1687
|
-
continue
|
|
1095
|
+
if pending_text_role not in text_roles:
|
|
1096
|
+
raise Fail('text_roles 缺少 %s 的判断' % pending_text_role)
|
|
1097
|
+
role = text_roles[pending_text_role]
|
|
1098
|
+
slot_type = role if role in ('title', 'subtitle', 'header', 'footer') else 'body'
|
|
1099
|
+
line, role_n = re.subn(r'(\{\s*role:\s*)[\w-]+',
|
|
1100
|
+
r'\g<1>%s' % role, line, count=1)
|
|
1101
|
+
line, type_n = re.subn(r'(\btype:\s*)[\w-]+',
|
|
1102
|
+
r'\g<1>%s' % slot_type, line, count=1)
|
|
1103
|
+
if role_n != 1 or type_n != 1:
|
|
1104
|
+
raise Fail('text_roles.%s 没有命中一个文本槽' % pending_text_role)
|
|
1105
|
+
if (modes.get(cur) == 'flow' and role in ('header', 'footer')
|
|
1106
|
+
and re.match(r'^\s{12}-\s*\{', line) and 'box:' not in line):
|
|
1107
|
+
box = role_boxes.get(pending_text_role)
|
|
1108
|
+
if not box:
|
|
1109
|
+
raise Fail('text_roles.%s 识别为 %s,但 slots 中没有坐标'
|
|
1110
|
+
% (pending_text_role, role))
|
|
1111
|
+
line = re.sub(r',\s*type:', ', box: %s, type:' % box, line, count=1)
|
|
1112
|
+
fixed_items.append(line)
|
|
1688
1113
|
used_text_roles.add(pending_text_role)
|
|
1114
|
+
pending_text_role = None
|
|
1115
|
+
continue
|
|
1116
|
+
used_text_roles.add(pending_text_role)
|
|
1689
1117
|
pending_text_role = None
|
|
1690
1118
|
out.append(line)
|
|
1691
|
-
if cur and re.match(r'^ role:\s*', line):
|
|
1692
|
-
emitted_roles.add(cur)
|
|
1693
1119
|
if m and m.group(1) in names:
|
|
1694
1120
|
out.append(' name: "%s"' % names.pop(m.group(1)))
|
|
1695
1121
|
if m and m.group(1) in roles:
|
|
1696
1122
|
out.append(' role: %s' % roles.pop(m.group(1)))
|
|
1697
|
-
emitted_roles.add(m.group(1))
|
|
1698
1123
|
flush_fixed_items()
|
|
1699
1124
|
if names:
|
|
1700
1125
|
raise Fail('names 里这些页型在 layouts 下找不到:%s' % ', '.join(sorted(names)))
|
|
@@ -1704,14 +1129,13 @@ def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=Non
|
|
|
1704
1129
|
if unused_text_roles:
|
|
1705
1130
|
raise Fail('text_roles 里这些判断没有命中文本槽:%s'
|
|
1706
1131
|
% ', '.join(sorted(unused_text_roles)))
|
|
1707
|
-
missing_role = [
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
]
|
|
1132
|
+
missing_role = [k for k in re.findall(r'^ ([\w-]+):\s*$', '\n'.join(blocks['layouts'][1]), re.M)
|
|
1133
|
+
if not re.search(r'^ %s:\s*$(?:\n(?! \S).*)*?\n role:' % re.escape(k),
|
|
1134
|
+
'\n'.join(out), re.M)]
|
|
1711
1135
|
if missing_role:
|
|
1712
1136
|
raise Fail('这些页型没有 role(在 layouts.yaml 的 roles 段填):%s' % ', '.join(missing_role))
|
|
1713
|
-
#
|
|
1714
|
-
#
|
|
1137
|
+
# `layout_modes.*: TODO` 由上面那道通用 TODO 扫描报(它连行内提示一起打出来),
|
|
1138
|
+
# 这里只管它管不到的两种:整行被删、以及填了 flow/slots 之外的值。
|
|
1715
1139
|
forms = layout_forms(blocks['layouts'][1])
|
|
1716
1140
|
bad = []
|
|
1717
1141
|
for k, v in sorted(modes.items()):
|
|
@@ -1721,6 +1145,9 @@ def build_layouts_md(layouts_blocks, canvas, asset_decisions=None, asset_ids=Non
|
|
|
1721
1145
|
bad.append('%s 的 layout_modes 判断是 %r' % (k, v))
|
|
1722
1146
|
elif v not in forms[k]:
|
|
1723
1147
|
bad.append('%s 选了 %s 但该页型没有这一份' % (k, v))
|
|
1148
|
+
for k, forms_for_layout in sorted(forms.items()):
|
|
1149
|
+
if len(forms_for_layout) >= 2 and k not in modes:
|
|
1150
|
+
bad.append('%s 缺 layout_modes 判断' % k)
|
|
1724
1151
|
if bad:
|
|
1725
1152
|
raise Fail('layout_modes 只能填 flow 或 slots,一个页型一个词:%s' % ';'.join(bad))
|
|
1726
1153
|
declared = set(re.findall(r'^ background:\s*(\S+)\s*$',
|
|
@@ -1773,13 +1200,7 @@ def main(argv=None):
|
|
|
1773
1200
|
raise Fail('%s 非空;加 --force 覆盖' % pack)
|
|
1774
1201
|
|
|
1775
1202
|
left = []
|
|
1776
|
-
|
|
1777
|
-
draft_files = ['manifest.yaml', 'frontmatter.yaml', 'body.md']
|
|
1778
|
-
# 新草案的可编辑判断已移到小型控制文件;layouts.yaml 中保留的旧控制区只是
|
|
1779
|
-
# 兼容坐标事实,仍含 TODO 也不会进入最终 layouts.md。
|
|
1780
|
-
draft_files.append('layout-controls.yaml' if os.path.exists(control_path)
|
|
1781
|
-
else 'layouts.yaml')
|
|
1782
|
-
for f in draft_files:
|
|
1203
|
+
for f in ('manifest.yaml', 'frontmatter.yaml', 'layouts.yaml', 'body.md'):
|
|
1783
1204
|
p = os.path.join(lout, f)
|
|
1784
1205
|
if not os.path.exists(p):
|
|
1785
1206
|
continue
|
|
@@ -1797,7 +1218,6 @@ def main(argv=None):
|
|
|
1797
1218
|
% len(re.sub(r'\s+', '', mdesc.group(1))))
|
|
1798
1219
|
|
|
1799
1220
|
manifest = read_manifest(os.path.join(lout, 'manifest.yaml'))
|
|
1800
|
-
bind_asset_vision_group_scopes(lout, manifest)
|
|
1801
1221
|
if args.style_name:
|
|
1802
1222
|
manifest['name'] = args.style_name
|
|
1803
1223
|
if not manifest.get('name'):
|
|
@@ -1809,37 +1229,15 @@ def main(argv=None):
|
|
|
1809
1229
|
l_fm = split_top_blocks(open(os.path.join(lout, 'frontmatter.yaml'),
|
|
1810
1230
|
encoding='utf-8').read())
|
|
1811
1231
|
body = open(os.path.join(lout, 'body.md'), encoding='utf-8').read()
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
for key, _, lines in layouts_blocks:
|
|
1816
|
-
layout_name = None
|
|
1817
|
-
for line in lines:
|
|
1818
|
-
layout_match = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1819
|
-
if layout_match:
|
|
1820
|
-
layout_name = layout_match.group(1)
|
|
1821
|
-
match = re.search(r'\bsource_media:\s*([^,}]+)', line)
|
|
1822
|
-
if match:
|
|
1823
|
-
source = unquote(match.group(1).strip())
|
|
1824
|
-
bound_sources.add(source)
|
|
1825
|
-
box = source_box(line) or slot_box(line)
|
|
1826
|
-
if key == 'layouts' and box is not None:
|
|
1827
|
-
bound_slots[source].add((layout_name, box))
|
|
1828
|
-
has_asset_judgments = bool(
|
|
1829
|
-
manifest.get('asset_vision_groups') or manifest.get('asset_decisions'))
|
|
1830
|
-
asset_decisions, asset_ids = apply_asset_decisions(
|
|
1831
|
-
manifest,
|
|
1832
|
-
bound_sources=bound_sources if has_asset_judgments else None,
|
|
1833
|
-
bound_slots=bound_slots if has_asset_judgments else None)
|
|
1232
|
+
lay_path = os.path.join(lout, 'layouts.yaml')
|
|
1233
|
+
layouts_blocks = (split_top_blocks(open(lay_path, encoding='utf-8').read())
|
|
1234
|
+
if os.path.exists(lay_path) else None)
|
|
1834
1235
|
canvas = extract['canvas']['px']
|
|
1835
|
-
final_layouts = (build_layouts_md(
|
|
1836
|
-
layouts_blocks, canvas, asset_decisions=asset_decisions, asset_ids=asset_ids)
|
|
1837
|
-
if layouts_blocks is not None else None)
|
|
1838
1236
|
|
|
1839
1237
|
os.makedirs(pack, exist_ok=True)
|
|
1840
1238
|
consumer, audit, copied = place_assets(manifest, extract, stage1, pack)
|
|
1841
1239
|
|
|
1842
|
-
lay_text =
|
|
1240
|
+
lay_text = open(lay_path, encoding='utf-8').read() if os.path.exists(lay_path) else ''
|
|
1843
1241
|
body = expand_placeholders(body, manifest, consumer, audit, extract, lay_text)
|
|
1844
1242
|
design = build_design(manifest, l_fm, consumer, body,
|
|
1845
1243
|
has_sidecar=layouts_blocks is not None, canvas=canvas)
|
|
@@ -1847,7 +1245,7 @@ def main(argv=None):
|
|
|
1847
1245
|
f.write(design)
|
|
1848
1246
|
if layouts_blocks is not None:
|
|
1849
1247
|
with open(os.path.join(pack, 'layouts.md'), 'w', encoding='utf-8') as f:
|
|
1850
|
-
f.write(
|
|
1248
|
+
f.write(build_layouts_md(layouts_blocks, canvas))
|
|
1851
1249
|
|
|
1852
1250
|
# 审计记录写回**抽取工作目录**,不进交付包:包是消费产物,`ref/` 里的东西
|
|
1853
1251
|
# 没有任何门禁读、消费模型也用不上,放进去只会让「别读我」和「几 MB 材料」同时下发。
|
|
@@ -1938,7 +1336,6 @@ def main(argv=None):
|
|
|
1938
1336
|
rc = rc or r1.returncode
|
|
1939
1337
|
if rc:
|
|
1940
1338
|
print('\n门禁未过(exit %d)。回修属 L 层的事:改判断单后重跑本脚本。' % rc)
|
|
1941
|
-
return rc
|
|
1942
1339
|
return rc
|
|
1943
1340
|
|
|
1944
1341
|
|