@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.
@@ -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())