285 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
	
		
			6.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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
 | 
						|
 | 
						|
    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):
 | 
						|
                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",
 | 
						|
]
 |