@lark-apaas/coding-steering 0.1.32-dev.5abff3b → 0.1.32-dev.942e73f
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 +12 -5
- package/steering/design-html/skills/pptx-style-extract/scripts/census.py +8 -2
- package/steering/design-html/skills/pptx-style-extract/scripts/draft.py +67 -13
- package/steering/design-html/skills/pptx-style-extract/scripts/extract.py +229 -10
- 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 +113 -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 +5 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_layout_css.py +102 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/test_logo_scope.py +148 -0
- package/steering/design-html/skills/pptx-style-extract/scripts/verify_logo_scope.py +188 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for template logo placement in generated deck HTML."""
|
|
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 verify_logo_scope import validate_logo_scope
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogoScopeTest(unittest.TestCase):
|
|
14
|
+
def write_pack(self, root):
|
|
15
|
+
with open(os.path.join(root, 'design.md'), 'w', encoding='utf-8') as stream:
|
|
16
|
+
stream.write(
|
|
17
|
+
'---\n'
|
|
18
|
+
'assets:\n'
|
|
19
|
+
' logo-1:\n'
|
|
20
|
+
' path: assets/logos/1.png\n'
|
|
21
|
+
' kind: logo\n'
|
|
22
|
+
'layouts: layouts.md\n'
|
|
23
|
+
'---\n'
|
|
24
|
+
)
|
|
25
|
+
with open(os.path.join(root, 'layouts.md'), 'w', encoding='utf-8') as stream:
|
|
26
|
+
stream.write(
|
|
27
|
+
'---\n'
|
|
28
|
+
'canvas: 1920x1080\n'
|
|
29
|
+
'layouts:\n'
|
|
30
|
+
' cover:\n'
|
|
31
|
+
' role: cover\n'
|
|
32
|
+
' slots:\n'
|
|
33
|
+
' - {role: logo, box: [64, 64, 224, 48], type: pic, asset: logo-1}\n'
|
|
34
|
+
' content:\n'
|
|
35
|
+
' role: content\n'
|
|
36
|
+
' slots:\n'
|
|
37
|
+
' - {role: title, box: [100, 100, 800, 100], type: title}\n'
|
|
38
|
+
' closing:\n'
|
|
39
|
+
' role: closing\n'
|
|
40
|
+
' slots:\n'
|
|
41
|
+
' - {role: logo, box: [64, 64, 224, 48], type: pic, asset: logo-1}\n'
|
|
42
|
+
'---\n'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def write_html(self, root, content):
|
|
46
|
+
path = os.path.join(root, 'index.html')
|
|
47
|
+
with open(path, 'w', encoding='utf-8') as stream:
|
|
48
|
+
stream.write(content)
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
def test_logo_is_limited_to_every_archetype_with_its_slot(self):
|
|
52
|
+
with tempfile.TemporaryDirectory() as root:
|
|
53
|
+
self.write_pack(root)
|
|
54
|
+
html = self.write_html(
|
|
55
|
+
root,
|
|
56
|
+
'<deck-stage>'
|
|
57
|
+
'<section data-pptx-layout="cover">'
|
|
58
|
+
'<img src="assets/pptx-volcengine/logos/1.png"></section>'
|
|
59
|
+
'<section data-pptx-layout="content"><h1>正文</h1></section>'
|
|
60
|
+
'<section data-pptx-layout="closing">'
|
|
61
|
+
'<img src="assets/pptx-volcengine/logos/1.png"></section>'
|
|
62
|
+
'</deck-stage>',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.assertEqual(
|
|
66
|
+
[],
|
|
67
|
+
validate_logo_scope(root, html, 'assets/pptx-volcengine'),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def test_logo_on_an_unowned_archetype_is_rejected(self):
|
|
71
|
+
with tempfile.TemporaryDirectory() as root:
|
|
72
|
+
self.write_pack(root)
|
|
73
|
+
html = self.write_html(
|
|
74
|
+
root,
|
|
75
|
+
'<deck-stage>'
|
|
76
|
+
'<section data-pptx-layout="content">'
|
|
77
|
+
'<img src="assets/pptx-volcengine/logos/1.png"></section>'
|
|
78
|
+
'</deck-stage>',
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
problems = validate_logo_scope(root, html, 'assets/pptx-volcengine')
|
|
82
|
+
|
|
83
|
+
self.assertEqual(1, len(problems))
|
|
84
|
+
self.assertIn('content', problems[0])
|
|
85
|
+
self.assertIn('logo-1', problems[0])
|
|
86
|
+
|
|
87
|
+
def test_nested_content_section_does_not_become_a_slide(self):
|
|
88
|
+
with tempfile.TemporaryDirectory() as root:
|
|
89
|
+
self.write_pack(root)
|
|
90
|
+
html = self.write_html(
|
|
91
|
+
root,
|
|
92
|
+
'<deck-stage><section data-pptx-layout="cover">'
|
|
93
|
+
'<section class="content"><img src="assets/pptx-volcengine/logos/1.png">'
|
|
94
|
+
'</section></section></deck-stage>',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.assertEqual(
|
|
98
|
+
[],
|
|
99
|
+
validate_logo_scope(root, html, 'assets/pptx-volcengine'),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def test_each_slide_declares_its_template_archetype(self):
|
|
103
|
+
with tempfile.TemporaryDirectory() as root:
|
|
104
|
+
self.write_pack(root)
|
|
105
|
+
html = self.write_html(
|
|
106
|
+
root,
|
|
107
|
+
'<deck-stage><section><h1>未声明页型</h1></section></deck-stage>',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.assertEqual(
|
|
111
|
+
['第 1 页缺少 data-pptx-layout,无法核验模板 logo 归属'],
|
|
112
|
+
validate_logo_scope(root, html, 'assets/pptx-volcengine'),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def test_logo_in_global_css_is_rejected(self):
|
|
116
|
+
with tempfile.TemporaryDirectory() as root:
|
|
117
|
+
self.write_pack(root)
|
|
118
|
+
html = self.write_html(
|
|
119
|
+
root,
|
|
120
|
+
'<style>.slide-logo { background-image: url('
|
|
121
|
+
'"assets/pptx-volcengine/logos/1.png") }</style>'
|
|
122
|
+
'<deck-stage><section data-pptx-layout="content">正文</section></deck-stage>',
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.assertEqual(
|
|
126
|
+
['模板 logo logo-1 出现在 slide section 外,无法核验页型归属'],
|
|
127
|
+
validate_logo_scope(root, html, 'assets/pptx-volcengine'),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def test_logo_in_a_slide_style_block_is_rejected(self):
|
|
131
|
+
with tempfile.TemporaryDirectory() as root:
|
|
132
|
+
self.write_pack(root)
|
|
133
|
+
html = self.write_html(
|
|
134
|
+
root,
|
|
135
|
+
'<deck-stage><section data-pptx-layout="content"><style>'
|
|
136
|
+
'.slide-logo { background-image: url('
|
|
137
|
+
'"assets/pptx-volcengine/logos/1.png") }</style>'
|
|
138
|
+
'正文</section></deck-stage>',
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self.assertEqual(
|
|
142
|
+
['模板 logo logo-1 出现在 slide section 外,无法核验页型归属'],
|
|
143
|
+
validate_logo_scope(root, html, 'assets/pptx-volcengine'),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == '__main__':
|
|
148
|
+
unittest.main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Verify that generated slides use template logos only on owning archetypes."""
|
|
3
|
+
import argparse
|
|
4
|
+
import posixpath
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from html.parser import HTMLParser
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
from check_v2 import Pack
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
URL_RE = re.compile(r'url\(\s*[\'"]?([^\'")\s]+)', re.I)
|
|
14
|
+
VOID_TAGS = {
|
|
15
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
16
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalized_path(value):
|
|
21
|
+
path = urlsplit(value).path.replace('\\', '/')
|
|
22
|
+
return posixpath.normpath(path).lstrip('./')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def asset_urls(pack, asset_prefix):
|
|
26
|
+
prefix = normalized_path(asset_prefix).rstrip('/')
|
|
27
|
+
urls = {}
|
|
28
|
+
for asset_id, (entry, _) in pack.assets.items():
|
|
29
|
+
if not isinstance(entry, dict) or entry.get('kind') != 'logo':
|
|
30
|
+
continue
|
|
31
|
+
path = entry.get('path')
|
|
32
|
+
if not isinstance(path, str) or not path:
|
|
33
|
+
continue
|
|
34
|
+
relative = normalized_path(path)
|
|
35
|
+
if relative.startswith('assets/'):
|
|
36
|
+
relative = relative[len('assets/'):]
|
|
37
|
+
urls[posixpath.join(prefix, relative)] = asset_id
|
|
38
|
+
return urls
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def contains_asset(value, asset_id):
|
|
42
|
+
if isinstance(value, dict):
|
|
43
|
+
return (value.get('asset') == asset_id
|
|
44
|
+
or any(contains_asset(child, asset_id) for child in value.values()))
|
|
45
|
+
if isinstance(value, list):
|
|
46
|
+
return any(contains_asset(child, asset_id) for child in value)
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def logo_owners(pack):
|
|
51
|
+
owners = {}
|
|
52
|
+
for asset_url, asset_id in asset_urls(pack, '').items():
|
|
53
|
+
del asset_url
|
|
54
|
+
owners[asset_id] = {
|
|
55
|
+
name
|
|
56
|
+
for name, (layout, _) in pack.layouts.items()
|
|
57
|
+
if contains_asset(layout, asset_id)
|
|
58
|
+
}
|
|
59
|
+
return owners
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def urls_from_attrs(attrs):
|
|
63
|
+
urls = []
|
|
64
|
+
for key, value in attrs:
|
|
65
|
+
if not value:
|
|
66
|
+
continue
|
|
67
|
+
if key.lower() in ('src', 'href'):
|
|
68
|
+
urls.append(value)
|
|
69
|
+
elif key.lower() == 'srcset':
|
|
70
|
+
urls.extend(item.strip().split(' ', 1)[0] for item in value.split(','))
|
|
71
|
+
elif key.lower() == 'style':
|
|
72
|
+
urls.extend(URL_RE.findall(value))
|
|
73
|
+
return urls
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SlideAssetParser(HTMLParser):
|
|
77
|
+
def __init__(self):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.slides = []
|
|
80
|
+
self._tags = []
|
|
81
|
+
self._sections = []
|
|
82
|
+
self._active_slides = []
|
|
83
|
+
self._style_depth = 0
|
|
84
|
+
self.outside_urls = []
|
|
85
|
+
|
|
86
|
+
def handle_starttag(self, tag, attrs):
|
|
87
|
+
tag = tag.lower()
|
|
88
|
+
urls = urls_from_attrs(attrs)
|
|
89
|
+
parent = self._tags[-1] if self._tags else None
|
|
90
|
+
if tag == 'section':
|
|
91
|
+
slide = (
|
|
92
|
+
{'layout': dict(attrs).get('data-pptx-layout'), 'urls': urls}
|
|
93
|
+
if parent == 'deck-stage' else None
|
|
94
|
+
)
|
|
95
|
+
self._sections.append(slide)
|
|
96
|
+
if slide is not None:
|
|
97
|
+
self._active_slides.append(slide)
|
|
98
|
+
elif self._active_slides:
|
|
99
|
+
self._active_slides[-1]['urls'].extend(urls)
|
|
100
|
+
else:
|
|
101
|
+
self.outside_urls.extend(urls)
|
|
102
|
+
if tag == 'style':
|
|
103
|
+
self._style_depth += 1
|
|
104
|
+
if tag not in VOID_TAGS:
|
|
105
|
+
self._tags.append(tag)
|
|
106
|
+
|
|
107
|
+
def handle_startendtag(self, tag, attrs):
|
|
108
|
+
if self._active_slides:
|
|
109
|
+
self._active_slides[-1]['urls'].extend(urls_from_attrs(attrs))
|
|
110
|
+
else:
|
|
111
|
+
self.outside_urls.extend(urls_from_attrs(attrs))
|
|
112
|
+
|
|
113
|
+
def handle_endtag(self, tag):
|
|
114
|
+
tag = tag.lower()
|
|
115
|
+
if tag == 'style' and self._style_depth:
|
|
116
|
+
self._style_depth -= 1
|
|
117
|
+
if tag == 'section' and self._sections:
|
|
118
|
+
slide = self._sections.pop()
|
|
119
|
+
if slide is not None:
|
|
120
|
+
self.slides.append(slide)
|
|
121
|
+
self._active_slides.pop()
|
|
122
|
+
if tag not in VOID_TAGS and self._tags:
|
|
123
|
+
self._tags.pop()
|
|
124
|
+
|
|
125
|
+
def handle_data(self, data):
|
|
126
|
+
if not self._style_depth:
|
|
127
|
+
return
|
|
128
|
+
self.outside_urls.extend(URL_RE.findall(data))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def validate_logo_scope(pack_dir, html_path, asset_prefix):
|
|
132
|
+
"""Return every template-logo placement that does not match the slide archetype."""
|
|
133
|
+
pack = Pack(pack_dir)
|
|
134
|
+
known_urls = asset_urls(pack, asset_prefix)
|
|
135
|
+
owners = logo_owners(pack)
|
|
136
|
+
if not known_urls:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
with open(html_path, encoding='utf-8') as stream:
|
|
140
|
+
text = stream.read()
|
|
141
|
+
parser = SlideAssetParser()
|
|
142
|
+
parser.feed(text)
|
|
143
|
+
parser.close()
|
|
144
|
+
|
|
145
|
+
problems = []
|
|
146
|
+
for url in parser.outside_urls:
|
|
147
|
+
asset_id = known_urls.get(normalized_path(url))
|
|
148
|
+
if asset_id:
|
|
149
|
+
problems.append(
|
|
150
|
+
'模板 logo %s 出现在 slide section 外,无法核验页型归属' % asset_id)
|
|
151
|
+
for number, slide in enumerate(parser.slides, 1):
|
|
152
|
+
layout = slide['layout']
|
|
153
|
+
if not layout:
|
|
154
|
+
problems.append('第 %d 页缺少 data-pptx-layout,无法核验模板 logo 归属' % number)
|
|
155
|
+
continue
|
|
156
|
+
if layout not in pack.layouts:
|
|
157
|
+
problems.append('第 %d 页声明了不存在的模板页型: %s' % (number, layout))
|
|
158
|
+
continue
|
|
159
|
+
for url in slide['urls']:
|
|
160
|
+
asset_id = known_urls.get(normalized_path(url))
|
|
161
|
+
if asset_id and layout not in owners.get(asset_id, set()):
|
|
162
|
+
allowed = '、'.join(sorted(owners.get(asset_id) or ())) or '(无)'
|
|
163
|
+
problems.append(
|
|
164
|
+
'第 %d 页页型 %s 不得使用 %s;只允许: %s'
|
|
165
|
+
% (number, layout, asset_id, allowed))
|
|
166
|
+
return problems
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main(argv=None):
|
|
170
|
+
parser = argparse.ArgumentParser(
|
|
171
|
+
description='Verify that generated deck HTML places PPTX logos only on owning layouts.')
|
|
172
|
+
parser.add_argument('pack_dir')
|
|
173
|
+
parser.add_argument('html_path')
|
|
174
|
+
parser.add_argument('--asset-prefix', required=True)
|
|
175
|
+
args = parser.parse_args(argv)
|
|
176
|
+
|
|
177
|
+
problems = validate_logo_scope(args.pack_dir, args.html_path, args.asset_prefix)
|
|
178
|
+
if not problems:
|
|
179
|
+
print('PPTX_LOGO_SCOPE: PASS')
|
|
180
|
+
return 0
|
|
181
|
+
print('PPTX_LOGO_SCOPE: FAIL count=%d' % len(problems))
|
|
182
|
+
for problem in problems:
|
|
183
|
+
print('[logoScope] %s' % problem)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == '__main__':
|
|
188
|
+
sys.exit(main())
|