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…
	
	Add table
		Add a link
		
	
		Reference in a new issue