import csv import logging import os import pprint import re from collections import Counter, OrderedDict from itertools import zip_longest from django import template from django.conf import settings from django.contrib.staticfiles import finders from django.core.management.base import BaseCommand logger = logging.getLogger(__name__) RE_PATTERNS = { 'view_html': '[\'\"](.*\.html)', 'html_html': '{% (?:extends|include) [\'\"]?(.*\.html)', 'html_style': '{% static [\'\"]?(.*\.css)', 'css_media': ( '^\s*\@media([^{]+)\{\s*([\s\S]*?})\s*}' ), 'css_selector': ( '^\s*([.#\[:_A-Za-z][^{]*)' '{([\s\S]*?)}' ), 'html_class': 'class=[\'\"]([a-zA-Z0-9-_\s]*)', 'html_id': 'id=[\'\"]([a-zA-Z0-9-_]*)' } class Command(BaseCommand): help = 'Finds and fixes unused css styles in the templates' requires_system_checks = False def add_arguments(self, parser): # positional arguments parser.add_argument( 'apps', nargs='+', type=str, help='name of the apps to be optimized' ) # Named (optional) arguments parser.add_argument( '--together', action='store_true', help='optimize the apps together' ) parser.add_argument( '--css', action='store_true', help='optimize only css rules in each file' ) def handle(self, *args, **options): apps_list = options['apps'] for app in apps_list: if options['css']: self.optimize_css(app) # else: # optimize_all(app) def optimize_css(self, app_name): # get html and css files used in the app files = get_files(app_name) # get_selectors_from_css css_selectors = get_selectors_css(files['style']) # get_selectors_from_html html_selectors = get_selectors_html(files['html']) # get duplication of css rules from css files css_dup_report = get_css_duplication(css_selectors) def get_files(app_name): # the view file for the app app_view = os.path.join(settings.PROJECT_DIR, app_name, 'views.py') # get template files called from the view all_html_list = file_match_pattern(app_view, 'view_html') # list of unique template files uniq_html_list = list(OrderedDict.fromkeys(all_html_list).keys()) # list of stylesheets all_style_list = [] file_patterns = ['html_html', 'html_style'] # get html and css files called from within templates i = 0 while i < len(uniq_html_list): template_name = uniq_html_list[i] try: # a dict containing 'html' and 'css' files temp_files = templates_match_pattern( template_name, file_patterns ) except template.exceptions.TemplateDoesNotExist as e: print("template file not found: ", str(e)) all_html_list = [ h for h in all_html_list if h != template_name ] del uniq_html_list[i] else: all_html_list.extend(temp_files[0]) uniq_html_list = list( OrderedDict.fromkeys(all_html_list).keys() ) all_style_list.extend(temp_files[1]) i += 1 # counter dict for the html files called from view result = { 'html': Counter(all_html_list), 'style': Counter(all_style_list) } print(result) return result def get_selectors_css(files): selectors = {} media_selectors = {} for file in files: if any(vendor in file for vendor in ['bootstrap', 'font-awesome']): continue result = finders.find(file) if result: with open(result) as f: data = f.read() media_selectors[file] = string_match_pattern( data, 'css_media' ) new_data = string_replace_pattern( data, 'css_media' ) selectors[file] = { 'default': string_match_pattern( new_data, 'css_selector' ) } # pp = pprint.PrettyPrinter(compact=False, width=120) # pp.pprint(media_selectors) for file, match_list in media_selectors.items(): for match in match_list: query = match[0] block_text = ' '.join(match[1].split()) results = string_match_pattern( block_text, 'css_selector' ) f_query = ' '.join(query.replace(':', ': ').split()) if f_query in selectors[file]: selectors[file][f_query].extend(results) else: selectors[file][f_query] = results # pp.pprint(selectors) return selectors def get_selectors_html(files): selectors = {} for file in files: results = templates_match_pattern(file, ['html_class', 'html_id']) selectors[file] = { 'class': results[0], 'id': results[1], } return selectors def file_match_pattern(file, patterns): with open(file) as f: data = f.read() results = string_match_pattern(data, patterns) return results def string_match_pattern(data, patterns): if not isinstance(patterns, str): results = [] for p in patterns: re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE) results.append(re.findall(re_pattern, data)) else: re_pattern = re.compile(RE_PATTERNS[patterns], re.MULTILINE) results = re.findall(re_pattern, data) return results def string_replace_pattern(data, patterns): if not isinstance(patterns, str): for p in patterns: re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE) data = re.sub(re_pattern, '', data) else: re_pattern = re.compile(RE_PATTERNS[patterns], re.MULTILINE) data = re.sub(re_pattern, '', data) return data def templates_match_pattern(template_name, patterns): t = template.loader.get_template(template_name) data = t.template.source results = string_match_pattern(data, patterns) return results def get_css_duplication(css_selectors): # duplicate css selectors in stylesheets for file in css_selectors: print(file) for media in css_selectors[file]: print(' '.join(media.replace(':', ': ').split())) print(len(css_selectors[file][media]), 'rules') # for selector in selectors: # if selector[0] in count: # count[selector[0]] += 1 # # print(file, selector[0], count[selector[0]]) # else: # count[selector[0]] = 1 def write_report(results, filename='frontend'): full_filename = '../optimize_' + filename + '.csv' output_file = os.path.join( settings.PROJECT_DIR, full_filename ) with open(output_file, 'w', newline='') as f: w = csv.writer(f) print(zip_longest(*results)) for r in zip_longest(*results): w.writerow(r) html_tags = [ "a", "abbr", "address", "article", "area", "aside", "audio", "b", "base", "bdi", "bdo", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "datalist", "dd", "del", "details", "dfn", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "map", "mark", "menu", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "source", "small", "span", "strong", "style", "sub", "summary", "sup", "textarea", "table", "tbody", "td", "tfoot", "thead", "th", "time", "title", "tr", "u", "ul", "var", "video", "wbr" ] bootstrap_classes = [ "active", ]