@lark-apaas/coding-steering 0.1.18-dev.ac6c20c → 0.1.18-dev.c17a349
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/README.md +19 -21
- package/package.json +1 -1
- package/steering/design-html/skills/animated-video/SKILL.md +2 -2
- package/steering/design-html/skills/charts/SKILL.md +52 -7
- package/steering/design-html/skills/{data-report → data-viz}/SKILL.md +65 -9
- package/steering/design-html/skills/frontend-design/SKILL.md +2 -2
- package/steering/design-html/skills/interactive-prototype/SKILL.md +35 -2
- package/steering/design-html/skills/mini-game/SKILL.md +71 -0
- package/steering/design-html/skills/mini-game/references/three-js.md +54 -0
- package/steering/design-html/skills/pptx-style-extract/SKILL.md +148 -0
- package/steering/design-html/skills/pptx-style-extract/font-fallback.yaml +129 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +961 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/check_v2.py +1052 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +2498 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_md.py +75 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/export_consumer_zip.py +175 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +1068 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/ooxml.py +716 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/package.py +1343 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/parts.py +464 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/query.py +557 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/render_pages.py +685 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_background_composite.py +57 -0
- 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 +127 -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/scripts/verify_font.py +68 -0
- package/steering/design-html/skills/pptx-style-extract/v2-format-spec.md +205 -0
- package/steering/design-html/skills/preflight/SKILL.md +51 -0
- package/steering/design-html/skills/preflight/scripts/probe.sh +108 -0
- package/steering/design-html/skills/slide-deck/SKILL.md +160 -0
- package/steering/design-html/skills/slide-deck/scripts/check_local_references.py +179 -0
- package/steering/design-html/skills/{visual-exposure → visual-report}/SKILL.md +24 -2
- package/steering/nestjs-react-fullstack/skills/plugin-guide/SKILL.md +5 -3
- package/steering/nestjs-react-fullstack/skills_common/trigger-guide/SKILL.md +180 -0
- package/steering/nestjs-react-fullstack/{skills/trigger-guide/SKILL.md → skills_common/trigger-guide/references/trigger-lifecycle.md} +11 -162
- 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
- package/steering/design-html/skills/make-a-deck/SKILL.md +0 -180
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for the consumer-facing layout slot CSS contract."""
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
+
|
|
10
|
+
from check_v2 import Pack, rule_v2_16 # noqa: E402
|
|
11
|
+
from draft import emit_layouts, slot_style # noqa: E402
|
|
12
|
+
from package import FONTSIZE_RE, build_layouts_md, split_top_blocks # noqa: E402
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LayoutCssTest(unittest.TestCase):
|
|
16
|
+
def test_text_slot_emits_rendering_style_as_css_only(self):
|
|
17
|
+
shape = {
|
|
18
|
+
'text': {
|
|
19
|
+
'bodyPr': {
|
|
20
|
+
'anchor': 'ctr',
|
|
21
|
+
'insets_px': {'lIns': 12, 'tIns': 8, 'rIns': 10, 'bIns': 6},
|
|
22
|
+
'rot': '900000',
|
|
23
|
+
},
|
|
24
|
+
'lstStyle': {
|
|
25
|
+
'lvl1pPr': {
|
|
26
|
+
'sz_px': 48,
|
|
27
|
+
'weight': 600,
|
|
28
|
+
'italic': True,
|
|
29
|
+
'underline': 'sng',
|
|
30
|
+
'strike': 'sngStrike',
|
|
31
|
+
'spc_px': 1.5,
|
|
32
|
+
'color': {'resolved': '#123456'},
|
|
33
|
+
'algn': 'ctr',
|
|
34
|
+
'lnSpc': {'mult': 1.0},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
'paragraphs': [],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
slot = {
|
|
41
|
+
'role': 'title',
|
|
42
|
+
'type': 'title',
|
|
43
|
+
'box': [100, 80, 800, 160],
|
|
44
|
+
'sz': 48,
|
|
45
|
+
'txt': '标题',
|
|
46
|
+
}
|
|
47
|
+
slot.update(slot_style(shape))
|
|
48
|
+
archetype = {
|
|
49
|
+
'name': 'layout-1',
|
|
50
|
+
'zh': '标题页',
|
|
51
|
+
'role': 'content',
|
|
52
|
+
'bg': None,
|
|
53
|
+
'slots': [slot],
|
|
54
|
+
'decor': [],
|
|
55
|
+
'pages': [1],
|
|
56
|
+
'rep': 1,
|
|
57
|
+
'pic_n': 0,
|
|
58
|
+
'confidence': 'high',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
62
|
+
emit_layouts([archetype], output_dir)
|
|
63
|
+
with open(os.path.join(output_dir, 'layouts.yaml'), encoding='utf-8') as stream:
|
|
64
|
+
layouts_yaml = stream.read()
|
|
65
|
+
layouts_md = build_layouts_md(split_top_blocks(layouts_yaml), (1920, 1080))
|
|
66
|
+
|
|
67
|
+
self.assertIn(
|
|
68
|
+
'css: "box-sizing: border-box; padding: 8px 10px 6px 12px; '
|
|
69
|
+
'font-size: 48px; font-weight: 600; font-style: italic; '
|
|
70
|
+
'text-decoration: underline line-through; letter-spacing: 1.5px; color: #123456; '
|
|
71
|
+
'text-align: center; line-height: 1.2; display: flex; '
|
|
72
|
+
'flex-direction: column; justify-content: center; rotate: 15deg"',
|
|
73
|
+
layouts_md,
|
|
74
|
+
)
|
|
75
|
+
for legacy_key in ('size', 'weight', 'color', 'align', 'valign', 'insets_px'):
|
|
76
|
+
self.assertNotRegex(layouts_md, rf'[,{{]\s*{legacy_key}:')
|
|
77
|
+
self.assertEqual(FONTSIZE_RE.findall(layouts_md), ['48'])
|
|
78
|
+
|
|
79
|
+
def test_layout_gate_rejects_legacy_slot_style_keys(self):
|
|
80
|
+
with tempfile.TemporaryDirectory() as pack_dir:
|
|
81
|
+
with open(os.path.join(pack_dir, 'design.md'), 'w', encoding='utf-8') as stream:
|
|
82
|
+
stream.write('---\nversion: alpha\nlayouts: layouts.md\n---\n\nRead layouts.md.\n')
|
|
83
|
+
with open(os.path.join(pack_dir, 'layouts.md'), 'w', encoding='utf-8') as stream:
|
|
84
|
+
stream.write(
|
|
85
|
+
'---\ncanvas: 1920x1080\nlayouts:\n cover:\n role: cover\n'
|
|
86
|
+
' slots:\n - {role: title, type: title, box: [0, 0, 800, 100], '
|
|
87
|
+
'size: 48, align: center}\n confidence: high\n---\n'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
result = rule_v2_16(Pack(pack_dir))
|
|
91
|
+
|
|
92
|
+
self.assertEqual(result.level, 'FAIL')
|
|
93
|
+
self.assertEqual(len(result.fails), 1)
|
|
94
|
+
self.assertIn('旧样式键 align/size', result.fails[0])
|
|
95
|
+
|
|
96
|
+
def test_normautofit_fontscale_shrinks_emitted_font_size(self):
|
|
97
|
+
# 章节大号数字:160px 字号靠 normAutofit fontScale 0.9 装进 144px 的框。
|
|
98
|
+
# 不乘 fontScale,消费端拿到 160px,字比框高,渐变裁切把底部切成透明。
|
|
99
|
+
shape = {
|
|
100
|
+
'text': {
|
|
101
|
+
'bodyPr': {'anchor': 't', 'font_scale': 0.9, 'ln_spc_reduction': 0.1},
|
|
102
|
+
'lstStyle': {'lvl1pPr': {'sz_px': 160, 'lnSpc': {'mult': 1.0}}},
|
|
103
|
+
'paragraphs': [{'runs': [{'text': '01.'}]}],
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
style = slot_style(shape)
|
|
107
|
+
self.assertIn('font-size: 144px', style['css'])
|
|
108
|
+
self.assertNotIn('font-size: 160px', style['css'])
|
|
109
|
+
# lnSpcReduction 0.1 把 1.0*1.2 的行高压到 1.08
|
|
110
|
+
self.assertIn('line-height: 1.08', style['css'])
|
|
111
|
+
|
|
112
|
+
def test_missing_autofit_leaves_font_size_untouched(self):
|
|
113
|
+
# 无 normAutofit(或无 fontScale)时零影响:字号原样、行高不缩。
|
|
114
|
+
shape = {
|
|
115
|
+
'text': {
|
|
116
|
+
'bodyPr': {'anchor': 't'},
|
|
117
|
+
'lstStyle': {'lvl1pPr': {'sz_px': 160, 'lnSpc': {'mult': 1.0}}},
|
|
118
|
+
'paragraphs': [{'runs': [{'text': '01.'}]}],
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
style = slot_style(shape)
|
|
122
|
+
self.assertIn('font-size: 160px', style['css'])
|
|
123
|
+
self.assertIn('line-height: 1.2', style['css'])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
unittest.main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for preserving per-container radii."""
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from draft import emit_body, emit_frontmatter
|
|
8
|
+
from query import _recipe_css
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def render_frontmatter(radii):
|
|
12
|
+
data = {
|
|
13
|
+
'radii_census': radii,
|
|
14
|
+
'spacing_candidates': {},
|
|
15
|
+
}
|
|
16
|
+
tokens = [('surface', {'hex': '#FFFFFF'})]
|
|
17
|
+
|
|
18
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
19
|
+
emit_frontmatter(
|
|
20
|
+
data,
|
|
21
|
+
tokens,
|
|
22
|
+
[],
|
|
23
|
+
{},
|
|
24
|
+
[],
|
|
25
|
+
[],
|
|
26
|
+
output_dir,
|
|
27
|
+
)
|
|
28
|
+
with open(os.path.join(output_dir, 'frontmatter.yaml'), encoding='utf-8') as stream:
|
|
29
|
+
return stream.read()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def render_body():
|
|
33
|
+
data = {
|
|
34
|
+
'canvas': {'px': [1920, 1080]},
|
|
35
|
+
'form_hint': {'form': 2},
|
|
36
|
+
'counts': {'slides': 1},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
40
|
+
emit_body(
|
|
41
|
+
data,
|
|
42
|
+
[],
|
|
43
|
+
[],
|
|
44
|
+
{},
|
|
45
|
+
[],
|
|
46
|
+
[],
|
|
47
|
+
[],
|
|
48
|
+
{},
|
|
49
|
+
output_dir,
|
|
50
|
+
)
|
|
51
|
+
with open(os.path.join(output_dir, 'body.md'), encoding='utf-8') as stream:
|
|
52
|
+
return stream.read()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RoundedContractTest(unittest.TestCase):
|
|
56
|
+
def test_multiple_radius_tiers_are_not_collapsed_into_one_card_token(self):
|
|
57
|
+
frontmatter = render_frontmatter(
|
|
58
|
+
[
|
|
59
|
+
{'px': 6.9, 'n': 2},
|
|
60
|
+
{'px': 11.9, 'n': 2},
|
|
61
|
+
{'px': 14.4, 'n': 3},
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.assertNotIn('rounded:', frontmatter)
|
|
66
|
+
|
|
67
|
+
def test_single_radius_tier_can_remain_a_global_token(self):
|
|
68
|
+
frontmatter = render_frontmatter([{'px': 12, 'n': 9}])
|
|
69
|
+
|
|
70
|
+
self.assertIn('rounded:\n card: 12px', frontmatter)
|
|
71
|
+
|
|
72
|
+
def test_rare_rounded_exceptions_do_not_override_a_zero_radius_majority(self):
|
|
73
|
+
frontmatter = render_frontmatter(
|
|
74
|
+
[
|
|
75
|
+
{'px': 0, 'n': 241},
|
|
76
|
+
{'px': 3.4, 'n': 4},
|
|
77
|
+
{'px': 6.9, 'n': 3},
|
|
78
|
+
{'px': 50.5, 'n': 1},
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.assertNotIn('rounded:', frontmatter)
|
|
83
|
+
|
|
84
|
+
def test_generated_usage_defaults_unspecified_container_radius_to_zero(self):
|
|
85
|
+
body = render_body()
|
|
86
|
+
|
|
87
|
+
self.assertIn('没有 `border-radius` 就按 `0`', body)
|
|
88
|
+
self.assertIn('不得自行补圆角', body)
|
|
89
|
+
|
|
90
|
+
def test_recipe_does_not_promote_one_rounded_exception_to_the_whole_group(self):
|
|
91
|
+
css = _recipe_css(
|
|
92
|
+
{'type': 'solid', 'color': {'hex': '#FFFFFF'}},
|
|
93
|
+
None,
|
|
94
|
+
[0] * 70 + [50.5],
|
|
95
|
+
None,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.assertFalse(any('border-radius' in declaration for declaration in css))
|
|
99
|
+
|
|
100
|
+
def test_recipe_keeps_a_radius_shared_by_the_whole_group(self):
|
|
101
|
+
css = _recipe_css(
|
|
102
|
+
{'type': 'solid', 'color': {'hex': '#FFFFFF'}},
|
|
103
|
+
None,
|
|
104
|
+
[6.9] * 17,
|
|
105
|
+
None,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.assertIn('border-radius: 6.9px', css)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
unittest.main()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for inherited layout text and model-decided text 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 draft import draft_layouts, emit_layouts, inherited_text_shapes
|
|
12
|
+
from package import build_layouts_md, split_top_blocks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def text_shape(part, layer, shape_id, box, text, placeholder):
|
|
16
|
+
return {
|
|
17
|
+
'part': part,
|
|
18
|
+
'layer': layer,
|
|
19
|
+
'id': shape_id,
|
|
20
|
+
'kind': 'sp',
|
|
21
|
+
'name': 'Text Placeholder',
|
|
22
|
+
'ph': placeholder,
|
|
23
|
+
'box': box,
|
|
24
|
+
'text': {
|
|
25
|
+
'bodyPr': {},
|
|
26
|
+
'lstStyle': {
|
|
27
|
+
'lvl1pPr': {
|
|
28
|
+
'sz_px': 48,
|
|
29
|
+
'weight': 600,
|
|
30
|
+
'color': {'resolved': '#C41230'},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
'paragraphs': [
|
|
34
|
+
{
|
|
35
|
+
'runs': [{'text': text}],
|
|
36
|
+
},
|
|
37
|
+
] if text else [],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TextRoleContractTest(unittest.TestCase):
|
|
43
|
+
def test_empty_non_placeholder_layout_shape_is_not_a_text_slot(self):
|
|
44
|
+
layout_part = 'ppt/slideLayouts/slideLayout2.xml'
|
|
45
|
+
decorative_shape = text_shape(
|
|
46
|
+
layout_part,
|
|
47
|
+
'layout',
|
|
48
|
+
'9',
|
|
49
|
+
{'x': 0, 'y': 0, 'w': 1920, 'h': 24},
|
|
50
|
+
'',
|
|
51
|
+
None,
|
|
52
|
+
)
|
|
53
|
+
decorative_shape['name'] = 'Decorative bar'
|
|
54
|
+
|
|
55
|
+
self.assertEqual(inherited_text_shapes([decorative_shape], []), [])
|
|
56
|
+
|
|
57
|
+
def test_empty_slide_inherits_text_slot_and_css_from_its_layout(self):
|
|
58
|
+
layout_part = 'ppt/slideLayouts/slideLayout2.xml'
|
|
59
|
+
slide_part = 'ppt/slides/slide1.xml'
|
|
60
|
+
shapes = [
|
|
61
|
+
text_shape(
|
|
62
|
+
layout_part,
|
|
63
|
+
'layout',
|
|
64
|
+
'10',
|
|
65
|
+
{'x': 120, 'y': 80, 'w': 840, 'h': 120},
|
|
66
|
+
'Example heading',
|
|
67
|
+
{'type': 'body', 'idx': '10'},
|
|
68
|
+
),
|
|
69
|
+
text_shape(
|
|
70
|
+
slide_part,
|
|
71
|
+
'slide',
|
|
72
|
+
'2',
|
|
73
|
+
None,
|
|
74
|
+
'',
|
|
75
|
+
{'type': 'body', 'idx': '10'},
|
|
76
|
+
),
|
|
77
|
+
]
|
|
78
|
+
data = {
|
|
79
|
+
'canvas': {'px': [1920, 1080]},
|
|
80
|
+
'form_hint': {'form': 0},
|
|
81
|
+
'slides': [
|
|
82
|
+
{
|
|
83
|
+
'part': slide_part,
|
|
84
|
+
'layout': layout_part,
|
|
85
|
+
'background': None,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
'background_composites': {},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
92
|
+
os.makedirs(os.path.join(output_dir, 'ref'))
|
|
93
|
+
with open(
|
|
94
|
+
os.path.join(output_dir, 'ref', 'shapes.json'),
|
|
95
|
+
'w',
|
|
96
|
+
encoding='utf-8',
|
|
97
|
+
) as stream:
|
|
98
|
+
json.dump({'shapes': shapes}, stream)
|
|
99
|
+
archetypes, _, _ = draft_layouts(data, output_dir)
|
|
100
|
+
|
|
101
|
+
self.assertEqual(len(archetypes), 1)
|
|
102
|
+
self.assertEqual(len(archetypes[0]['slots']), 1)
|
|
103
|
+
slot = archetypes[0]['slots'][0]
|
|
104
|
+
self.assertEqual(slot['box'], [120, 80, 840, 120])
|
|
105
|
+
self.assertEqual(slot['txt'], 'Example heading')
|
|
106
|
+
self.assertEqual(slot['role'], 'body')
|
|
107
|
+
self.assertEqual(slot['type'], 'body')
|
|
108
|
+
self.assertTrue(slot['_needs_role'])
|
|
109
|
+
self.assertIn('font-size: 48px', slot['css'])
|
|
110
|
+
self.assertIn('font-weight: 600', slot['css'])
|
|
111
|
+
self.assertIn('color: #C41230', slot['css'])
|
|
112
|
+
|
|
113
|
+
def test_text_role_judgement_changes_semantics_without_dropping_slot(self):
|
|
114
|
+
archetype = {
|
|
115
|
+
'name': 'layout-1',
|
|
116
|
+
'zh': None,
|
|
117
|
+
'role': 'content',
|
|
118
|
+
'bg': None,
|
|
119
|
+
'slots': [
|
|
120
|
+
{
|
|
121
|
+
'role': 'body',
|
|
122
|
+
'type': 'body',
|
|
123
|
+
'box': [120, 80, 840, 120],
|
|
124
|
+
'sz': 48,
|
|
125
|
+
'txt': 'Example heading',
|
|
126
|
+
'css': 'font-size: 48px; color: #C41230',
|
|
127
|
+
'_needs_role': True,
|
|
128
|
+
'_source_layer': 'layout',
|
|
129
|
+
'_placeholder': 'body/10',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
'decor': [],
|
|
133
|
+
'pages': [1],
|
|
134
|
+
'rep': 1,
|
|
135
|
+
'pic_n': 0,
|
|
136
|
+
'confidence': 'low',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
with tempfile.TemporaryDirectory() as output_dir:
|
|
140
|
+
emit_layouts([archetype], output_dir)
|
|
141
|
+
path = os.path.join(output_dir, 'layouts.yaml')
|
|
142
|
+
with open(path, encoding='utf-8') as stream:
|
|
143
|
+
draft = stream.read()
|
|
144
|
+
|
|
145
|
+
self.assertIn('text_roles:', draft)
|
|
146
|
+
self.assertIn(
|
|
147
|
+
'layout-1-text-1: TODO文本角色',
|
|
148
|
+
draft,
|
|
149
|
+
)
|
|
150
|
+
self.assertEqual(draft.count('box: [120, 80, 840, 120]'), 1)
|
|
151
|
+
|
|
152
|
+
decided = draft.replace(
|
|
153
|
+
'layout-1-text-1: TODO文本角色',
|
|
154
|
+
'layout-1-text-1: title',
|
|
155
|
+
)
|
|
156
|
+
layouts_md = build_layouts_md(split_top_blocks(decided), (1920, 1080))
|
|
157
|
+
|
|
158
|
+
self.assertNotIn('text_roles:', layouts_md)
|
|
159
|
+
self.assertEqual(layouts_md.count('box: [120, 80, 840, 120]'), 1)
|
|
160
|
+
self.assertIn(
|
|
161
|
+
'role: title, box: [120, 80, 840, 120], type: title',
|
|
162
|
+
layouts_md,
|
|
163
|
+
)
|
|
164
|
+
self.assertIn('css: "font-size: 48px; color: #C41230"', layouts_md)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == '__main__':
|
|
168
|
+
unittest.main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""字体镜像可加载性验证(L4 固定流程的脚本形态)。
|
|
3
|
+
|
|
4
|
+
python3 verify_font.py "Noto Sans SC" [--wght "400;500;600;700"] [--repeat 2]
|
|
5
|
+
|
|
6
|
+
对 https://miaoda.feishu.cn/fonts/css2 请求 N 次(镜像多字重响应不稳定,默认复测 2 次),
|
|
7
|
+
解析每次返回的 @font-face font-weight 集合,输出:
|
|
8
|
+
|
|
9
|
+
run 1: 400,500,600,700
|
|
10
|
+
run 2: 400
|
|
11
|
+
verdict: usable=400 unstable=500,600,700
|
|
12
|
+
|
|
13
|
+
usable = 每次都返回的字重(可放心用);unstable = 时有时无(按浏览器合成加粗处理并记 gaps);
|
|
14
|
+
全部请求失败 = 该族不可加载,走 font-fallback.yaml 降级。exit 0 = 至少一档 usable。
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import urllib.request
|
|
21
|
+
|
|
22
|
+
MIRROR = 'https://miaoda.feishu.cn/fonts/css2'
|
|
23
|
+
UA = ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
|
|
24
|
+
'(KHTML, like Gecko) Chrome/120.0 Safari/537.36')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fetch_weights(family, wght):
|
|
28
|
+
spec = family.replace(' ', '+') + (':wght@' + wght if wght else '')
|
|
29
|
+
url = '%s?%s&display=swap' % (MIRROR, urllib.parse.quote('family=' + spec, safe='=+&:;@'))
|
|
30
|
+
req = urllib.request.Request(url, headers={'User-Agent': UA})
|
|
31
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
32
|
+
css = r.read().decode('utf-8', 'replace')
|
|
33
|
+
if '@font-face' not in css:
|
|
34
|
+
return None
|
|
35
|
+
return sorted(set(re.findall(r'font-weight:\s*(\d+)', css)), key=int)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main():
|
|
39
|
+
ap = argparse.ArgumentParser()
|
|
40
|
+
ap.add_argument('family')
|
|
41
|
+
ap.add_argument('--wght', default='400;500;600;700')
|
|
42
|
+
ap.add_argument('--repeat', type=int, default=2)
|
|
43
|
+
a = ap.parse_args()
|
|
44
|
+
|
|
45
|
+
runs = []
|
|
46
|
+
for i in range(a.repeat):
|
|
47
|
+
try:
|
|
48
|
+
w = fetch_weights(a.family, a.wght)
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
w = None
|
|
51
|
+
print('run %d: 请求失败(%s)' % (i + 1, exc.__class__.__name__))
|
|
52
|
+
runs.append(set())
|
|
53
|
+
continue
|
|
54
|
+
print('run %d: %s' % (i + 1, ','.join(w) if w else '无 @font-face'))
|
|
55
|
+
runs.append(set(w or []))
|
|
56
|
+
|
|
57
|
+
usable = set.intersection(*runs) if runs else set()
|
|
58
|
+
unstable = set.union(*runs) - usable if runs else set()
|
|
59
|
+
if usable:
|
|
60
|
+
print('verdict: usable=%s%s' % (','.join(sorted(usable, key=int)),
|
|
61
|
+
' unstable=' + ','.join(sorted(unstable, key=int)) if unstable else ''))
|
|
62
|
+
return 0
|
|
63
|
+
print('verdict: 不可加载 —— 查 font-fallback.yaml 降级,原始名留栈首 + 记 gaps')
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
sys.exit(main())
|