diff --git a/README.md b/README.md index f012b4a..655c25f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ # css-html-prettify -Async single-file cross-platform Unicode-ready Python3-ready Prettifier Beautifier for the Web. +Async single-file cross-platform no-dependencies Prettifier Beautifier for the Web. [![GPL License](http://img.shields.io/badge/license-GPL-blue.svg?style=plastic)](http://opensource.org/licenses/GPL-3.0) [![LGPL License](http://img.shields.io/badge/license-LGPL-blue.svg?style=plastic)](http://opensource.org/licenses/LGPL-3.0) [![Python Version](https://img.shields.io/badge/Python-3-brightgreen.svg?style=plastic)](http://python.org) -[![GPL License](http://img.shields.io/badge/license-GPL-blue.svg?style=plastic)](http://opensource.org/licenses/GPL-3.0) [![LGPL License](http://img.shields.io/badge/license-LGPL-blue.svg?style=plastic)](http://opensource.org/licenses/LGPL-3.0) [![Python Version](https://img.shields.io/badge/Python-3-brightgreen.svg?style=plastic)](http://python.org) [![Join the chat at https://gitter.im/juancarlospaco/css-html-js-minify](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/juancarlospaco/css-html-js-minify?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge "Chat with Users and Developers, Get Solutions, Offer Help") [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif "Donate with or without Credit Card")](http://goo.gl/cB7PR) +![screenshot](https://source.unsplash.com/q78PYnUehV8/800x402 "Illustrative Photo by https://unsplash.com/@s_erwin") +https://pypi.python.org/pypi/css-html-prettify + ```bash css-html-prettify.py --help @@ -31,6 +33,7 @@ optional arguments: --watch Re-Compress if file changes (Experimental). --group Group Alphabetically CSS Poperties by name. --justify Right Justify CSS Properties (Experimental). + --extraline Add 1 New Line for each New Line (Experimental) CSS-HTML-Prettify: Takes file or folder full path string and process all CSS/SCSS/HTML found. If argument is not file/folder will fail. Check Updates @@ -43,7 +46,6 @@ not. Watch works for whole folders, with minimum of ~60 Secs between runs. - If full path is a folder with multiple files it will use Async Multiprocessing. - Pretty-Printed colored Logging to Standard Output and Log File on OS Temporary Folder. - Set its own Process name and show up on Process lists. -- Can check for updates for itself. - Full Unicode/UTF-8 support, SASS SCSS Support. - Smooth CPU usage. - Can Watch for changes on files. @@ -51,9 +53,9 @@ not. Watch works for whole folders, with minimum of ~60 Secs between runs. - `*.css` files are saved as `*.css`, `*.html` are saved as `*.html`, unless provided a prefix. -# Usage: +# Use -```bash +```shell css-html-prettify.py file.html css-html-prettify.py file.htm @@ -66,19 +68,34 @@ css-html-prettify.py /project/static/ ``` -# Install permanently on the system: +# Install - -**WGET:** ``` -sudo apt-get install python3-bs4 -sudo wget -O /usr/bin/css-html-prettify https://raw.githubusercontent.com/juancarlospaco/css-html-prettify/master/css-html-prettify.py -sudo chmod +x /usr/bin/css-html-prettify -css-html-prettify +pip install css-html-prettify ``` +Uninstall `pip uninstall css-html-prettify` + + +# Why? + +- This project is the small brother of [another project that does the inverse, a Minifier Compressor for the Web.](https://github.com/juancarlospaco/css-html-js-minify#css-html-js-minify) + + +# Requisites + +- [Python 3.6+](https://www.python.org "Python Homepage") + +**Optional:** + +- BeautifulSoup 4+ (Recommeded, for HTML5 Prettify, without BeautifulSoup it works, but only strict XHTML can be processed) + +# Example + +
**Input CSS:** + ```css /* dont remove this comment */ .class, #NotHex, input[type="text"], a:hover { @@ -101,6 +118,7 @@ css-html-prettify ``` **Output CSS:** + ```css @charset utf-8; @@ -134,36 +152,21 @@ css-html-prettify ``` - -# Why?: - -- This project is the small brother of [another project that does the inverse, a Minifier Compressor for the Web.](https://github.com/juancarlospaco/css-html-js-minify#css-html-js-minify) - - -# Requisites: - -- [Python 3.x](https://www.python.org "Python Homepage") *(or Python 2.x, or PyPy 2.x, or PyPy 3.x)* -- BeautifulSoup 4. +
-# Coding Style Guide: +# Coding Style Guide -- Lint, PEP-8, PEP-257, PyLama, iSort must Pass Ok. `pip install pep8 pep257 pylama isort` +- Lint, [PEP-8](https://www.python.org/dev/peps/pep-0008), [PEP-257](https://www.python.org/dev/peps/pep-0257), [iSort](https://github.com/timothycrosley/isort) must Pass Ok. `pip install prospector pre-commit isort` +- If theres any kind of Tests, they must Pass Ok, if theres no Tests, its ok, if Tests provided, is even better. -# Contributors: +# Contributors -- **Ad-Hocracy Meritocracy**: 3 Pull Requests Merged on Master you become Repo Admin. *Join us!* +- **Please Star this Repo on Github !**, it helps to show up faster on searchs. - [Help](https://help.github.com/articles/using-pull-requests) and more [Help](https://help.github.com/articles/fork-a-repo) and Interactive Quick [Git Tutorial](https://try.github.io). -# Licence: - -- GNU GPL Latest Version *AND* GNU LGPL Latest Version *AND* any Licence YOU Request via Bug Report. - - -Donate, Charityware : ---------------------- +# Licence -- [Charityware](https://en.wikipedia.org/wiki/Donationware) is a licensing model that supplies fully operational unrestricted software to the user and requests an optional donation be paid to a third-party beneficiary non-profit. The amount may be left to discretion of the user. -- If you want to Donate please [click here](http://www.icrc.org/eng/donations/index.jsp) or [click here](http://www.atheistalliance.org/support-aai/donate) or [click here](http://www.msf.org/donate) or [click here](http://richarddawkins.net/) or [click here](http://www.supportunicef.org/) or [click here](http://www.amnesty.org/en/donate) +- GNU GPL Latest Version *AND* GNU LGPL Latest Version *AND* any Licence [YOU Request via Bug Report](https://github.com/juancarlospaco/css-html-prettify/issues/new). diff --git a/css-html-prettify.py b/css-html-prettify.py index 377084b..3d5810f 100644 --- a/css-html-prettify.py +++ b/css-html-prettify.py @@ -4,48 +4,29 @@ """CSS-HTML-Prettify. -StandAlone Async single-file cross-platform no-dependencies -Unicode-ready Python3-ready Prettifier Beautifier for the Web. +StandAlone Async cross-platform Prettifier Beautifier for the Web. """ import itertools -import logging as log import os import re -import socket import sys + from argparse import ArgumentParser -from copy import copy -from ctypes import byref, cdll, create_string_buffer from datetime import datetime -from getpass import getuser -from multiprocessing import Pool, cpu_count -from platform import platform, python_version -from random import randint -from tempfile import gettempdir +from multiprocessing import cpu_count, Pool from time import sleep +from subprocess import getoutput try: from bs4 import BeautifulSoup except ImportError: - print("BeautifulSoup4 Not Found!, use: sudo apt-get install python3-bs4") - -try: - from urllib import request - from subprocess import getoutput - import resource # windows dont have resource -except ImportError: - request = getoutput = resource = None + BeautifulSoup = None + print("BeautifulSoup4 Not Found, use: pip install BeautifulSoup4") -__version__ = "1.0.0" -__license__ = "GPLv3+ LGPLv3+" -__author__ = "Juan Carlos" -__email__ = "juancarlospaco@gmail.com" -__url__ = "https://github.com/juancarlospaco/css-html-prettify" -__source__ = ("https://raw.githubusercontent.com/juancarlospaco/" - "css-html-prettify/master/css-html-prettify.py") +__version__ = '2.5.5' start_time = datetime.now() @@ -56,24 +37,28 @@ animation-name animation-play-state animation-timing-function appearance azimuth -backface-visibility background background-attachment background-clip -background-color background-image background-origin background-position -background-repeat background-size baseline-shift bikeshedding bookmark-label -bookmark-level bookmark-state bookmark-target border border-bottom -border-bottom-color border-bottom-left-radius border-bottom-right-radius -border-bottom-style border-bottom-width border-collapse border-color -border-image border-image-outset border-image-repeat border-image-slice -border-image-source border-image-width border-left border-left-color -border-left-style border-left-width border-radius border-right -border-right-color border-right-style border-right-width border-spacing -border-style border-top border-top-color border-top-left-radius -border-top-right-radius border-top-style border-top-width border-width bottom -box-decoration-break box-shadow box-sizing +backface-visibility background background-blend-mode background-attachment +background-clip background-color background-image background-origin +background-position background-position-block background-position-inline +background-position-x background-position-y background-repeat background-size +baseline-shift bikeshedding bookmark-label bookmark-level bookmark-state +bookmark-target border border-bottom border-bottom-color +border-bottom-left-radius border-bottom-parts border-bottom-right-radius +border-bottom-style border-bottom-width border-clip border-clip-top +border-clip-right border-clip-bottom border-clip-left border-collapse +border-color border-corner-shape border-image border-image-outset +border-image-repeat border-image-slice border-image-source border-image-width +border-left border-left-color border-left-style border-left-parts +border-left-width border-limit border-parts border-radius border-right +border-right-color border-right-style border-right-width border-right-parts +border-spacing border-style border-top border-top-color border-top-left-radius +border-top-parts border-top-right-radius border-top-style border-top-width +border-width bottom box-decoration-break box-shadow box-sizing caption-side clear clip color column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width -columns content counter-increment counter-reset cue cue-after cue-before -cursor +columns content counter-increment counter-reset corners corner-shape +cue cue-after cue-before cursor direction display drop-initial-after-adjust drop-initial-after-align drop-initial-before-adjust drop-initial-before-align drop-initial-size @@ -81,11 +66,14 @@ elevation empty-cells -fit fit-position float font font-family font-size font-size-adjust -font-stretch font-style font-variant font-weight +flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap fit +fit-position float font font-family font-size font-size-adjust font-stretch +font-style font-variant font-weight grid-columns grid-rows +justify-content + hanging-punctuation height hyphenate-character hyphenate-resource hyphens icon image-orientation image-resolution inline-box-align @@ -131,22 +119,22 @@ z-index -''' +''' # Do Not compact this string, new lines are used to Group up stuff. ############################################################################### # CSS prettify -def _compile_props(props_text, grouped=False): +def _compile_props(props_text: str, grouped: bool=False) -> tuple: """Take a list of props and prepare them.""" - props = [] - for line_of_props in props_text.strip().lower().splitlines(): - props += line_of_props.split(" ") + props, prefixes = [], "-webkit-,-khtml-,-epub-,-moz-,-ms-,-o-,".split(",") + for propline in props_text.strip().lower().splitlines(): + props += [pre + pro for pro in propline.split(" ") for pre in prefixes] props = filter(lambda line: not line.startswith('#'), props) if not grouped: props = list(filter(None, props)) - return props, [0]*len(props) + return props, [0] * len(props) final_props, groups, g_id = [], [], 0 for prop in props: if prop.strip(): @@ -154,30 +142,29 @@ def _compile_props(props_text, grouped=False): groups.append(g_id) else: g_id += 1 - return final_props, groups + return (final_props, groups) -def _prioritify(line_buffer, pgs): +def _prioritify(line_of_css: str, css_props_text_as_list: tuple) -> tuple: """Return args priority, priority is integer and smaller means higher.""" - props, groups = pgs - priority, group = 9999, 0 - for css_property in props: - if line_buffer.find(css_property + ':') != -1: - priority = props.index(css_property) - group = groups[priority] + sorted_css_properties, groups_by_alphabetic_order = css_props_text_as_list + priority_integer, group_integer = 9999, 0 + for css_property in sorted_css_properties: + if css_property.lower() == line_of_css.split(":")[0].lower().strip(): + priority_integer = sorted_css_properties.index(css_property) + group_integer = groups_by_alphabetic_order[priority_integer] break - return priority, group + return (priority_integer, group_integer) def _props_grouper(props, pgs): """Return groups for properties.""" - log.debug("Grouping all CSS / SCSS Properties.") if not props: return props - props = sorted([ - _ if _.strip().endswith(";") - and not _.strip().endswith("*/") and not _.strip().endswith("/*") - else _.rstrip() + ";\n" for _ in props]) + # props = sorted([ + # _ if _.strip().endswith(";") and + # not _.strip().endswith("*/") and not _.strip().endswith("/*") + # else _.rstrip() + ";\n" for _ in props]) props_pg = zip(map(lambda prop: _prioritify(prop, pgs), props), props) props_pg = sorted(props_pg, key=lambda item: item[0][1]) props_by_groups = map( @@ -194,14 +181,13 @@ def _props_grouper(props, pgs): return props -def sort_properties(css_unsorted_string): +def sort_properties(css_unsorted_string: str) -> str: """CSS Property Sorter Function. This function will read buffer argument, split it to a list by lines, sort it by defined rule, and return sorted buffer if it's CSS property. This function depends on '_prioritify' function. """ - log.debug("Alphabetically Sorting all CSS / SCSS Properties.") css_pgs = _compile_props(CSS_PROPS_TEXT, grouped=bool(args.group)) pattern = re.compile(r'(.*?{\r?\n?)(.*?)(}.*?)|(.*)', re.DOTALL + re.MULTILINE) @@ -223,27 +209,25 @@ def sort_properties(css_unsorted_string): return sorted_buffer -def remove_empty_rules(css): +def remove_empty_rules(css: str) -> str: """Remove empty rules.""" - log.debug("Removing all unnecessary empty rules.") return re.sub(r"[^\}\{]+\{\}", "", css) -def condense_zero_units(css): +def condense_zero_units(css: str) -> str: """Replace `0(px, em, %, etc)` with `0`.""" - log.debug("Condensing all zeroes on values.") - return re.sub(r"([\s:])(0)(px|em|rem|%|in|cm|mm|pc|pt|ex)", r"\1\2", css) + return re.sub(r"([\s:])(0)(px|em|%|in|q|ch|cm|mm|pc|pt|ex|rem|s|ms|" + r"deg|grad|rad|turn|vw|vh|vmin|vmax|fr)", r"\1\2", css) -def condense_semicolons(css): +def condense_semicolons(css: str) -> str: """Condense multiple adjacent semicolon characters into one.""" - log.debug("Condensing all unnecessary multiple adjacent semicolons.") return re.sub(r";;+", ";", css) -def wrap_css_lines(css, line_length=80): +def wrap_css_lines(css: str, line_length: int=80) -> str: """Wrap the lines of the given CSS to an approximate length.""" - log.debug("Wrapping lines to ~{} max line lenght.".format(line_length)) + print(f"Wrapping lines to ~{line_length} max line lenght.") lines, line_start = [], 0 for i, char in enumerate(css): # Its safe to break after } characters. @@ -255,32 +239,27 @@ def wrap_css_lines(css, line_length=80): return "\n".join(lines) -def add_encoding(css): +def add_encoding(css: str) -> str: """Add @charset 'UTF-8'; if missing.""" - log.debug("Adding encoding declaration if needed.") return "@charset utf-8;\n\n\n" + css if "@charset" not in css else css -def normalize_whitespace(css): +def normalize_whitespace(css: str) -> str: """Normalize css string white spaces.""" - log.debug("Starting to Normalize white spaces on CSS if needed.") css_no_trailing_whitespace = "" for line_of_css in css.splitlines(): # remove all trailing white spaces css_no_trailing_whitespace += line_of_css.rstrip() + "\n" css = css_no_trailing_whitespace css = re.sub(r"\n{3}", "\n\n\n", css) # if 3 new lines,make them 2 css = re.sub(r"\n{5}", "\n\n\n\n\n", css) # if 5 new lines, make them 4 - h_line = "/* {} */".format("-" * 72) # if >6 new lines, horizontal line - css = re.sub(r"\n{6,}", "\n\n\n{}\n\n\n".format(h_line), css) + css = re.sub(r"\n{6,}", f"\n\n\n/*{'-' * 72}*/\n\n\n", css) css = css.replace(" ;\n", ";\n").replace("{\n", " {\n") css = re.sub("\s{2,}{\n", " {\n", css) - log.debug("Finished Normalize white spaces on CSS.") return css.replace("\t", " ").rstrip() + "\n" -def justify_right(css): +def justify_right(css: str) -> str: """Justify to the Right all CSS properties on the argument css string.""" - log.debug("Starting Justify to the Right all CSS / SCSS Property values.") max_indent, right_justified_css = 1, "" for css_line in css.splitlines(): c_1 = len(css_line.split(":")) == 2 and css_line.strip().endswith(";") @@ -305,13 +284,11 @@ def justify_right(css): right_justified_css += justified_line_of_css + "\n" else: right_justified_css += line_of_css + "\n" - log.debug("Finished Justify to the Right all CSS / SCSS Property values.") return right_justified_css if max_indent > 1 else css -def split_long_selectors(css): +def split_long_selectors(css: str) -> str: """Split too large CSS Selectors chained with commas if > 80 chars.""" - log.debug("Splitting too long chained selectors on CSS / SCSS.") result = "" for line in css.splitlines(): cond_1 = len(line) > 80 and "," in line and line.strip().endswith("{") @@ -324,15 +301,14 @@ def split_long_selectors(css): return result -def simple_replace(css): +def simple_replace(css: str) -> str: """dumb simple replacements on CSS.""" return css.replace("}\n#", "}\n\n#").replace( "}\n.", "}\n\n.").replace("}\n*", "}\n\n*") -def css_prettify(css, justify=False): +def css_prettify(css: str, justify: bool=False, extraline: bool=False) -> str: """Prettify CSS main function.""" - log.info("Prettify CSS / SCSS...") css = sort_properties(css) css = condense_zero_units(css) css = wrap_css_lines(css, 80) @@ -342,7 +318,8 @@ def css_prettify(css, justify=False): css = justify_right(css) if justify else css css = add_encoding(css) css = simple_replace(css) - log.info("Finished Prettify CSS / SCSS !.") + if extraline: + css = "\n\n".join(css.replace("\t", " ").splitlines()) + "\n" return css @@ -350,55 +327,66 @@ def css_prettify(css, justify=False): # HTML Prettify -# http://stackoverflow.com/a/15513483 -orig_prettify = BeautifulSoup.prettify -regez = re.compile(r'^(\s*)', re.MULTILINE) +if BeautifulSoup: + # http://stackoverflow.com/a/15513483 + orig_prettify = BeautifulSoup.prettify + regez = re.compile(r'^(\s*)', re.MULTILINE) -def prettify(self, encoding=None, formatter="minimal", indent_width=4): - """Monkey Patch the BS4 prettify to allow custom indentations.""" - log.debug("Monkey Patching BeautifulSoup on-the-fly to process HTML...") - return regez.sub(r'\1' * indent_width, - orig_prettify(self, encoding, formatter)) + def prettify(self, encoding=None, formatter="minimal", indent_width=4): + """Monkey Patch the BS4 prettify to allow custom indentations.""" + print("Monkey Patching BeautifulSoup on-the-fly to process HTML...") + return regez.sub(r'\1' * indent_width, + orig_prettify(self, encoding, formatter)) -BeautifulSoup.prettify = prettify + BeautifulSoup.prettify = prettify -def html_prettify(html): - """Prettify HTML main function.""" - log.info("Prettify HTML...") - html = BeautifulSoup(html).prettify() - html = html.replace("\t", " ").strip() + "\n" - log.info("Finished prettify HTML !.") - return html + def html_prettify(html: str, extraline: bool=False) -> str: + """Prettify HTML main function.""" + html = BeautifulSoup(html).prettify() + if extraline: + html = "\n\n".join(html.replace("\t", " ").splitlines()) + "\n" + return html +else: + # XHTML Prettify + def html_prettify(html: str, extraline: bool=False) -> str: + """Prettify XHTML main function.""" + html = minidom.parseString(html).toprettyxml(indent=" ")[22:] + if extraline: + html = "\n\n".join(html.replace("\t", " ").splitlines()) + "\n" + return html ############################################################################## -def walkdir_to_filelist(where, target, omit): - """Perform full walk of where, gather full path of all files.""" - log.debug("""Recursively Scanning {}, searching for {}, and ignoring {}. - """.format(where, target, omit)) - return tuple([os.path.join(root, f) for root, d, files in os.walk(where) - for f in files if not f.startswith('.') # ignore hidden - and not f.endswith(omit) # not process processed file - and f.endswith(target)]) # only process target files +def walk2list(folder: str, target: tuple, omit: tuple=(), + showhidden: bool=False, topdown: bool=True, + onerror: object=None, followlinks: bool=False) -> tuple: + """Perform full walk, gather full path of all files.""" + oswalk = os.walk(folder, topdown=topdown, + onerror=onerror, followlinks=followlinks) + + return [os.path.abspath(os.path.join(r, f)) + for r, d, fs in oswalk + for f in fs if not f.startswith(() if showhidden else ".") and + not f.endswith(omit) and f.endswith(target)] def process_multiple_files(file_path): """Process multiple CSS, HTML files with multiprocessing.""" - log.debug("Process {} is Compressing {}.".format(os.getpid(), file_path)) + print(f"Process {os.getpid()} is processing {file_path}.") if args.watch: previous = int(os.stat(file_path).st_mtime) - log.info("Process {} is Watching {}.".format(os.getpid(), file_path)) + print(f"Process {os.getpid()} is Watching {file_path}.") while True: actual = int(os.stat(file_path).st_mtime) if previous == actual: sleep(60) else: previous = actual - log.debug("Modification detected on {}.".format(file_path)) + print("Modification detected on {file_path}.") if file_path.endswith((".css", ".scss")): process_single_css_file(file_path) else: @@ -410,13 +398,12 @@ def process_multiple_files(file_path): process_single_html_file(file_path) -def prefixer_extensioner(file_path): +def prefixer_extensioner(file_path: str) -> str: """Take a file path and safely prepend a prefix and change extension. This is needed because filepath.replace('.foo', '.bar') sometimes may replace '/folder.foo/file.foo' into '/folder.bar/file.bar' wrong!. """ - log.debug("Prepending '{}' Prefix to {}.".format(args.prefix, file_path)) extension = os.path.splitext(file_path)[1].lower() filenames = os.path.splitext(os.path.basename(file_path))[0] filenames = args.prefix + filenames if args.prefix else filenames @@ -425,199 +412,29 @@ def prefixer_extensioner(file_path): return file_path -def process_single_css_file(css_file_path): +def process_single_css_file(css_file_path: str) -> str: """Process a single CSS file.""" - log.info("Processing CSS / SCSS file: {}".format(css_file_path)) global args - try: # Python3 - with open(css_file_path, encoding="utf-8-sig") as css_file: - original_css = css_file.read() - except: # Python2 - with open(css_file_path) as css_file: - original_css = css_file.read() - log.debug("INPUT: Reading CSS file {}.".format(css_file_path)) - pretty_css = css_prettify(original_css, justify=args.justify) + with open(css_file_path, encoding="utf-8-sig") as css_file: + original_css = css_file.read() + pretty_css = css_prettify(original_css, args.justify, args.extraline) if args.timestamp: - taim = "/* {} */ ".format(datetime.now().isoformat()[:-7].lower()) + taim = f"/* {datetime.now().replace(microsecond=0).isoformat(' ')} */ " pretty_css = taim + pretty_css min_css_file_path = prefixer_extensioner(css_file_path) - try: - with open(min_css_file_path, "w", encoding="utf-8") as output_file: - output_file.write(pretty_css) - except: - with open(min_css_file_path, "w") as output_file: - output_file.write(pretty_css) - log.debug("OUTPUT: Writing CSS Minified {}.".format(min_css_file_path)) + with open(min_css_file_path, "w", encoding="utf-8") as output_file: + output_file.write(pretty_css) + return pretty_css -def process_single_html_file(html_file_path): +def process_single_html_file(html_file_path: str) -> str: """Process a single HTML file.""" - log.info("Processing HTML file: {}".format(html_file_path)) - try: # Python3 - with open(html_file_path, encoding="utf-8-sig") as html_file: - pretty_html = html_prettify(html_file.read()) - except: # Python2 - with open(html_file_path) as html_file: - pretty_html = html_prettify(html_file.read()) - log.debug("INPUT: Reading HTML file {}.".format(html_file_path)) + with open(html_file_path, encoding="utf-8-sig") as html_file: + pretty_html = html_prettify(html_file.read(), args.extraline) html_file_path = prefixer_extensioner(html_file_path) - try: # Python3 - with open(html_file_path, "w", encoding="utf-8") as output_file: - output_file.write(pretty_html) - except: # Python2 - with open(html_file_path, "w") as output_file: - output_file.write(pretty_html) - log.debug("OUTPUT: Writing HTML Minified {}.".format(html_file_path)) - - -def check_for_updates(): - """Method to check for updates from Git repo versus this version.""" - this_version = str(open(__file__).read()) - last_version = str(request.urlopen(__source__).read().decode("utf8")) - if this_version != last_version: - log.warning("Theres new Version available!,Update from " + __source__) - else: - log.info("No new updates!,You have the lastest version of this app.") - - -def make_root_check_and_encoding_debug(): - """Debug and Log Encodings and Check for root/administrator,return Boolean. - - >>> make_root_check_and_encoding_debug() - True - """ - log.info(__doc__) - log.debug("Python {0} on {1}.".format(python_version(), platform())) - log.debug("STDIN Encoding: {0}.".format(sys.stdin.encoding)) - log.debug("STDERR Encoding: {0}.".format(sys.stderr.encoding)) - log.debug("STDOUT Encoding:{}".format(getattr(sys.stdout, "encoding", ""))) - log.debug("Default Encoding: {0}.".format(sys.getdefaultencoding())) - log.debug("FileSystem Encoding: {0}.".format(sys.getfilesystemencoding())) - log.debug("PYTHONIOENCODING Encoding: {0}.".format( - os.environ.get("PYTHONIOENCODING", None))) - os.environ["PYTHONIOENCODING"] = "utf-8" - if not sys.platform.startswith("win"): # root check - if not os.geteuid(): - log.critical("Runing as root is not Recommended,NOT Run as root!.") - return False - elif sys.platform.startswith("win"): # administrator check - if getuser().lower().startswith("admin"): - log.critical("Runing as Administrator is not Recommended!.") - return False - return True - - -def set_process_name_and_cpu_priority(name): - """Set process name and cpu priority. - - >>> set_process_name_and_cpu_priority("test_test") - True - """ - try: - os.nice(19) # smooth cpu priority - libc = cdll.LoadLibrary("libc.so.6") # set process name - buff = create_string_buffer(len(name.lower().strip()) + 1) - buff.value = bytes(name.lower().strip().encode("utf-8")) - libc.prctl(15, byref(buff), 0, 0, 0) - except Exception: - return False # this may fail on windows and its normal, so be silent. - else: - log.debug("Process Name set to: {0}.".format(name)) - return True - - -def set_single_instance(name, single_instance=True, port=8888): - """Set process name and cpu priority, return socket.socket object or None. - - >>> isinstance(set_single_instance("test"), socket.socket) - True - """ - __lock = None - if single_instance: - try: # Single instance app ~crossplatform, uses udp socket. - log.info("Creating Abstract UDP Socket Lock for Single Instance.") - __lock = socket.socket( - socket.AF_UNIX if sys.platform.startswith("linux") - else socket.AF_INET, socket.SOCK_STREAM) - __lock.bind( - "\0_{name}__lock".format(name=str(name).lower().strip()) - if sys.platform.startswith("linux") else ("127.0.0.1", port)) - except socket.error as e: - log.warning(e) - else: - log.info("Socket Lock for Single Instance: {}.".format(__lock)) - else: # if multiple instance want to touch same file bad things can happen - log.warning("Multiple instance on same file can cause Race Condition.") - return __lock - - -def make_logger(name=str(os.getpid())): - """Build and return a Logging Logger.""" - if not sys.platform.startswith("win") and sys.stderr.isatty(): - def add_color_emit_ansi(fn): - """Add methods we need to the class.""" - def new(*args): - """Method overload.""" - if len(args) == 2: - new_args = (args[0], copy(args[1])) - else: - new_args = (args[0], copy(args[1]), args[2:]) - if hasattr(args[0], 'baseFilename'): - return fn(*args) - levelno = new_args[1].levelno - if levelno >= 50: - color = '\x1b[31;5;7m\n ' # blinking red with black - elif levelno >= 40: - color = '\x1b[31m' # red - elif levelno >= 30: - color = '\x1b[33m' # yellow - elif levelno >= 20: - color = '\x1b[32m' # green - elif levelno >= 10: - color = '\x1b[35m' # pink - else: - color = '\x1b[0m' # normal - try: - new_args[1].msg = color + str(new_args[1].msg) + ' \x1b[0m' - except Exception as reason: - print(reason) # Do not use log here. - return fn(*new_args) - return new - # all non-Windows platforms support ANSI Colors so we use them - log.StreamHandler.emit = add_color_emit_ansi(log.StreamHandler.emit) - else: - log.debug("Colored Logs not supported on {0}.".format(sys.platform)) - log_file = os.path.join(gettempdir(), str(name).lower().strip() + ".log") - log.basicConfig(level=-1, filemode="w", filename=log_file, - format="%(levelname)s:%(asctime)s %(message)s %(lineno)s") - log.getLogger().addHandler(log.StreamHandler(sys.stderr)) - adrs = "/dev/log" if sys.platform.startswith("lin") else "/var/run/syslog" - try: - handler = log.handlers.SysLogHandler(address=adrs) - except: - log.debug("Unix SysLog Server not found,ignored Logging to SysLog.") - else: - log.addHandler(handler) - log.debug("Logger created with Log file at: {0}.".format(log_file)) - return log - - -def make_post_execution_message(app=__doc__.splitlines()[0].strip()): - """Simple Post-Execution Message with information about RAM and Time. - - >>> make_post_execution_message() >= 0 - True - """ - ram_use = int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * - resource.getpagesize() / 1024 / 1024 if resource else 0) - log.info("Total Maximum RAM Memory used: ~{0} MegaBytes.".format(ram_use)) - log.info("Total Working Time: {0}.".format(datetime.now() - start_time)) - if randint(0, 100) < 25: # ~25% chance to see the message,dont get on logs - print("Thanks for using this App,share your experience!{0}".format(""" - Twitter: https://twitter.com/home?status=I%20Like%20{n}!:%20{u} - Facebook: https://www.facebook.com/share.php?u={u}&t=I%20Like%20{n} - G+: https://plus.google.com/share?url={u}""".format(u=__url__, n=app))) - return ram_use + with open(html_file_path, "w", encoding="utf-8") as output_file: + output_file.write(pretty_html) + return pretty_html def make_arguments_parser(): @@ -638,8 +455,6 @@ def make_arguments_parser(): help="Add a Time Stamp on all CSS/SCSS output files.") parser.add_argument('--quiet', action='store_true', help="Quiet, Silent, force disable all Logging.") - parser.add_argument('--checkupdates', action='store_true', - help="Check for Updates from Internet while running.") parser.add_argument('--after', type=str, help="Command to execute after run (Experimental).") parser.add_argument('--before', type=str, @@ -650,68 +465,47 @@ def make_arguments_parser(): help="Group Alphabetically CSS Poperties by name.") parser.add_argument('--justify', action='store_true', help="Right Justify CSS Properties (Experimental).") + parser.add_argument('--extraline', action='store_true', + help="Add 1 New Line for each New Line (Experimental)") global args args = parser.parse_args() - - -def only_on_py3(boolean_argument=True): - """Deprecate features if not using Python >= 3, to motivate migration.""" - if isinstance(boolean_argument, (tuple, list)): # argument is iterable. - boolean_argument = all(boolean_argument) - else: # argument is boolean, or evaluate as boolean, even if is not. - boolean_argument = bool(boolean_argument) - if sys.version_info.major >= 3: - return boolean_argument # good to go - else: # Migrate to python 3, is free and easy, get a virtualenv at least. - log.critical("Feature only available on Python 3, feature ignored !.") - log.debug("Please Migrate to Python 3 for better User Experience...") - return False + return args def main(): """Main Loop.""" make_arguments_parser() - make_logger("css-html-prettify") - make_root_check_and_encoding_debug() - set_process_name_and_cpu_priority("css-html-prettify") - set_single_instance("css-html-prettify") - if only_on_py3(args.checkupdates): - check_for_updates() - if only_on_py3(args.quiet): - log.disable(log.CRITICAL) - log.info(__doc__ + __version__) - if only_on_py3((args.before, getoutput)): - log.info(getoutput(str(args.before))) - # Work based on if argument is file or folder, folder is slower. - if os.path.isfile(args.fullpath - ) and args.fullpath.endswith((".css", ".scss")): - log.info("Target is a CSS / SCSS File.") + global log + print(__doc__ + __version__) + if args.before and getoutput: + print(getoutput(str(args.before))) + if os.path.isfile(args.fullpath) and args.fullpath.endswith( + (".css", ".scss")): # Work based on if argument is file or folder. + print("Target is a CSS / SCSS File.") list_of_files = str(args.fullpath) process_single_css_file(args.fullpath) elif os.path.isfile(args.fullpath ) and args.fullpath.endswith((".htm", ".html")): - log.info("Target is a HTML File.") + print("Target is a HTML File.") list_of_files = str(args.fullpath) process_single_html_file(args.fullpath) elif os.path.isdir(args.fullpath): - log.info("Target is a Folder with CSS / SCSS, HTML, JS.") - log.warning("Processing a whole Folder may take some time...") - list_of_files = walkdir_to_filelist( + print("Target is a Folder with CSS / SCSS, HTML, JS.") + print("Processing a whole Folder may take some time...") + list_of_files = walk2list( args.fullpath, (".css", ".scss", ".html", ".htm"), ".min.css") pool = Pool(cpu_count()) # Multiprocessing Async pool.map_async(process_multiple_files, list_of_files) pool.close() pool.join() else: - log.critical("File or folder not found,or cant be read,or I/O Error.") + print("File or folder not found,or cant be read,or I/O Error.") sys.exit(1) - if only_on_py3((args.after, getoutput)): - log.info(getoutput(str(args.after))) - log.info('-' * 80) - log.info('Files Processed: {}.'.format(list_of_files)) - log.info('Number of Files Processed: {}'.format( - len(list_of_files) if isinstance(list_of_files, tuple) else 1)) - make_post_execution_message() + if args.after and getoutput: + print(getoutput(str(args.after))) + print(f'\n {"-" * 80} \n Files Processed: {list_of_files}.') + print(f'''Number of Files Processed: + {len(list_of_files) if isinstance(list_of_files, tuple) else 1}''') if __name__ in '__main__': diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..5d116e2 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,81 @@ +# See: https://setuptools.readthedocs.io/en/latest/setuptools.html#metadata +[metadata] +name = css-html-prettify +description = StandAlone Async cross-platform Prettifier Beautifier for the Web. +url = https://github.com/juancarlospaco/css-html-prettify#css-html-prettify +download_url = https://github.com/juancarlospaco/css-html-prettify#css-html-prettify +author = Juan Carlos +author_email = juancarlospaco@gmail.com +maintainer = Juan Carlos +maintainer_email = juancarlospaco@gmail.com +keywords = python3, argentina, CSS, HTML, Prettify, CSS3, HTML5, Web, Beautify +license = GPL-3 LGPL-3 +platforms = Linux, Darwin, Windows +version = 2.5.5 +project_urls = + Docs = https://github.com/juancarlospaco/css-html-prettify/README.md + Bugs = https://github.com/juancarlospaco/css-html-prettify/issues + +# license_file = LICENSE +long_description = file: README.md +long_description_content_type = text/markdown +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Other Environment + Intended Audience :: Developers + Intended Audience :: Other Audience + Natural Language :: English + License :: OSI Approved :: GNU General Public License (GPL) + License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) + License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Operating System :: OS Independent + Operating System :: POSIX :: Linux + Operating System :: Microsoft :: Windows + Operating System :: MacOS :: MacOS X + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: Implementation :: CPython + Topic :: Software Development + + +[options] +zip_safe = True +include_package_data = True +python_requires = >=3.6 +tests_require = isort ; prospector ; pre-commit ; pre-commit-hooks +install_requires = beautifulsoup4 +setup_requires = beautifulsoup4 +packages = find: + +[bdist_wheel] +universal = 1 +python-tag = py36 + +[install_lib] +compile = 0 +optimize = 2 + +[bdist_egg] +exclude-source-files = true + +# [options.package_data] +# * = *.pxd, *.pyx, *.json, *.txt + +# [options.exclude_package_data] +# ;* = *.c, *.so, *.js + +# [options.entry_points] +# console_scripts = +# foo = my_package.some_module:main_func +# bar = other_module:some_func +# gui_scripts = +# baz = my_package_gui:start_func + +# [options.packages.find] +# where = . +# include = *.py, *.pyw +# exclude = *.c, *.so, *.js, *.tests, *.tests.*, tests.*, tests diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..34546f6 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# +# To generate DEB package from Python Package: +# sudo pip3 install stdeb +# python3 setup.py --verbose --command-packages=stdeb.command bdist_deb +# +# +# To generate RPM package from Python Package: +# sudo apt-get install rpm +# python3 setup.py bdist_rpm --verbose --fix-python --binary-only +# +# +# To generate EXE MS Windows from Python Package (from MS Windows only): +# python3 setup.py bdist_wininst --verbose +# +# +# To generate PKGBUILD ArchLinux from Python Package (from PyPI only): +# sudo pip3 install git+https://github.com/bluepeppers/pip2arch.git +# pip2arch.py PackageNameHere +# +# +# To Upload to PyPI by executing: +# python3 setup.py register +# python3 setup.py bdist_egg sdist --formats=zip upload --sign + + +from setuptools import setup, Command +from zipapp import create_archive + + +class ZipApp(Command): + description, user_options = "Creates a zipapp.", [] + + def initialize_options(self): pass # Dont needed, but required. + + def finalize_options(self): pass # Dont needed, but required. + + def run(self): + return create_archive("css-html-prettify.py", "css-html-prettify.pyz", + "/usr/bin/env python3") + + +setup( + requires=['anglerfish', 'beautifulsoup4'], + scripts=["css-html-prettify.py"], + cmdclass={"zipapp": ZipApp}, +)