|
77 | 77 | re.compile(r'^shadow-.+'): [], |
78 | 78 | # Shapes |
79 | 79 | re.compile(r'^o_web_editor_[A-Z].+'): [], |
80 | | - # Snippets |
81 | | - # TODO our convention (badly followed) for classes which are specific to a |
82 | | - # snippet's inner components or options is to start that class with the |
83 | | - # class specific to the snippet itself. For instance, for a s_some_stuff |
84 | | - # snippet, use s_some_stuff_button or s_some_stuff_small. The test here |
85 | | - # flags as wrong such an usecase... unless you explicitly whitelist it. It |
86 | | - # should be smarter than that and make following our convention always ok |
87 | | - # without the need to change this test's whitelist. |
88 | | - re.compile(r'^s_.*'): [ |
89 | | - 's_alert_md', |
90 | | - 's_blockquote_with_icon', 's_blockquote', |
91 | | - 's_carousel_default', 's_carousel_rounded', 's_carousel_boxed', |
92 | | - 's_carousel_indicators_dots', 's_carousel_indicators_hidden', 's_carousel_controllers_indicators_outside', |
93 | | - 's_carousel_cards_with_img', 's_carousel_cards_card', |
94 | | - 's_quotes_carousel', |
95 | | - 's_dynamic', 's_dynamic_empty', |
96 | | - 's_dynamic_snippet_blog_posts', 's_blog_posts_effect_marley', 's_blog_post_big_picture', 's_blog_posts_post_picture_size_default', |
97 | | - 's_event_upcoming_snippet', 's_event_event_picture', |
98 | | - 's_col_no_bgcolor', 's_col_no_resize', |
99 | | - 's_image_gallery', 's_image_gallery_indicators_arrows_boxed', 's_image_gallery_indicators_arrows_rounded', |
100 | | - 's_image_gallery_indicators_dots', 's_image_gallery_indicators_squared', 's_image_gallery_indicators_rounded', 's_image_gallery_indicators_hidden', 's_image_gallery_indicators_bars', 's_image_gallery_indicators_outside','s_image_gallery_controllers_outside_arrows_right', 's_image_gallery_controllers_outside', |
101 | | - 's_newsletter_list', 's_newsletter_subscribe_form', |
102 | | - 's_parallax_is_fixed', 's_parallax_no_overflow_hidden', |
103 | | - 's_process_steps_connector_line', |
104 | | - 's_product_catalog_dish_name', 's_product_catalog_dish_dot_leaders', |
105 | | - 's_progress_bar_label_hidden', 's_progress_bar_label_inline', |
106 | | - 's_rating_no_title', |
107 | | - 's_table_of_content_vertical_navbar', 's_table_of_content_navbar_sticky', 's_table_of_content_navbar_wrap', |
108 | | - 's_timeline_card', |
109 | | - 's_website_form_custom', 's_website_form_dnone', 's_website_form_field', 's_website_form_input', 's_website_form_mark', 's_website_form_submit', 's_website_form_no_submit_label', |
110 | | - 's_donation_btn', 's_donation_custom_btn', 's_newsletter_subscribe_form_input_small', |
111 | | - 's_tabs_common', 's_tabs_nav_vertical', 's_tabs_nav_with_descriptions', |
112 | | - ], |
113 | 80 | # Text |
114 | 81 | re.compile(r'^text-(?!(center|end|start|bg-|lg-)).*$'): [ |
115 | 82 | 'text-break', 'text-decoration-none', 'text-reset', |
|
119 | 86 | # Width |
120 | 87 | re.compile(r'^w-\d*$'): [], |
121 | 88 | } |
| 89 | +# Special case for "s_" classes that respect our convention: classes that share |
| 90 | +# the same base and follow the naming pattern (s_some, s_some_button) are not |
| 91 | +# flagged as conflicting. Explicitly whitelist exceptions that don't follow the |
| 92 | +# pattern only. |
| 93 | +# TODO all these classes were processed but we might want to re-check them all |
| 94 | +# to minimize the list if possible. |
| 95 | +S_CLASSES_WHITELIST = [ |
| 96 | + # Classes that rightfully belong here at the moment |
| 97 | + 's_col_no_bgcolor', 's_col_no_resize', 's_allow_columns', |
| 98 | + 's_nb_column_fixed', 's_dialog_preview', |
| 99 | + 's_parallax_is_fixed', 's_parallax_bg', 's_parallax_no_overflow_hidden', |
| 100 | + 's_carousel_cards_card', 's_timeline_card', 's_blog_posts', 's_events', |
| 101 | + 's_appointments', |
| 102 | + |
| 103 | + # Classes that should not be here... but are here by compatibility (not |
| 104 | + # following our "s_" conventions correctly). |
| 105 | + 's_process_step', 's_process_step_svg_defs', 's_number', 's_tabs_common', |
| 106 | + 's_process_steps_connector_line', 's_tabs_nav', 's_tabs_main', |
| 107 | + 's_tabs_nav_vertical', 's_tabs_nav_with_descriptions', 's_tabs_content', |
| 108 | + 's_carousel', 's_carousel_default', 's_carousel_boxed', 's_carousel_intro', |
| 109 | + 's_carousel_rounded', 's_carousel_cards', 's_carousel_indicators_numbers', |
| 110 | + 's_carousel_indicators_dots', 's_quotes_carousel', 's_rating_no_title', |
| 111 | + 's_carousel_indicators_hidden', 's_blog_post_big_picture', |
| 112 | + 's_newsletter_list', 's_event_upcoming_snippet', 's_event_event_picture', |
| 113 | + 's_newsletter_subscribe_form', |
| 114 | + |
| 115 | + # FIXME those classes have no reason to be here... missing data-snippet? |
| 116 | + 's_hr', 's_accordion', 's_accordion_highlight', 's_media_list_item', |
| 117 | + 's_media_list_img_wrapper', 's_media_list_body', 's_media_list_img', |
| 118 | + 's_website_form_datetime', |
| 119 | +] |
122 | 120 |
|
123 | 121 |
|
124 | 122 | @tagged('post_install', '-at_install') |
@@ -186,7 +184,7 @@ def check(theme_name, website): |
186 | 184 | html_text = self.env['ir.qweb']._render(view.id) |
187 | 185 | if not html_text: |
188 | 186 | continue |
189 | | - html_tree = html.fromstring(html_text) |
| 187 | + html_tree = html.fromstring(f'<wrap>{html_text}</wrap>') |
190 | 188 | blocks_el = html_tree.xpath("//*[@id='o_scroll']") |
191 | 189 | if blocks_el: |
192 | 190 | # Only look at blocks in website.snippets |
@@ -216,6 +214,46 @@ def check(theme_name, website): |
216 | 214 | % (theme_name, view.key, conflict, classes, conflicting_classes_re.pattern) |
217 | 215 | ) |
218 | 216 |
|
| 217 | + # Special handling for snippet classes following |
| 218 | + # naming convention: if classes match the |
| 219 | + # 's_snippet_name_*' pattern, they are allowed. |
| 220 | + non_whitelisted_s_classes = { |
| 221 | + cl for cl in classes |
| 222 | + if cl.startswith('s_') and cl not in S_CLASSES_WHITELIST |
| 223 | + } |
| 224 | + if non_whitelisted_s_classes: |
| 225 | + all_parent_s_classes = set() |
| 226 | + parent_el = el |
| 227 | + is_in_snippet = view.key.startswith('website.configurator_') |
| 228 | + # Find all parent elements classes that start |
| 229 | + # with 's_' (including on the current element). |
| 230 | + while parent_el is not None: |
| 231 | + parent_classes = set(parent_el.attrib.get('class', '').split()) |
| 232 | + all_parent_s_classes.update({cl for cl in parent_classes if cl.startswith('s_')}) |
| 233 | + if parent_el.attrib.get('data-snippet'): |
| 234 | + is_in_snippet = True |
| 235 | + break |
| 236 | + parent_el = parent_el.getparent() |
| 237 | + if is_in_snippet: |
| 238 | + # Accept classes that are prefixed by such a |
| 239 | + # parent class (+ '_'). |
| 240 | + non_whitelisted_s_classes = { |
| 241 | + cl for cl in non_whitelisted_s_classes |
| 242 | + if not any(cl.startswith(f'{parent_cls}_') for parent_cls in all_parent_s_classes) |
| 243 | + } |
| 244 | + is_snippet_root = el.attrib.get('data-snippet') \ |
| 245 | + or el.getparent().tag == 'wrap' and view.key.startswith('website.configurator_') |
| 246 | + if len(non_whitelisted_s_classes) > (1 if is_snippet_root else 0): |
| 247 | + errors.append( |
| 248 | + "Using %r, view %r contains 's_' classes that do not respect our conventions: %r in %r" |
| 249 | + % (theme_name, view.key, non_whitelisted_s_classes, classes) |
| 250 | + ) |
| 251 | + else: |
| 252 | + errors.append( |
| 253 | + "Using %r, view %r contains 's_' classes (%r) that are not in a snippet" |
| 254 | + % (theme_name, view.key, non_whitelisted_s_classes) |
| 255 | + ) |
| 256 | + |
219 | 257 | for el in html_tree.xpath('//*[@style]'): |
220 | 258 | styles = el.attrib['style'].split(';') |
221 | 259 | non_empty_styles = filter(lambda style: style, styles) |
|
0 commit comments