diff --git a/utils/management/commands/optimize_frontend.py b/utils/management/commands/optimize_frontend.py new file mode 100644 index 00000000..51e460c3 --- /dev/null +++ b/utils/management/commands/optimize_frontend.py @@ -0,0 +1,287 @@ +import csv +import logging +import os +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_selector': ( + '^\s*([.#\[:_A-Za-z][.#\[\]\(\)=:+~\-_A-Za-z0-9\s>,]*)' + '{([\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: + self.optimize_all(app) + + def optimize_css(self, app_name): + # get html and css files used in the app + files = self.get_files(app_name) + # get_selectors_from_css + css_selectors = self.get_selectors_css(files['style']) + # get_selectors_from_html + html_selectors = self.get_selectors_html(files['html']) + + # duplicate css selectors in stylesheets + for file, selectors in css_selectors.items(): + count = {} + 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 + # print(count) + + def get_files(self, 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'])[0] + # 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(self, files): + selectors = {} + for file in files: + if any(vendor in file for vendor in ['bootstrap', 'font-awesome']): + continue + result = finders.find(file) + if result: + selectors[file] = file_match_pattern( + result, ['css_selector'] + )[0] + return selectors + + def get_selectors_html(self, files): + selectors = {} + for file in files: + results = templates_match_pattern(file, ['html_class', 'html_id']) + selectors[file] = { + 'class': results[0], + 'id': results[0], + } + return selectors + + def selectors_css(self, 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): + # print(r) + w.writerow(r) + + +def file_match_pattern(file, patterns): + results = [] + with open(file) as f: + data = f.read() + for p in patterns: + results.append( + re.findall(re.compile(RE_PATTERNS[p], re.MULTILINE), data) + ) + return results + + +def templates_match_pattern(template_name, patterns): + t = template.loader.get_template(template_name) + data = t.template.source + results = [] + for p in patterns: + results.append( + re.findall(re.compile(RE_PATTERNS[p], re.MULTILINE), data) + ) + return results + + +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" +] + +exempt_classes = [ + "active", +]