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 {
|
select {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,9 @@
|
||||||
<script src="{% static 'datacenterlight/js/jquery.js' %}"></script>
|
<script src="{% static 'datacenterlight/js/jquery.js' %}"></script>
|
||||||
<!-- Bootstrap Core JavaScript -->
|
<!-- Bootstrap Core JavaScript -->
|
||||||
<script src="{% static 'datacenterlight/js/bootstrap.min.js' %}"></script>
|
<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="//cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.9/validator.min.js"></script>
|
||||||
|
|
||||||
<script src="{% static 'datacenterlight/js/main.js' %}"></script>
|
<script src="{% static 'datacenterlight/js/main.js' %}"></script>
|
||||||
|
|
||||||
</body>
|
</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 logging
|
||||||
import os
|
import os
|
||||||
import pprint
|
|
||||||
import re
|
import re
|
||||||
from collections import Counter, OrderedDict
|
from collections import Counter, OrderedDict
|
||||||
from itertools import zip_longest
|
# from itertools import zip_longest
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -31,7 +54,10 @@ RE_PATTERNS = {
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
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
|
requires_system_checks = False
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
|
@ -50,7 +76,7 @@ class Command(BaseCommand):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--css',
|
'--css',
|
||||||
action='store_true',
|
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):
|
def handle(self, *args, **options):
|
||||||
|
@ -62,17 +88,40 @@ class Command(BaseCommand):
|
||||||
# optimize_all(app)
|
# optimize_all(app)
|
||||||
|
|
||||||
def optimize_css(self, app_name):
|
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
|
# get html and css files used in the app
|
||||||
files = get_files(app_name)
|
files = get_files(app_name)
|
||||||
# get_selectors_from_css
|
# get_selectors_from_css
|
||||||
css_selectors = get_selectors_css(files['style'])
|
css_selectors = get_selectors_css(files['style'])
|
||||||
# get_selectors_from_html
|
# get_selectors_from_html
|
||||||
html_selectors = get_selectors_html(files['html'])
|
html_selectors = get_selectors_html(files['html'])
|
||||||
# get duplication of css rules from css files
|
report = {
|
||||||
css_dup_report = get_css_duplication(css_selectors)
|
'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):
|
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
|
# the view file for the app
|
||||||
app_view = os.path.join(settings.PROJECT_DIR, app_name, 'views.py')
|
app_view = os.path.join(settings.PROJECT_DIR, app_name, 'views.py')
|
||||||
# get template files called from the view
|
# get template files called from the view
|
||||||
|
@ -109,13 +158,31 @@ def get_files(app_name):
|
||||||
'html': Counter(all_html_list),
|
'html': Counter(all_html_list),
|
||||||
'style': Counter(all_style_list)
|
'style': Counter(all_style_list)
|
||||||
}
|
}
|
||||||
print(result)
|
# print(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_selectors_css(files):
|
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 = {}
|
selectors = {}
|
||||||
media_selectors = {}
|
media_selectors = {}
|
||||||
|
# get media selectors and other simple declarations
|
||||||
for file in files:
|
for file in files:
|
||||||
if any(vendor in file for vendor in ['bootstrap', 'font-awesome']):
|
if any(vendor in file for vendor in ['bootstrap', 'font-awesome']):
|
||||||
continue
|
continue
|
||||||
|
@ -123,20 +190,12 @@ def get_selectors_css(files):
|
||||||
if result:
|
if result:
|
||||||
with open(result) as f:
|
with open(result) as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
media_selectors[file] = string_match_pattern(
|
media_selectors[file] = string_match_pattern(data, 'css_media')
|
||||||
data, 'css_media'
|
new_data = string_remove_pattern(data, 'css_media')
|
||||||
)
|
|
||||||
new_data = string_replace_pattern(
|
|
||||||
data, 'css_media'
|
|
||||||
)
|
|
||||||
selectors[file] = {
|
selectors[file] = {
|
||||||
'default': string_match_pattern(
|
'default': string_match_pattern(new_data, 'css_selector')
|
||||||
new_data, 'css_selector'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
# pp = pprint.PrettyPrinter(compact=False, width=120)
|
# get declarations from media queries
|
||||||
# pp.pprint(media_selectors)
|
|
||||||
|
|
||||||
for file, match_list in media_selectors.items():
|
for file, match_list in media_selectors.items():
|
||||||
for match in match_list:
|
for match in match_list:
|
||||||
query = match[0]
|
query = match[0]
|
||||||
|
@ -149,11 +208,19 @@ def get_selectors_css(files):
|
||||||
selectors[file][f_query].extend(results)
|
selectors[file][f_query].extend(results)
|
||||||
else:
|
else:
|
||||||
selectors[file][f_query] = results
|
selectors[file][f_query] = results
|
||||||
# pp.pprint(selectors)
|
|
||||||
return selectors
|
return selectors
|
||||||
|
|
||||||
|
|
||||||
def get_selectors_html(files):
|
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 = {}
|
selectors = {}
|
||||||
for file in files:
|
for file in files:
|
||||||
results = templates_match_pattern(file, ['html_class', 'html_id'])
|
results = templates_match_pattern(file, ['html_class', 'html_id'])
|
||||||
|
@ -165,6 +232,19 @@ def get_selectors_html(files):
|
||||||
|
|
||||||
|
|
||||||
def file_match_pattern(file, patterns):
|
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:
|
with open(file) as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
results = string_match_pattern(data, patterns)
|
results = string_match_pattern(data, patterns)
|
||||||
|
@ -172,6 +252,19 @@ def file_match_pattern(file, patterns):
|
||||||
|
|
||||||
|
|
||||||
def string_match_pattern(data, 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):
|
if not isinstance(patterns, str):
|
||||||
results = []
|
results = []
|
||||||
for p in patterns:
|
for p in patterns:
|
||||||
|
@ -183,7 +276,17 @@ def string_match_pattern(data, patterns):
|
||||||
return results
|
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):
|
if not isinstance(patterns, str):
|
||||||
for p in patterns:
|
for p in patterns:
|
||||||
re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE)
|
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):
|
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)
|
t = template.loader.get_template(template_name)
|
||||||
data = t.template.source
|
data = t.template.source
|
||||||
results = string_match_pattern(data, patterns)
|
results = string_match_pattern(data, patterns)
|
||||||
|
@ -202,32 +318,65 @@ def templates_match_pattern(template_name, patterns):
|
||||||
|
|
||||||
|
|
||||||
def get_css_duplication(css_selectors):
|
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
|
# duplicate css selectors in stylesheets
|
||||||
for file in css_selectors:
|
rule_count = {}
|
||||||
print(file)
|
for file, media_selectors in css_selectors.items():
|
||||||
for media in css_selectors[file]:
|
rule_count[file] = {}
|
||||||
print(' '.join(media.replace(':', ': ').split()))
|
for media, rules in media_selectors.items():
|
||||||
print(len(css_selectors[file][media]), 'rules')
|
rules_dict = Counter([rule[0] for rule in rules])
|
||||||
# for selector in selectors:
|
dup_rules_dict = {k: v for k, v in rules_dict.items() if v > 1}
|
||||||
# if selector[0] in count:
|
if dup_rules_dict:
|
||||||
# count[selector[0]] += 1
|
rule_count[file][media] = dup_rules_dict
|
||||||
# # print(file, selector[0], count[selector[0]])
|
return rule_count
|
||||||
# else:
|
|
||||||
# count[selector[0]] = 1
|
|
||||||
|
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'):
|
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(
|
output_file = os.path.join(
|
||||||
settings.PROJECT_DIR, full_filename
|
settings.PROJECT_DIR, full_filename
|
||||||
)
|
)
|
||||||
with open(output_file, 'w', newline='') as f:
|
with open(output_file, 'w', newline='') as f:
|
||||||
w = csv.writer(f)
|
data = template.loader.render_to_string('utils/report.html', results)
|
||||||
print(zip_longest(*results))
|
f.write(data)
|
||||||
for r in zip_longest(*results):
|
# w = csv.writer(f)
|
||||||
w.writerow(r)
|
# 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 = [
|
html_tags = [
|
||||||
"a",
|
"a",
|
||||||
"abbr",
|
"abbr",
|
||||||
|
@ -336,7 +485,3 @@ html_tags = [
|
||||||
"video",
|
"video",
|
||||||
"wbr"
|
"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…
Reference in a new issue