optimize script documentation and report output
This commit is contained in:
		
					parent
					
						
							
								4ebd52cd69
							
						
					
				
			
			
				commit
				
					
						a43428539f
					
				
			
		
					 4 changed files with 244 additions and 43 deletions
				
			
		| 
						 | 
				
			
			@ -175,7 +175,7 @@
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 576px) {
 | 
			
		||||
@media (max-width: 575px) {
 | 
			
		||||
  select {
 | 
			
		||||
    width: 280px;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,7 +55,9 @@
 | 
			
		|||
    <script src="{% static 'datacenterlight/js/jquery.js' %}"></script>
 | 
			
		||||
    <!-- Bootstrap Core JavaScript -->
 | 
			
		||||
    <script src="{% static 'datacenterlight/js/bootstrap.min.js' %}"></script>
 | 
			
		||||
    <!-- Bootstrap Validator -->
 | 
			
		||||
    <script src="//cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.9/validator.min.js"></script>
 | 
			
		||||
 | 
			
		||||
    <script src="{% static 'datacenterlight/js/main.js' %}"></script>
 | 
			
		||||
 | 
			
		||||
</body>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,33 @@
 | 
			
		|||
import csv
 | 
			
		||||
"""
 | 
			
		||||
This command finds and creates a report for all the usage of css rules in
 | 
			
		||||
an app. It aims to optimize existing codebase as well as assist the frontend
 | 
			
		||||
developer when designing new components by avoiding unnecessary duplication and
 | 
			
		||||
suggesting more/optimal alternatives.
 | 
			
		||||
 | 
			
		||||
Features:
 | 
			
		||||
    Currently the command can find out and display:
 | 
			
		||||
        - Media Breakpoints used in a stylesheet
 | 
			
		||||
        - Duplicate selectors in a stylesheet
 | 
			
		||||
        - Unused selectors
 | 
			
		||||
    Work in progress to enable these features:
 | 
			
		||||
        - Duplicate style declaration for same selector
 | 
			
		||||
        - DOM validation
 | 
			
		||||
        - Finding out dead styles (those that are always cancelled)
 | 
			
		||||
        - Optimize media declarations
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
    $ python manage.py optimize_frontend datacenterlight
 | 
			
		||||
    above command produces a file ../optimize_frontend.html which contains a
 | 
			
		||||
    report with the above mentioned features
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# import csv
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import pprint
 | 
			
		||||
import re
 | 
			
		||||
from collections import Counter, OrderedDict
 | 
			
		||||
from itertools import zip_longest
 | 
			
		||||
# from itertools import zip_longest
 | 
			
		||||
 | 
			
		||||
from django import template
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +54,10 @@ RE_PATTERNS = {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help = 'Finds and fixes unused css styles in the templates'
 | 
			
		||||
    help = (
 | 
			
		||||
        'Finds unused and duplicate style declarations from the stylesheets '
 | 
			
		||||
        'used in the templates of each app'
 | 
			
		||||
    )
 | 
			
		||||
    requires_system_checks = False
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
| 
						 | 
				
			
			@ -50,7 +76,7 @@ class Command(BaseCommand):
 | 
			
		|||
        parser.add_argument(
 | 
			
		||||
            '--css',
 | 
			
		||||
            action='store_true',
 | 
			
		||||
            help='optimize only css rules in each file'
 | 
			
		||||
            help='optimize only the css rules declared in each stylesheet'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
| 
						 | 
				
			
			@ -62,17 +88,40 @@ class Command(BaseCommand):
 | 
			
		|||
            #     optimize_all(app)
 | 
			
		||||
 | 
			
		||||
    def optimize_css(self, app_name):
 | 
			
		||||
        """Optimize declarations inside a css stylesheet
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            app_name (str): The application 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)
 | 
			
		||||
        report = {
 | 
			
		||||
            'css_dup': get_css_duplication(css_selectors),
 | 
			
		||||
            'css_unused': get_css_unused(css_selectors, html_selectors)
 | 
			
		||||
        }
 | 
			
		||||
        # write report
 | 
			
		||||
        write_report(report)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_files(app_name):
 | 
			
		||||
    """Get all the `html` and `css` files used in an app.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        app_name (str): The application name
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        dict: A dictonary containing Counter of occurence of each
 | 
			
		||||
        html and css file in `html` and `style` fields respectively.
 | 
			
		||||
        For example:
 | 
			
		||||
        {
 | 
			
		||||
            'html': {'datacenterlight/success.html': 1},
 | 
			
		||||
            'style': {'datacenterlight/css/bootstrap.min.css': 2}
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
    # 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
 | 
			
		||||
| 
						 | 
				
			
			@ -109,13 +158,31 @@ def get_files(app_name):
 | 
			
		|||
        'html': Counter(all_html_list),
 | 
			
		||||
        'style': Counter(all_style_list)
 | 
			
		||||
    }
 | 
			
		||||
    print(result)
 | 
			
		||||
    # print(result)
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_selectors_css(files):
 | 
			
		||||
    """Gets the selectors and declarations from a stylesheet.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        files (list): A list of path of stylesheets.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        dict: A nested dictionary with the structre as
 | 
			
		||||
        `{'file': {'media-selector': [('selectors',`declarations')]}}`
 | 
			
		||||
        For example:
 | 
			
		||||
        {
 | 
			
		||||
            'datacenterlight/css/landing-page.css':{
 | 
			
		||||
                '(min-width: 768px)': [
 | 
			
		||||
                    ('.lead-right', 'text-align: right;'),
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    """
 | 
			
		||||
    selectors = {}
 | 
			
		||||
    media_selectors = {}
 | 
			
		||||
    # get media selectors and other simple declarations
 | 
			
		||||
    for file in files:
 | 
			
		||||
        if any(vendor in file for vendor in ['bootstrap', 'font-awesome']):
 | 
			
		||||
            continue
 | 
			
		||||
| 
						 | 
				
			
			@ -123,20 +190,12 @@ def get_selectors_css(files):
 | 
			
		|||
        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'
 | 
			
		||||
            )
 | 
			
		||||
            media_selectors[file] = string_match_pattern(data, 'css_media')
 | 
			
		||||
            new_data = string_remove_pattern(data, 'css_media')
 | 
			
		||||
            selectors[file] = {
 | 
			
		||||
                'default': string_match_pattern(
 | 
			
		||||
                    new_data, 'css_selector'
 | 
			
		||||
                )
 | 
			
		||||
                'default': string_match_pattern(new_data, 'css_selector')
 | 
			
		||||
            }
 | 
			
		||||
    # pp = pprint.PrettyPrinter(compact=False, width=120)
 | 
			
		||||
    # pp.pprint(media_selectors)
 | 
			
		||||
 | 
			
		||||
    # get declarations from media queries
 | 
			
		||||
    for file, match_list in media_selectors.items():
 | 
			
		||||
        for match in match_list:
 | 
			
		||||
            query = match[0]
 | 
			
		||||
| 
						 | 
				
			
			@ -149,11 +208,19 @@ def get_selectors_css(files):
 | 
			
		|||
                selectors[file][f_query].extend(results)
 | 
			
		||||
            else:
 | 
			
		||||
                selectors[file][f_query] = results
 | 
			
		||||
    # pp.pprint(selectors)
 | 
			
		||||
    return selectors
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_selectors_html(files):
 | 
			
		||||
    """Get `class` and `id` used in html files.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        files (list): A list of html files path.
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        dict: a dictonary of all the classes and ids found in the file, in
 | 
			
		||||
        `class` and `id` field respectively.
 | 
			
		||||
    """
 | 
			
		||||
    selectors = {}
 | 
			
		||||
    for file in files:
 | 
			
		||||
        results = templates_match_pattern(file, ['html_class', 'html_id'])
 | 
			
		||||
| 
						 | 
				
			
			@ -165,6 +232,19 @@ def get_selectors_html(files):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def file_match_pattern(file, patterns):
 | 
			
		||||
    """Match a regex pattern in a file
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        file (str): Complete path of file
 | 
			
		||||
        patterns (list or str): The pattern(s) to be searched in the file
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        list: A list of all the matches in the file. Each item is a list of
 | 
			
		||||
        all the captured groups in the pattern. If multiple patterns are given,
 | 
			
		||||
        the returned list is a list of such lists.
 | 
			
		||||
        For example:
 | 
			
		||||
        [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')]
 | 
			
		||||
    """
 | 
			
		||||
    with open(file) as f:
 | 
			
		||||
        data = f.read()
 | 
			
		||||
    results = string_match_pattern(data, patterns)
 | 
			
		||||
| 
						 | 
				
			
			@ -172,6 +252,19 @@ def file_match_pattern(file, patterns):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def string_match_pattern(data, patterns):
 | 
			
		||||
    """Match a regex pattern in a string
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        data (str): the string to search for the pattern
 | 
			
		||||
        patterns (list or str): The pattern(s) to be searched in the file
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        list: A list of all the matches in the string. Each item is a list of
 | 
			
		||||
        all the captured groups in the pattern. If multiple patterns are given,
 | 
			
		||||
        the returned list is a list of such lists.
 | 
			
		||||
        For example:
 | 
			
		||||
        [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')]
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(patterns, str):
 | 
			
		||||
        results = []
 | 
			
		||||
        for p in patterns:
 | 
			
		||||
| 
						 | 
				
			
			@ -183,7 +276,17 @@ def string_match_pattern(data, patterns):
 | 
			
		|||
    return results
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def string_replace_pattern(data, patterns):
 | 
			
		||||
def string_remove_pattern(data, patterns):
 | 
			
		||||
    """Remove a pattern from a string
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        data (str): the string to search for the patter
 | 
			
		||||
        patterns (list or str): The pattern(s) to be removed from the file
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        str: The new string with all instance of matching pattern removed
 | 
			
		||||
        from it
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(patterns, str):
 | 
			
		||||
        for p in patterns:
 | 
			
		||||
            re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE)
 | 
			
		||||
| 
						 | 
				
			
			@ -195,6 +298,19 @@ def string_replace_pattern(data, patterns):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def templates_match_pattern(template_name, patterns):
 | 
			
		||||
    """Match a regex pattern in the first found template file
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        file (str): Path of template file
 | 
			
		||||
        patterns (list or str): The pattern(s) to be searched in the file
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        list: A list of all the matches in the file. Each item is a list of
 | 
			
		||||
        all the captured groups in the pattern. If multiple patterns are given,
 | 
			
		||||
        the returned list is a list of such lists.
 | 
			
		||||
        For example:
 | 
			
		||||
        [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')]
 | 
			
		||||
    """
 | 
			
		||||
    t = template.loader.get_template(template_name)
 | 
			
		||||
    data = t.template.source
 | 
			
		||||
    results = string_match_pattern(data, patterns)
 | 
			
		||||
| 
						 | 
				
			
			@ -202,32 +318,65 @@ def templates_match_pattern(template_name, patterns):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
def get_css_duplication(css_selectors):
 | 
			
		||||
    """Get duplicate selectors from the same stylesheet
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        css_selectors (dict): A dictonary containing css selectors from
 | 
			
		||||
        all the files in the app in the below structure.
 | 
			
		||||
        `{'file': {'media-selector': [('selectors',`declarations')]}}`
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        dict: A dictonary containing the count of any duplicate selector in
 | 
			
		||||
        each file.
 | 
			
		||||
        `{'file': {'media-selector': {'selector': count}}}`
 | 
			
		||||
    """
 | 
			
		||||
    # 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
 | 
			
		||||
    rule_count = {}
 | 
			
		||||
    for file, media_selectors in css_selectors.items():
 | 
			
		||||
        rule_count[file] = {}
 | 
			
		||||
        for media, rules in media_selectors.items():
 | 
			
		||||
            rules_dict = Counter([rule[0] for rule in rules])
 | 
			
		||||
            dup_rules_dict = {k: v for k, v in rules_dict.items() if v > 1}
 | 
			
		||||
            if dup_rules_dict:
 | 
			
		||||
                rule_count[file][media] = dup_rules_dict
 | 
			
		||||
    return rule_count
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_css_unused(css_selectors, html_selectors):
 | 
			
		||||
    """Get selectors from stylesheets that are not used in any of the html
 | 
			
		||||
    files in which the stylesheet is used.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        css_selectors (dict): A dictonary containing css selectors from
 | 
			
		||||
        all the files in the app in the below structure.
 | 
			
		||||
        `{'file': {'media-selector': [('selectors',`declarations')]}}`
 | 
			
		||||
        html_selectors (dict): A dictonary containing the 'class' and 'id'
 | 
			
		||||
        declarations from all html files
 | 
			
		||||
    """
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_report(results, filename='frontend'):
 | 
			
		||||
    full_filename = '../optimize_' + filename + '.csv'
 | 
			
		||||
    """Write the generated report to a file for re-use
 | 
			
		||||
 | 
			
		||||
    Args;
 | 
			
		||||
        results (dict): A dictonary of results obtained from different tests
 | 
			
		||||
        filename (str): An optional suffix for the output file
 | 
			
		||||
    """
 | 
			
		||||
    full_filename = '../optimize_' + filename + '.html'
 | 
			
		||||
    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)
 | 
			
		||||
        data = template.loader.render_to_string('utils/report.html', results)
 | 
			
		||||
        f.write(data)
 | 
			
		||||
        # w = csv.writer(f)
 | 
			
		||||
        # print(zip_longest(*results))
 | 
			
		||||
        # for r in zip_longest(*results):
 | 
			
		||||
        #     w.writerow(r)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# a list of all the html tags (to be moved in a json file)
 | 
			
		||||
html_tags = [
 | 
			
		||||
    "a",
 | 
			
		||||
    "abbr",
 | 
			
		||||
| 
						 | 
				
			
			@ -336,7 +485,3 @@ html_tags = [
 | 
			
		|||
    "video",
 | 
			
		||||
    "wbr"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
bootstrap_classes = [
 | 
			
		||||
    "active",
 | 
			
		||||
]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										54
									
								
								utils/templates/utils/report.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								utils/templates/utils/report.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
{% load staticfiles i18n %}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="{{LANGUAGE_CODE}}">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
  <meta name="description" content="Frontend Style Usage Report">
 | 
			
		||||
  <meta name="author" content="ungleich GmbH">
 | 
			
		||||
  <title>Usage Report - {% block title %}{% endblock %}</title>
 | 
			
		||||
  <!-- Bootstrap Core CSS -->
 | 
			
		||||
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
 | 
			
		||||
  <link rel="shortcut icon" href="{% static 'ungleich_page/img/favicon.ico' %}" type="image/x-icon">
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
  <div class="container pt-3">
 | 
			
		||||
    <div class="card">
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <div class="card-title pb-3">
 | 
			
		||||
          <h3>Duplicate Rules in a Stylesheet</h3>
 | 
			
		||||
          <hr>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="card-text">
 | 
			
		||||
          {% for file, media_group in css_dup.items %}
 | 
			
		||||
            <strong>{{file}}</strong>
 | 
			
		||||
            <ul class="list-unstyled">
 | 
			
		||||
              {% for media, rules in media_group.items %}
 | 
			
		||||
                <li>
 | 
			
		||||
                  {{media}} :
 | 
			
		||||
                  <ul>
 | 
			
		||||
                    {% for rule, count in rules.items %}
 | 
			
		||||
                      <li><strong>{{rule}}</strong> <em>({{count}})</em></li>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                  </ul>
 | 
			
		||||
                </li>
 | 
			
		||||
              {% empty %}
 | 
			
		||||
                <li class="text-success">No Duplicates!</li>
 | 
			
		||||
              {% endfor %}
 | 
			
		||||
            </ul>
 | 
			
		||||
          {% endfor %}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  {% for app in app_list %} {% endfor %}
 | 
			
		||||
  <!-- jQuery -->
 | 
			
		||||
  <script src="{% static 'datacenterlight/js/jquery.js' %}"></script>
 | 
			
		||||
  <!-- Bootstrap Core JavaScript -->
 | 
			
		||||
  <script src="{% static 'datacenterlight/js/bootstrap.min.js' %}"></script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue