Given a CSS file the script generates an HTML preview of all the CSS definitions by recursively drilling down the chain of nested classes, ids and tag names. Rules with `position: absolute|fixed|sticky` are wrapped into `iframe` elements in order to avoid overlap. The output goes to stdout since I wanted to avoid keeping storing too much data in memory. Content that goes inside iframes is printed first to a temporary buffer rather than stdout because that text needs to be html-escaped. Would be interested to get feedback on: - performance and efficiency (CPU, memory): is there anything inherently inefficient/slow with the chosen approach that can be optimised? - code organisation - obvious (or subtle) code smells - naming and legibility - possible criteria to check correctness or well-formedness of the generated output <!-- language: lang-python --> #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Generates an HTML file for previewing all styles defined in a CSS file # # dependencies: cssutils # USAGE: # css_preview_generator.py style.css > preview.html import html import io import sys import cssutils image_placeholder = "data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='150' viewBox='0 0 300 150'%3E%3Crect fill='yellow' width='300' height='150'/%3E%3Ctext fill='rgba(0,0,0,0.5)' x='50%25' y='50%25' text-anchor='middle'%3E300×150%3C/text%3E%3C/svg%3E" def render(s, out): if out and not out.closed: return print(s, end='', file=out) else: print(s, flush=True) def down_the_rabbit_hole(chunks, full_selector, out=None): if len(chunks): chunk = chunks.pop(0) render_open_tag(chunk, out) down_the_rabbit_hole(chunks, full_selector, out) render_close_tag(chunk, out) else: render(full_selector, out) prefix_map = { '.': 'class', '#': 'id' } def extract_class_id(defn, extracted_attrs=''): try: for prefix in prefix_map.keys(): if prefix in defn: items = defn.split(prefix) value = ' '.join(items[1:]) # return a tuple of (tagname, 'class="bla blu"') or (tagname, 'id="abc"') tag = items[0] if any(suffix in tag for suffix in prefix_map.keys()): return extract_class_id(tag, f'{prefix_map[prefix]}="{value}"') else: return items[0], f'{extracted_attrs} {prefix_map[prefix]}="{value}"' except Exception as e: print(e, file=sys.stderr) return defn, '' def render_open_tag(definition, out): if definition.startswith(('.', '#')): _, class_or_id = extract_class_id(definition) render(f'<div {class_or_id}>', out) else: if definition == 'a' or definition.startswith(('a.', 'a#')): tag, class_or_id = extract_class_id(definition) render(f'''<a {class_or_id} href="#">''', out) elif definition == 'img' or definition.startswith(('img.','img#')): render(f'<img src="{image_placeholder}" alt="[image]">', out) else: tag, class_or_id = extract_class_id(definition) if tag.lower() == 'td': render(f'<table><thead><tr><th>🟨🟧[th]🟧🟨</th></thead><tbody><tr><td {class_or_id}>🟩🟦[td]🟦🟩<br/>', out) else: render(f'<{tag} {class_or_id}>', out) def render_close_tag(definition, out): if definition.startswith(('.', '#')): render('</div>', out) else: if definition == 'a' or definition.startswith(('a.', 'a#')): render(f'⚓️ {definition}</a>', out) else: tag, _ = extract_class_id(definition) if tag.lower() == 'td': render('</td></tr></tbody></table>', out) else: render(f'</{tag}>', out) if __name__ == '__main__': if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help'): print(f'Usage: {sys.argv[0]} style.css > preview.html') sys.exit(-1) already_seen = [] css_file = sys.argv[1] sheet = cssutils.parseFile(css_file) print(f'''<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CSS preview: {css_file}</title> <link href="{css_file}" rel="stylesheet" type="text/css" /> </head> <body> ''') selectors_requiring_iframe = [] # build a list of absolute & fixed rules for rule in sheet: if isinstance(rule, cssutils.css.CSSStyleRule): position = getattr(rule.style, 'position', None) if position in ('fixed', 'absolute', 'sticky'): for single_selector in rule.selectorList: # type: cssutils.css.Selector selectors_requiring_iframe.append(single_selector.selectorText) # deduplicate list selectors_requiring_iframe = list(dict.fromkeys(selectors_requiring_iframe)) for rule in sheet: if isinstance(rule, cssutils.css.CSSStyleRule): selectors: cssutils.css.SelectorList = getattr(rule, 'selectorList', []) full_selectors_text = rule.selectorText print(f'CSS Rule: {full_selectors_text}', file=sys.stderr) for single_selector in selectors: # type: cssutils.css.Selector current_selector_text = single_selector.selectorText if not single_selector or current_selector_text.startswith(('html', 'body')): continue # 1. convert '>' to space # 2. '~' '*' '+' and '[]' (not supported, ignoring them, convert to space, breaks semantics FIXME) for c in '>*~+': if c in current_selector_text: current_selector_text = current_selector_text.replace(c, ' ') for c in ':[': if c in current_selector_text: current_selector_text = current_selector_text.split(c)[0] if current_selector_text in already_seen: continue else: already_seen.append(current_selector_text) if ' ' in current_selector_text: current_selector_text = current_selector_text.replace(' ', ' ') position = getattr(rule.style, 'position', None) # if current selector is a child of an absolute/fixed rule then also wrap it in an iframe matching_abs_parents = [sel for sel in selectors_requiring_iframe if sel in current_selector_text] need_iframe = position in ('fixed', 'absolute', 'sticky') or len(matching_abs_parents) need_table = False out = None if need_iframe: print( f'''<iframe style="border:1px dotted #acad9e;" width="400" height="300" srcdoc="{html.escape(f'<html><head><link href="{css_file}" rel="stylesheet" type="text/css"/></head><body style="background:#f6f4ee">')}''', end='') out = io.StringIO() print(f'\t{current_selector_text}', file=sys.stderr) down_the_rabbit_hole(current_selector_text.split(), full_selectors_text, out) if need_iframe: print(html.escape(out.getvalue()), end='') out.close() print('"></iframe>') print(''' </body> </html>''') The latest version is [here][1]. [1]: https://github.com/glowinthedark/css_stylesheet_preview_generator/blob/master/css_preview_generator.py