@lark-apaas/coding-steering 0.1.18-dev.e6a2787 → 0.1.18-dev.ec7b8c1
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 +4 -3
- 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 +404 -82
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +146 -24
- 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 +60 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py +63 -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 +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 +13 -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
|
@@ -98,7 +98,8 @@ L 层判断单 schema(<l-out-dir> 四个文件,这段就是填写说明书
|
|
|
98
98
|
role: cover
|
|
99
99
|
background: bg-cover
|
|
100
100
|
slots:
|
|
101
|
-
- {role: title, box: [<x>, <y>, <w>, <h>], type: title
|
|
101
|
+
- {role: title, box: [<x>, <y>, <w>, <h>], type: title,
|
|
102
|
+
css: "<由源模板转译出的 CSS 声明串>"}
|
|
102
103
|
confidence: high
|
|
103
104
|
|
|
104
105
|
可选 `body:` 块标量 —— 追加到 layouts.md frontmatter 之后作为说明正文。
|
|
@@ -507,7 +508,7 @@ def render_assets_block(consumer):
|
|
|
507
508
|
|
|
508
509
|
# ------------------------------------------------- 数值可追溯机检(抄错即 FAIL)
|
|
509
510
|
HEX_RE = re.compile(r'#([0-9A-Fa-f]{6})\b')
|
|
510
|
-
FONTSIZE_RE = re.compile(r'\
|
|
511
|
+
FONTSIZE_RE = re.compile(r'\b(?:fontSize|font-size):\s*([\d.]+)px')
|
|
511
512
|
BOX_RE = re.compile(r'\bbox:\s*\[\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*,'
|
|
512
513
|
r'\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\]')
|
|
513
514
|
KEYLINE_RE = re.compile(r'^(\s*)-?\s*([A-Za-z_][\w-]*):\s*(.*)$')
|
|
@@ -902,6 +903,64 @@ def build_design(manifest, l_frontmatter, consumer, body, has_sidecar, canvas):
|
|
|
902
903
|
return text
|
|
903
904
|
|
|
904
905
|
|
|
906
|
+
def layout_forms(lines):
|
|
907
|
+
"""每个页型草案里实际给了哪几种形态(flow / slots)。"""
|
|
908
|
+
out, cur = {}, None
|
|
909
|
+
for line in lines:
|
|
910
|
+
m = re.match(r'^ ([\w-]+):\s*$', line)
|
|
911
|
+
if m:
|
|
912
|
+
cur = m.group(1)
|
|
913
|
+
out[cur] = set()
|
|
914
|
+
continue
|
|
915
|
+
m = re.match(r'^ (flow|slots):\s*$', line)
|
|
916
|
+
if m and cur:
|
|
917
|
+
out[cur].add(m.group(1))
|
|
918
|
+
return out
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def layout_modes(lines):
|
|
922
|
+
"""读取扁平 `layout_modes:` 判断区。"""
|
|
923
|
+
out = {}
|
|
924
|
+
for line in lines:
|
|
925
|
+
m = re.match(r'^\s{2}([\w-]+):\s*([^\s#]+)', line)
|
|
926
|
+
if m:
|
|
927
|
+
out[m.group(1)] = m.group(2).strip()
|
|
928
|
+
return out
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def text_role_boxes(lines):
|
|
932
|
+
"""从 slots 里的同名标记取回文本槽坐标,供 flow 固定锚点复用。"""
|
|
933
|
+
out, pending = {}, None
|
|
934
|
+
for line in lines:
|
|
935
|
+
marker = re.match(r'^\s*#\s*text-role:\s*([\w-]+)\s*$', line)
|
|
936
|
+
if marker:
|
|
937
|
+
pending = marker.group(1)
|
|
938
|
+
continue
|
|
939
|
+
if pending:
|
|
940
|
+
box = re.search(r'\bbox:\s*(\[[^\]]+\])', line)
|
|
941
|
+
if box:
|
|
942
|
+
out[pending] = box.group(1)
|
|
943
|
+
pending = None
|
|
944
|
+
return out
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def select_layout_forms(lines, modes):
|
|
948
|
+
"""按 `layout_modes` 只保留每个页型选中的 flow 或 slots。"""
|
|
949
|
+
out, layout, drop = [], None, False
|
|
950
|
+
for line in lines:
|
|
951
|
+
layout_match = re.match(r'^ ([\w-]+):\s*$', line)
|
|
952
|
+
form_match = re.match(r'^ (flow|slots):\s*$', line)
|
|
953
|
+
if layout_match:
|
|
954
|
+
layout, drop = layout_match.group(1), False
|
|
955
|
+
elif form_match:
|
|
956
|
+
drop = modes.get(layout) not in (None, form_match.group(1))
|
|
957
|
+
elif drop and re.match(r'^ \S', line):
|
|
958
|
+
drop = False
|
|
959
|
+
if not drop:
|
|
960
|
+
out.append(line)
|
|
961
|
+
return out
|
|
962
|
+
|
|
963
|
+
|
|
905
964
|
def shrink_safe_area(lines):
|
|
906
965
|
"""text_safe 与 avoid 相交时把安全区收掉重叠的那部分。
|
|
907
966
|
|
|
@@ -960,8 +1019,8 @@ def build_layouts_md(layouts_blocks, canvas):
|
|
|
960
1019
|
raise Fail('layouts.yaml 不要写 canvas —— 脚本从 extract.json 取')
|
|
961
1020
|
if 'layouts' not in blocks:
|
|
962
1021
|
raise Fail('layouts.yaml 缺顶层键 `layouts:`')
|
|
963
|
-
# `names:` / `bg_rules:` 是给 L
|
|
964
|
-
# archetype,本身不进产物。让 L 层只改扁平键值,别去动 layouts 里的
|
|
1022
|
+
# `names:` / `roles:` / `text_roles:` / `layout_modes:` / `bg_rules:` 是给 L 层集中填判断的
|
|
1023
|
+
# 扁平区——在这里并回各 archetype,本身不进产物。让 L 层只改扁平键值,别去动 layouts 里的
|
|
965
1024
|
# slots/confidence 结构(嵌套结构手改极易破坏缩进,进而静默改变语义)。
|
|
966
1025
|
names, roles = {}, {}
|
|
967
1026
|
for key, sink in (('names', names), ('roles', roles)):
|
|
@@ -969,6 +1028,17 @@ def build_layouts_md(layouts_blocks, canvas):
|
|
|
969
1028
|
m = re.match(r'^\s{2}([\w-]+):\s*(.+?)\s*$', line)
|
|
970
1029
|
if m:
|
|
971
1030
|
sink[m.group(1)] = unquote(m.group(2))
|
|
1031
|
+
text_roles = {}
|
|
1032
|
+
allowed_text_roles = {'title', 'subtitle', 'header', 'footer', 'body'}
|
|
1033
|
+
for line in blocks.get('text_roles', ('', []))[1]:
|
|
1034
|
+
m = re.match(r'^\s{2}([\w-]+):\s*([A-Za-z-]+)(?:\s+#.*)?$', line)
|
|
1035
|
+
if not m:
|
|
1036
|
+
continue
|
|
1037
|
+
role_id, role = m.groups()
|
|
1038
|
+
if role not in allowed_text_roles:
|
|
1039
|
+
raise Fail('text_roles.%s 取值 %s 非法;应为 %s'
|
|
1040
|
+
% (role_id, role, '|'.join(sorted(allowed_text_roles))))
|
|
1041
|
+
text_roles[role_id] = role
|
|
972
1042
|
bg_rules, cur = {}, None
|
|
973
1043
|
for line in blocks.get('bg_rules', ('', []))[1]:
|
|
974
1044
|
# 键后面允许行内注释(草案会标「用它的页型:…」)
|
|
@@ -990,44 +1060,96 @@ def build_layouts_md(layouts_blocks, canvas):
|
|
|
990
1060
|
raise Fail('%s: %s' % (bg, err))
|
|
991
1061
|
out.append(' %s:' % bg)
|
|
992
1062
|
out += lines
|
|
1063
|
+
modes = layout_modes(blocks.get('layout_modes', ('', []))[1])
|
|
993
1064
|
out.append('layouts:')
|
|
994
|
-
|
|
1065
|
+
pending_text_role = None
|
|
1066
|
+
used_text_roles = set()
|
|
1067
|
+
source_layout_lines = blocks['layouts'][1]
|
|
1068
|
+
role_boxes = text_role_boxes(source_layout_lines)
|
|
1069
|
+
layout_lines = select_layout_forms(source_layout_lines, modes)
|
|
1070
|
+
cur, fixed_items = None, []
|
|
1071
|
+
|
|
1072
|
+
def flush_fixed_items():
|
|
1073
|
+
if not fixed_items:
|
|
1074
|
+
return
|
|
1075
|
+
out.append(' - kind: free')
|
|
1076
|
+
out.append(' items:')
|
|
1077
|
+
out.extend(fixed_items)
|
|
1078
|
+
fixed_items.clear()
|
|
1079
|
+
|
|
1080
|
+
for line in layout_lines:
|
|
1081
|
+
m = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1082
|
+
if m:
|
|
1083
|
+
flush_fixed_items()
|
|
1084
|
+
cur = m.group(1)
|
|
1085
|
+
elif fixed_items and re.match(r'^ \S', line):
|
|
1086
|
+
flush_fixed_items()
|
|
995
1087
|
# 判断单里的结构事实(栅格、间距序列、样张字数、命中配方)是给 L 层判断用的,
|
|
996
|
-
# 不进产物——消费端要的是结论,不是推导过程。
|
|
1088
|
+
# 不进产物——消费端要的是结论,不是推导过程。layout_mode 同理,它是判断的载体。
|
|
997
1089
|
if line.lstrip().startswith('#'):
|
|
1090
|
+
marker = re.match(r'^\s*#\s*text-role:\s*([\w-]+)\s*$', line)
|
|
1091
|
+
if marker:
|
|
1092
|
+
pending_text_role = marker.group(1)
|
|
998
1093
|
continue
|
|
1094
|
+
if pending_text_role:
|
|
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)
|
|
1113
|
+
used_text_roles.add(pending_text_role)
|
|
1114
|
+
pending_text_role = None
|
|
1115
|
+
continue
|
|
1116
|
+
used_text_roles.add(pending_text_role)
|
|
1117
|
+
pending_text_role = None
|
|
999
1118
|
out.append(line)
|
|
1000
|
-
m = re.match(r'^ ([\w-]+):\s*$', line)
|
|
1001
1119
|
if m and m.group(1) in names:
|
|
1002
1120
|
out.append(' name: "%s"' % names.pop(m.group(1)))
|
|
1003
1121
|
if m and m.group(1) in roles:
|
|
1004
1122
|
out.append(' role: %s' % roles.pop(m.group(1)))
|
|
1123
|
+
flush_fixed_items()
|
|
1005
1124
|
if names:
|
|
1006
1125
|
raise Fail('names 里这些页型在 layouts 下找不到:%s' % ', '.join(sorted(names)))
|
|
1007
1126
|
if roles:
|
|
1008
1127
|
raise Fail('roles 里这些页型在 layouts 下找不到:%s' % ', '.join(sorted(roles)))
|
|
1128
|
+
unused_text_roles = set(text_roles) - used_text_roles
|
|
1129
|
+
if unused_text_roles:
|
|
1130
|
+
raise Fail('text_roles 里这些判断没有命中文本槽:%s'
|
|
1131
|
+
% ', '.join(sorted(unused_text_roles)))
|
|
1009
1132
|
missing_role = [k for k in re.findall(r'^ ([\w-]+):\s*$', '\n'.join(blocks['layouts'][1]), re.M)
|
|
1010
1133
|
if not re.search(r'^ %s:\s*$(?:\n(?! \S).*)*?\n role:' % re.escape(k),
|
|
1011
1134
|
'\n'.join(out), re.M)]
|
|
1012
1135
|
if missing_role:
|
|
1013
1136
|
raise Fail('这些页型没有 role(在 layouts.yaml 的 roles 段填):%s' % ', '.join(missing_role))
|
|
1014
|
-
#
|
|
1015
|
-
#
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
if
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
raise Fail('这些页型同时留了 flow 和 slots,二选一删掉另一个:%s' % ', '.join(both))
|
|
1137
|
+
# `layout_modes.*: TODO` 由上面那道通用 TODO 扫描报(它连行内提示一起打出来),
|
|
1138
|
+
# 这里只管它管不到的两种:整行被删、以及填了 flow/slots 之外的值。
|
|
1139
|
+
forms = layout_forms(blocks['layouts'][1])
|
|
1140
|
+
bad = []
|
|
1141
|
+
for k, v in sorted(modes.items()):
|
|
1142
|
+
if len(forms.get(k) or ()) < 2:
|
|
1143
|
+
continue # 只有一份形态,无从选择
|
|
1144
|
+
if v not in ('flow', 'slots'):
|
|
1145
|
+
bad.append('%s 的 layout_modes 判断是 %r' % (k, v))
|
|
1146
|
+
elif v not in forms[k]:
|
|
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)
|
|
1151
|
+
if bad:
|
|
1152
|
+
raise Fail('layout_modes 只能填 flow 或 slots,一个页型一个词:%s' % ';'.join(bad))
|
|
1031
1153
|
declared = set(re.findall(r'^ background:\s*(\S+)\s*$',
|
|
1032
1154
|
'\n'.join(blocks['layouts'][1]), re.M))
|
|
1033
1155
|
stray = set(bg_rules) - declared
|
|
@@ -244,14 +244,10 @@ def _recipe_css(fill, line, radii, effects):
|
|
|
244
244
|
dash = (line or {}).get('dash')
|
|
245
245
|
style = 'dashed' if dash and 'dash' in dash else 'solid'
|
|
246
246
|
out.append('border: %dpx %s %s' % (w, style, lc))
|
|
247
|
-
if radii:
|
|
247
|
+
if radii and all(r >= 1 for r in radii):
|
|
248
248
|
lo, hi = min(radii), max(radii)
|
|
249
249
|
if abs(hi - lo) <= 0.5:
|
|
250
250
|
out.append('border-radius: %gpx' % round(lo, 1))
|
|
251
|
-
else:
|
|
252
|
-
out.append('border-radius: %gpx\x00/* 源内 %g~%gpx 共 %d 档,归一档位由 L11 定 */'
|
|
253
|
-
% (round(sum(radii) / len(radii), 1), round(lo, 1), round(hi, 1),
|
|
254
|
-
len(set(round(r, 1) for r in radii))))
|
|
255
251
|
for e in effects or []:
|
|
256
252
|
if e.get('type') == 'outerShdw':
|
|
257
253
|
col = _css_color(e.get('color')) or 'rgba(0,0,0,0.25)'
|
|
@@ -267,7 +263,7 @@ def _recipe_css(fill, line, radii, effects):
|
|
|
267
263
|
def _sig(fill, line, effects):
|
|
268
264
|
"""分组键 = 填充 + 描边 + 效果。**不含圆角**——OOXML 圆角是 min(w,h) 的百分比,
|
|
269
265
|
同一配方在不同尺寸的卡上绝对 px 必然不同,把它计入键会把一个配方拆成多组;
|
|
270
|
-
|
|
266
|
+
只有组内每个形状都明确共享同一绝对半径时才输出组级圆角,否则留给逐形状 CSS。"""
|
|
271
267
|
f = 'none'
|
|
272
268
|
if isinstance(fill, dict):
|
|
273
269
|
if fill.get('type') == 'solid':
|
|
@@ -305,8 +301,7 @@ def cmd_recipes(a, outdir):
|
|
|
305
301
|
g = groups.setdefault(k, {'n': 0, 'parts': Counter(), 'sizes': [], 'radii': [],
|
|
306
302
|
'fill': fill, 'line': line, 'fx': fx})
|
|
307
303
|
g['n'] += 1
|
|
308
|
-
|
|
309
|
-
g['radii'].append(radius)
|
|
304
|
+
g['radii'].append(radius or 0)
|
|
310
305
|
g['parts'][short(s['part'])] += 1
|
|
311
306
|
b = s.get('box') or {}
|
|
312
307
|
if b.get('w'):
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression test for the generated consumer-facing color contract."""
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from draft import emit_body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ColorContractTest(unittest.TestCase):
|
|
11
|
+
def test_body_allows_supporting_colors_without_a_second_accent_system(self):
|
|
12
|
+
data = {
|
|
13
|
+
'canvas': {'px': [1920, 1080]},
|
|
14
|
+
'counts': {'slides': 1},
|
|
15
|
+
'form_hint': {'form': 3},
|
|
16
|
+
}
|
|
17
|
+
tokens = [
|
|
18
|
+
('primary', {'hex': '#123456'}),
|
|
19
|
+
('surface', {'hex': '#FFFFFF'}),
|
|
20
|
+
]
|
|
21
|
+
fonts = [
|
|
22
|
+
{
|
|
23
|
+
'names': ['Example Sans'],
|
|
24
|
+
'stack': ['Example Sans', 'Noto Sans SC'],
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
roles = {'body': {'sz_px': 36}}
|
|
28
|
+
archetypes = [
|
|
29
|
+
{
|
|
30
|
+
'name': 'content',
|
|
31
|
+
'slots': [],
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
36
|
+
emit_body(
|
|
37
|
+
data,
|
|
38
|
+
tokens,
|
|
39
|
+
fonts,
|
|
40
|
+
roles,
|
|
41
|
+
[],
|
|
42
|
+
archetypes,
|
|
43
|
+
[],
|
|
44
|
+
{},
|
|
45
|
+
output_dir,
|
|
46
|
+
)
|
|
47
|
+
with open(os.path.join(output_dir, 'body.md'), encoding='utf-8') as stream:
|
|
48
|
+
body = stream.read()
|
|
49
|
+
|
|
50
|
+
self.assertIn('允许新增中性色、低彩度辅助色或局部语义色', body)
|
|
51
|
+
self.assertRegex(body, r'正负.*风险.*警告.*状态.*图表序列')
|
|
52
|
+
self.assertIn('必要时可以使用 Colors 之外的颜色', body)
|
|
53
|
+
self.assertIn('不能形成与模板主色竞争的第二强调色', body)
|
|
54
|
+
self.assertIn('色相、明度和饱和度关系', body)
|
|
55
|
+
self.assertIn('高饱和、高对比、大面积或跨页重复', body)
|
|
56
|
+
self.assertIn('标题、关键数字、图表主序列、卡片底色或渐变', body)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == '__main__':
|
|
60
|
+
unittest.main()
|
package/steering/design-html/skills/pptx-style-extract/scripts/test_design_consumer_contract.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for consumer rules emitted into design.md."""
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from draft import emit_body
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DesignConsumerContractTest(unittest.TestCase):
|
|
11
|
+
def test_generated_design_owns_template_consumption_rules(self):
|
|
12
|
+
data = {
|
|
13
|
+
'canvas': {'px': [1920, 1080]},
|
|
14
|
+
'counts': {'slides': 1},
|
|
15
|
+
'form_hint': {'form': 3},
|
|
16
|
+
}
|
|
17
|
+
tokens = [('primary', {'hex': '#123456'})]
|
|
18
|
+
fonts = [
|
|
19
|
+
{
|
|
20
|
+
'names': ['Example Sans'],
|
|
21
|
+
'stack': ['Example Sans', 'Noto Sans SC'],
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
roles = {'body': {'sz_px': 36}}
|
|
25
|
+
assets = [
|
|
26
|
+
{
|
|
27
|
+
'id': 'bg-content',
|
|
28
|
+
'kind': 'background',
|
|
29
|
+
'role': 'content',
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
archetypes = [{'name': 'content', 'slots': []}]
|
|
33
|
+
|
|
34
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
35
|
+
emit_body(
|
|
36
|
+
data,
|
|
37
|
+
tokens,
|
|
38
|
+
fonts,
|
|
39
|
+
roles,
|
|
40
|
+
assets,
|
|
41
|
+
archetypes,
|
|
42
|
+
[],
|
|
43
|
+
{},
|
|
44
|
+
output_dir,
|
|
45
|
+
)
|
|
46
|
+
with open(os.path.join(output_dir, 'body.md'), encoding='utf-8') as stream:
|
|
47
|
+
body = stream.read()
|
|
48
|
+
|
|
49
|
+
self.assertIn('是全局 token', body)
|
|
50
|
+
self.assertIn('局部 slot / decor 的 `css` 优先', body)
|
|
51
|
+
self.assertIn('字体使用 Typography 的完整栈与降级', body)
|
|
52
|
+
self.assertIn('将包内 `assets/` 复制到项目内相对目录', body)
|
|
53
|
+
self.assertIn('本机绝对路径', body)
|
|
54
|
+
self.assertIn('背景、资产和本段规则均来自本风格包', body)
|
|
55
|
+
self.assertIn('页面无资源加载失败、内容溢出或画幅裁切', body)
|
|
56
|
+
self.assertIn('沿用该页型已有的标题层级与局部 `css`', body)
|
|
57
|
+
self.assertIn('背景中已经可见的固定标题不再创建文本', body)
|
|
58
|
+
self.assertIn('没有 `subtitle` 槽就不新增副标题', body)
|
|
59
|
+
self.assertIn('区带自带 `margin: [左, 右]` 时用它的', body)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
unittest.main()
|