@lark-apaas/coding-steering 0.1.32-dev.a87aa13 → 0.1.32-dev.b2f659e
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 +28 -19
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +1181 -190
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +229 -10
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +18 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +643 -40
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +19 -3
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +4 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_asset_judgment_package.py +556 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +308 -1
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +14 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_flow_layout_contract.py +157 -7
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +1598 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +479 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_text_role_contract.py +151 -4
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_layout_assets.py +400 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +12 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +1 -1
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify that generated slides honor every asset bound to their PPTX layout."""
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import posixpath
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from html.parser import HTMLParser
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
|
|
13
|
+
from check_v2 import Pack
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
URL_RE = re.compile(r'url\(\s*[\'"]?([^\'")\s]+)', re.I)
|
|
17
|
+
VOID_TAGS = {
|
|
18
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
19
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def normalized_path(value):
|
|
24
|
+
path = urlsplit(value).path.replace('\\', '/')
|
|
25
|
+
return posixpath.normpath(path).lstrip('./')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def bound_asset_ids(value, known_asset_ids):
|
|
29
|
+
found = Counter()
|
|
30
|
+
if isinstance(value, dict):
|
|
31
|
+
for key, child in value.items():
|
|
32
|
+
if (key in ('asset', 'background')
|
|
33
|
+
and isinstance(child, str)
|
|
34
|
+
and child in known_asset_ids):
|
|
35
|
+
found[child] += 1
|
|
36
|
+
found.update(bound_asset_ids(child, known_asset_ids))
|
|
37
|
+
elif isinstance(value, list):
|
|
38
|
+
for child in value:
|
|
39
|
+
found.update(bound_asset_ids(child, known_asset_ids))
|
|
40
|
+
return found
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def layout_asset_contract(pack):
|
|
44
|
+
known_asset_ids = set(pack.assets)
|
|
45
|
+
owners = {}
|
|
46
|
+
for layout_name, (layout, _) in pack.layouts.items():
|
|
47
|
+
for asset_id, _, _ in layout_asset_instances(
|
|
48
|
+
layout, known_asset_ids, pack.canvas):
|
|
49
|
+
owners.setdefault(asset_id, set()).add(layout_name)
|
|
50
|
+
return owners
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def asset_urls(pack, asset_prefix, asset_ids):
|
|
54
|
+
prefix = normalized_path(asset_prefix).rstrip('/')
|
|
55
|
+
urls = {}
|
|
56
|
+
for asset_id in asset_ids:
|
|
57
|
+
entry, _ = pack.assets.get(asset_id, (None, None))
|
|
58
|
+
if not isinstance(entry, dict):
|
|
59
|
+
continue
|
|
60
|
+
path = entry.get('path')
|
|
61
|
+
if not isinstance(path, str) or not path:
|
|
62
|
+
continue
|
|
63
|
+
relative = normalized_path(path)
|
|
64
|
+
if relative.startswith('assets/'):
|
|
65
|
+
relative = relative[len('assets/'):]
|
|
66
|
+
urls.setdefault(posixpath.join(prefix, relative), set()).add(asset_id)
|
|
67
|
+
return urls
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def bound_asset_instances(value, known_asset_ids):
|
|
71
|
+
"""Return every positioned fixed-asset instance nested in slots or flow."""
|
|
72
|
+
instances = []
|
|
73
|
+
if isinstance(value, dict):
|
|
74
|
+
box = value.get('box')
|
|
75
|
+
asset_id = value.get('asset')
|
|
76
|
+
if (isinstance(box, list) and len(box) == 4
|
|
77
|
+
and isinstance(asset_id, str)
|
|
78
|
+
and asset_id in known_asset_ids):
|
|
79
|
+
instances.append((asset_id, value.get('role') or 'asset', box))
|
|
80
|
+
for key, child in value.items():
|
|
81
|
+
if key not in ('asset', 'background'):
|
|
82
|
+
instances.extend(bound_asset_instances(child, known_asset_ids))
|
|
83
|
+
elif isinstance(value, list):
|
|
84
|
+
for child in value:
|
|
85
|
+
instances.extend(bound_asset_instances(child, known_asset_ids))
|
|
86
|
+
return instances
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def layout_asset_instances(layout, known_asset_ids, canvas):
|
|
90
|
+
"""Return every fixed image instance declared by one layout."""
|
|
91
|
+
if not isinstance(layout, dict):
|
|
92
|
+
return []
|
|
93
|
+
instances = []
|
|
94
|
+
background = layout.get('background')
|
|
95
|
+
if canvas and isinstance(background, str) and background in known_asset_ids:
|
|
96
|
+
instances.append(
|
|
97
|
+
(background, 'background', [0, 0, canvas[0], canvas[1]]))
|
|
98
|
+
instances.extend(bound_asset_instances(layout, known_asset_ids))
|
|
99
|
+
return instances
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def inline_styles(value):
|
|
103
|
+
styles = {}
|
|
104
|
+
for declaration in (value or '').split(';'):
|
|
105
|
+
if ':' not in declaration:
|
|
106
|
+
continue
|
|
107
|
+
name, raw = declaration.split(':', 1)
|
|
108
|
+
styles[name.strip().lower()] = re.sub(
|
|
109
|
+
r'\s*!important\s*$', '', raw.strip().lower())
|
|
110
|
+
return styles
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def css_number(value):
|
|
114
|
+
match = re.fullmatch(r'(-?(?:\d+(?:\.\d*)?|\.\d+))(?:px)?', value or '')
|
|
115
|
+
return float(match.group(1)) if match else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def element_box(reference, canvas):
|
|
119
|
+
styles = reference['styles']
|
|
120
|
+
if reference['slide_root'] and not any(
|
|
121
|
+
name in styles for name in ('left', 'top', 'width', 'height')):
|
|
122
|
+
return [0.0, 0.0, float(canvas[0]), float(canvas[1])]
|
|
123
|
+
values = [
|
|
124
|
+
css_number(styles.get(name))
|
|
125
|
+
for name in ('left', 'top', 'width', 'height')
|
|
126
|
+
]
|
|
127
|
+
if styles.get('position') != 'absolute' or any(
|
|
128
|
+
value is None for value in values):
|
|
129
|
+
return None
|
|
130
|
+
return values
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def boxes_match(actual, expected, tolerance=1.0):
|
|
134
|
+
return actual is not None and all(
|
|
135
|
+
abs(actual_value - expected_value) <= tolerance
|
|
136
|
+
for actual_value, expected_value in zip(actual, expected)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def file_sha256(path):
|
|
141
|
+
digest = hashlib.sha256()
|
|
142
|
+
with open(path, 'rb') as stream:
|
|
143
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
|
144
|
+
digest.update(chunk)
|
|
145
|
+
return digest.hexdigest()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def copied_asset_problems(pack, html_path, asset_prefix, asset_ids):
|
|
149
|
+
"""Verify copied fixed assets still contain the source PPTX bytes."""
|
|
150
|
+
problems = []
|
|
151
|
+
html_root = os.path.dirname(os.path.abspath(html_path))
|
|
152
|
+
prefix = normalized_path(asset_prefix).rstrip('/')
|
|
153
|
+
for asset_id in sorted(asset_ids):
|
|
154
|
+
entry, _ = pack.assets.get(asset_id, (None, None))
|
|
155
|
+
path = entry.get('path') if isinstance(entry, dict) else None
|
|
156
|
+
if not isinstance(path, str) or not path:
|
|
157
|
+
continue
|
|
158
|
+
relative = normalized_path(path)
|
|
159
|
+
if relative.startswith('assets/'):
|
|
160
|
+
relative = relative[len('assets/'):]
|
|
161
|
+
source_path = os.path.join(pack.root, 'assets', *relative.split('/'))
|
|
162
|
+
copied_relative = posixpath.join(prefix, relative)
|
|
163
|
+
copied_path = os.path.join(html_root, *copied_relative.split('/'))
|
|
164
|
+
if not os.path.isfile(copied_path):
|
|
165
|
+
problems.append(
|
|
166
|
+
'固定素材 %s 未复制到项目: %s' % (asset_id, copied_relative))
|
|
167
|
+
continue
|
|
168
|
+
if not os.path.isfile(source_path):
|
|
169
|
+
problems.append(
|
|
170
|
+
'风格包中的固定素材 %s 不存在: %s' % (asset_id, path))
|
|
171
|
+
continue
|
|
172
|
+
if file_sha256(copied_path) != file_sha256(source_path):
|
|
173
|
+
problems.append(
|
|
174
|
+
'固定素材 %s 已被替换或改写,必须使用 PPTX 原文件' % asset_id)
|
|
175
|
+
return problems
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def urls_from_attrs(attrs):
|
|
179
|
+
urls = []
|
|
180
|
+
for key, value in attrs:
|
|
181
|
+
if not value:
|
|
182
|
+
continue
|
|
183
|
+
if key.lower() in ('src', 'href'):
|
|
184
|
+
urls.append(value)
|
|
185
|
+
elif key.lower() == 'srcset':
|
|
186
|
+
urls.extend(item.strip().split(' ', 1)[0] for item in value.split(','))
|
|
187
|
+
elif key.lower() == 'style':
|
|
188
|
+
urls.extend(URL_RE.findall(value))
|
|
189
|
+
return urls
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SlideAssetParser(HTMLParser):
|
|
193
|
+
def __init__(self):
|
|
194
|
+
super().__init__()
|
|
195
|
+
self.slides = []
|
|
196
|
+
self._stack = []
|
|
197
|
+
self._style_depth = 0
|
|
198
|
+
self.outside_urls = []
|
|
199
|
+
|
|
200
|
+
def handle_starttag(self, tag, attrs):
|
|
201
|
+
tag = tag.lower()
|
|
202
|
+
attrs_map = {key.lower(): value for key, value in attrs}
|
|
203
|
+
urls = urls_from_attrs(attrs)
|
|
204
|
+
parent = self._stack[-1] if self._stack else None
|
|
205
|
+
parent_slide = parent['slide'] if parent else None
|
|
206
|
+
slide_root = bool(
|
|
207
|
+
tag == 'section' and parent and parent['tag'] == 'deck-stage')
|
|
208
|
+
slide = ({
|
|
209
|
+
'layout': attrs_map.get('data-pptx-layout'),
|
|
210
|
+
'references': [],
|
|
211
|
+
} if slide_root else parent_slide)
|
|
212
|
+
if slide_root:
|
|
213
|
+
self.slides.append(slide)
|
|
214
|
+
styles = inline_styles(attrs_map.get('style'))
|
|
215
|
+
hidden = bool(
|
|
216
|
+
(parent and parent['hidden'])
|
|
217
|
+
or 'hidden' in attrs_map
|
|
218
|
+
or attrs_map.get('aria-hidden', '').lower() == 'true'
|
|
219
|
+
or styles.get('display') == 'none'
|
|
220
|
+
or styles.get('visibility') in ('hidden', 'collapse')
|
|
221
|
+
or styles.get('content-visibility') == 'hidden'
|
|
222
|
+
or css_number(styles.get('width')) == 0
|
|
223
|
+
or css_number(styles.get('height')) == 0
|
|
224
|
+
or (
|
|
225
|
+
css_number(styles.get('opacity')) is not None
|
|
226
|
+
and css_number(styles.get('opacity')) <= 0
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if slide is not None and (urls or attrs_map.get('data-pptx-asset')):
|
|
230
|
+
slide['references'].append({
|
|
231
|
+
'asset': attrs_map.get('data-pptx-asset'),
|
|
232
|
+
'hidden': hidden,
|
|
233
|
+
'slide_root': slide_root,
|
|
234
|
+
'source': attrs_map.get('src'),
|
|
235
|
+
'styles': styles,
|
|
236
|
+
'tag': tag,
|
|
237
|
+
'urls': urls,
|
|
238
|
+
})
|
|
239
|
+
elif urls:
|
|
240
|
+
self.outside_urls.extend(urls)
|
|
241
|
+
if tag == 'style':
|
|
242
|
+
self._style_depth += 1
|
|
243
|
+
if tag not in VOID_TAGS:
|
|
244
|
+
self._stack.append({
|
|
245
|
+
'hidden': hidden,
|
|
246
|
+
'slide': slide,
|
|
247
|
+
'tag': tag,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
def handle_startendtag(self, tag, attrs):
|
|
251
|
+
self.handle_starttag(tag, attrs)
|
|
252
|
+
if tag.lower() not in VOID_TAGS:
|
|
253
|
+
self.handle_endtag(tag)
|
|
254
|
+
|
|
255
|
+
def handle_endtag(self, tag):
|
|
256
|
+
tag = tag.lower()
|
|
257
|
+
if tag == 'style' and self._style_depth:
|
|
258
|
+
self._style_depth -= 1
|
|
259
|
+
if tag not in VOID_TAGS and self._stack:
|
|
260
|
+
self._stack.pop()
|
|
261
|
+
|
|
262
|
+
def handle_data(self, data):
|
|
263
|
+
if not self._style_depth:
|
|
264
|
+
return
|
|
265
|
+
self.outside_urls.extend(URL_RE.findall(data))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def validate_layout_assets(pack_dir, html_path, asset_prefix):
|
|
269
|
+
"""Return violations of the asset contract declared by each layout."""
|
|
270
|
+
pack = Pack(pack_dir)
|
|
271
|
+
owners = layout_asset_contract(pack)
|
|
272
|
+
known_urls = asset_urls(pack, asset_prefix, owners)
|
|
273
|
+
if not known_urls:
|
|
274
|
+
return []
|
|
275
|
+
asset_urls_by_id = {
|
|
276
|
+
asset_id: url
|
|
277
|
+
for url, asset_ids in known_urls.items()
|
|
278
|
+
for asset_id in asset_ids
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
with open(html_path, encoding='utf-8') as stream:
|
|
282
|
+
text = stream.read()
|
|
283
|
+
parser = SlideAssetParser()
|
|
284
|
+
parser.feed(text)
|
|
285
|
+
parser.close()
|
|
286
|
+
|
|
287
|
+
problems = copied_asset_problems(pack, html_path, asset_prefix, owners)
|
|
288
|
+
for url in parser.outside_urls:
|
|
289
|
+
for asset_id in sorted(known_urls.get(normalized_path(url), ())):
|
|
290
|
+
problems.append(
|
|
291
|
+
'模板资产 %s 出现在 slide section 外,无法核验页型归属' % asset_id)
|
|
292
|
+
for number, slide in enumerate(parser.slides, 1):
|
|
293
|
+
layout = slide['layout']
|
|
294
|
+
if not layout:
|
|
295
|
+
problems.append('第 %d 页缺少 data-pptx-layout,无法核验模板资产归属' % number)
|
|
296
|
+
continue
|
|
297
|
+
if layout not in pack.layouts:
|
|
298
|
+
problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
|
|
299
|
+
continue
|
|
300
|
+
expected = layout_asset_instances(
|
|
301
|
+
pack.layouts[layout][0], set(pack.assets), pack.canvas)
|
|
302
|
+
actual = []
|
|
303
|
+
for reference in slide['references']:
|
|
304
|
+
referenced_ids = set()
|
|
305
|
+
normalized_urls = [normalized_path(url) for url in reference['urls']]
|
|
306
|
+
for url in normalized_urls:
|
|
307
|
+
referenced_ids.update(known_urls.get(url, ()))
|
|
308
|
+
asset_id = reference['asset']
|
|
309
|
+
if not asset_id:
|
|
310
|
+
for referenced_id in sorted(referenced_ids):
|
|
311
|
+
problems.append(
|
|
312
|
+
'第 %d 页模板资产 %s 缺少 data-pptx-asset 实例标记'
|
|
313
|
+
% (number, referenced_id))
|
|
314
|
+
continue
|
|
315
|
+
if asset_id not in pack.assets:
|
|
316
|
+
problems.append(
|
|
317
|
+
'第 %d 页声明了不存在的模板资产: %s' % (number, asset_id))
|
|
318
|
+
continue
|
|
319
|
+
expected_url = asset_urls_by_id.get(asset_id)
|
|
320
|
+
uses_expected_source = (
|
|
321
|
+
len(normalized_urls) == 1
|
|
322
|
+
and normalized_urls[0] == expected_url
|
|
323
|
+
and (
|
|
324
|
+
reference['tag'] != 'img'
|
|
325
|
+
or (
|
|
326
|
+
reference['source']
|
|
327
|
+
and normalized_path(reference['source']) == expected_url
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
if not uses_expected_source:
|
|
332
|
+
problems.append(
|
|
333
|
+
'第 %d 页固定实例 %s 未引用对应的 PPTX 原素材'
|
|
334
|
+
% (number, asset_id))
|
|
335
|
+
if reference['hidden']:
|
|
336
|
+
problems.append(
|
|
337
|
+
'第 %d 页固定实例 %s 不可隐藏' % (number, asset_id))
|
|
338
|
+
actual.append({
|
|
339
|
+
'asset': asset_id,
|
|
340
|
+
'box': element_box(reference, pack.canvas),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
used_asset_counts = Counter(instance['asset'] for instance in actual)
|
|
344
|
+
for asset_id in sorted(used_asset_counts):
|
|
345
|
+
if layout not in owners.get(asset_id, set()):
|
|
346
|
+
allowed = '、'.join(sorted(owners.get(asset_id) or ())) or '(无)'
|
|
347
|
+
problems.append(
|
|
348
|
+
'第 %d 页页型 %s 不得使用 %s;只允许: %s'
|
|
349
|
+
% (number, layout, asset_id, allowed))
|
|
350
|
+
unmatched = list(actual)
|
|
351
|
+
for asset_id, _, expected_box in expected:
|
|
352
|
+
matching_index = next((
|
|
353
|
+
index for index, instance in enumerate(unmatched)
|
|
354
|
+
if instance['asset'] == asset_id
|
|
355
|
+
and boxes_match(instance['box'], expected_box)
|
|
356
|
+
), None)
|
|
357
|
+
if matching_index is not None:
|
|
358
|
+
unmatched.pop(matching_index)
|
|
359
|
+
continue
|
|
360
|
+
same_asset = next((
|
|
361
|
+
instance for instance in unmatched
|
|
362
|
+
if instance['asset'] == asset_id
|
|
363
|
+
), None)
|
|
364
|
+
if same_asset:
|
|
365
|
+
problems.append(
|
|
366
|
+
'第 %d 页固定实例 %s 的位置尺寸必须为 %s,当前为 %s'
|
|
367
|
+
% (number, asset_id, expected_box, same_asset['box']))
|
|
368
|
+
unmatched.remove(same_asset)
|
|
369
|
+
else:
|
|
370
|
+
problems.append(
|
|
371
|
+
'第 %d 页页型 %s 缺少固定实例 %s,位置尺寸应为 %s'
|
|
372
|
+
% (number, layout, asset_id, expected_box))
|
|
373
|
+
for instance in unmatched:
|
|
374
|
+
if layout in owners.get(instance['asset'], set()):
|
|
375
|
+
problems.append(
|
|
376
|
+
'第 %d 页页型 %s 额外使用了固定实例 %s'
|
|
377
|
+
% (number, layout, instance['asset']))
|
|
378
|
+
return problems
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv=None):
|
|
382
|
+
parser = argparse.ArgumentParser(
|
|
383
|
+
description='Verify that generated deck HTML honors PPTX layout asset bindings.')
|
|
384
|
+
parser.add_argument('pack_dir')
|
|
385
|
+
parser.add_argument('html_path')
|
|
386
|
+
parser.add_argument('--asset-prefix', required=True)
|
|
387
|
+
args = parser.parse_args(argv)
|
|
388
|
+
|
|
389
|
+
problems = validate_layout_assets(args.pack_dir, args.html_path, args.asset_prefix)
|
|
390
|
+
if not problems:
|
|
391
|
+
print('PPTX_LAYOUT_ASSETS: PASS')
|
|
392
|
+
return 0
|
|
393
|
+
print('PPTX_LAYOUT_ASSETS: FAIL count=%d' % len(problems))
|
|
394
|
+
for problem in problems:
|
|
395
|
+
print('[layoutAssets] %s' % problem)
|
|
396
|
+
return 1
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == '__main__':
|
|
400
|
+
sys.exit(main())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Backward-compatible entrypoint for PPTX layout asset verification."""
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from verify_layout_assets import main, validate_layout_assets
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
validate_logo_scope = validate_layout_assets
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__':
|
|
12
|
+
sys.exit(main())
|
|
@@ -133,7 +133,7 @@ layouts:
|
|
|
133
133
|
|
|
134
134
|
- **`background` 三形态**:`<asset-id>` / `{<theme>: <asset-id>}` / `{color: <colors-token>}`(`color` 是保留键,主题名禁止叫 color)。`asset` 两形态:`<asset-id>` / `{<theme>: <asset-id>}`。
|
|
135
135
|
- **背景安全扩展**:有真实背景图的 archetype 建议写 `text_safe: [x,y,w,h]`、`avoid: [{box: [x,y,w,h], reason: "..."}]`、`pairing_rule: "..."`。这些是消费约束,不参与封闭枚举;用于避免标题、正文、图表、卡片、表格、时间线及其容器外接矩形覆盖背景视觉主体、强光斑或深色透明区;透明容器也不能跨进禁放区。
|
|
136
|
-
- **流式页型**:内容长度会变化的内容页可用 `flow.regions` 表达纵向区带。`stack` 表达单列顺序,`grid` 表达并列列组,`free` 中的 item 必须带 `box`,用于 logo、页码、页眉和页脚等固定锚点。并列卡片可在 `grid.items` 中使用一层 `{role: group, css, gap, items}`:group 的 `css` 是卡片容器样式,内部 `items` 按顺序排布;不继续嵌套 group
|
|
136
|
+
- **流式页型**:内容长度会变化的内容页可用 `flow.regions` 表达纵向区带。`stack` 表达单列顺序,`grid` 表达并列列组,`free` 中的 item 必须带 `box`,用于 logo、页码、页眉和页脚等固定锚点。并列卡片可在 `grid.items` 中使用一层 `{role: group, css, gap, items}`:group 的 `css` 是卡片容器样式,内部 `items` 按顺序排布;不继续嵌套 group。区带可带自己的 `margin: [左, 右]`,覆盖 `flow` 整块的 `margin`(居中卡片组和贴左标题横向范围本就不同);不带则继承整块 `margin`。纵向位置与留白由消费模型结合实际内容决定,不把样张的 `y` 坐标当作流式硬约束。
|
|
137
137
|
- **`decor`(可选)**:这一页无文字的图形骨架——图标托底的圆、卡片、分隔线。每条 `{box, geom, css}`:`box` 定位,`css` 是可直接写进 style 的声明串,`geom` 取源形状的 prst(`ellipse` 另加 `border-radius: 50%`)。圆角以每条 `css` 为准,没有 `border-radius` 就按 `0`;不得因 `geom: roundRect` 自行补圆角,因为 OOXML 的 roundRect 可以有零圆角调节点。层级在背景之上、`slots` 之下;带 `asset` 的槽落在 decor 之上是版式本意,不算重叠。
|
|
138
138
|
- **slot 样式契约**:`box` 只承载 `[x,y,w,h]` 几何;可渲染属性统一放进 `css`,并可直接写入 HTML `style`。PPTX `bodyPr.insets_px` 转成 `box-sizing: border-box; padding: ...`,字号/字重/颜色/水平与垂直对齐/行高/字距/旋转分别转成标准 CSS。禁止在 slot 中输出 `size` / `weight` / `color` / `align` / `valign` / `insets_px` 等旧字段。
|
|
139
139
|
- **文本角色判断**:脚本把实例页及其引用版式中的现有文本槽、几何和 CSS 完整写入草案;`text_roles` 只供模型把这些槽判断为 `title | subtitle | header | footer | body`,不控制槽位去留。判断不清时用 `body`,不归纳模板中不存在的标题、页眉或页脚。
|