From f35a7b0e70032de2feec9f3bda09da44cf0e1073 Mon Sep 17 00:00:00 2001 From: srv Date: Mon, 28 Apr 2025 17:11:28 -0500 Subject: first commit --- plugins/compressor/minify.py | 266 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 plugins/compressor/minify.py (limited to 'plugins/compressor/minify.py') diff --git a/plugins/compressor/minify.py b/plugins/compressor/minify.py new file mode 100644 index 0000000..9107882 --- /dev/null +++ b/plugins/compressor/minify.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- + +"""CSS-JS-Minify. + +Minifier for the Web. +""" + +import os +import sys +import gzip +# import logging as log + +from argparse import ArgumentParser +from datetime import datetime +from functools import partial +from hashlib import sha1 +from multiprocessing import Pool, cpu_count +from subprocess import getoutput +from time import sleep + +from .css_minifer import css_minify +from .js_minifer import js_minify + + +__all__ = ('process_multiple_files', 'prefixer_extensioner', + 'process_single_css_file', 'process_single_js_file', + 'make_arguments_parser', 'main') + +color = { + 'cyan': '\033[1;36m', + 'end': '\033[0m', + 'green': '\033[1;32m' +} + + +def process_multiple_files(file_path, watch=False, wrap=False, timestamp=False, + comments=False, sort=False, overwrite=False, + zipy=False, prefix='', add_hash=False): + """Process multiple CSS, JS files with multiprocessing.""" + print("Process %s is Compressing %s" % (os.getpid(), file_path)) + if watch: + previous = int(os.stat(file_path).st_mtime) + print("Process %s is Watching %s" % (os.getpid(), file_path)) + while True: + actual = int(os.stat(file_path).st_mtime) + if previous == actual: + sleep(60) + else: + previous = actual + print("Modification detected on %s" % file_path) + if file_path.endswith(".css"): + process_single_css_file( + file_path, wrap=wrap, timestamp=timestamp, + comments=comments, sort=sort, overwrite=overwrite, + zipy=zipy, prefix=prefix, add_hash=add_hash) + elif file_path.endswith(".js"): + process_single_js_file( + file_path, timestamp=timestamp, + overwrite=overwrite, zipy=zipy) + else: + if file_path.endswith(".css"): + process_single_css_file( + file_path, wrap=wrap, timestamp=timestamp, + comments=comments, sort=sort, overwrite=overwrite, zipy=zipy, + prefix=prefix, add_hash=add_hash) + elif file_path.endswith(".js"): + process_single_js_file( + file_path, timestamp=timestamp, + overwrite=overwrite, zipy=zipy) + + +def prefixer_extensioner(file_path, old, new, + file_content=None, prefix='', add_hash=False): + """Take a file path and safely preppend 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!. + >>> prefixer_extensioner('/tmp/test.js', '.js', '.min.js') + '/tmp/test.min.js' + """ + print("Prepending '%s' Prefix to %s" % (new.upper(), file_path)) + + extension = os.path.splitext(file_path)[1].lower().replace(old, new) + filenames = os.path.splitext(os.path.basename(file_path))[0] + filenames = prefix + filenames if prefix else filenames + if add_hash and file_content: # http://stackoverflow.com/a/25568916 + filenames += "-" + sha1(file_content.encode("utf-8")).hexdigest()[:11] + print("Appending SHA1 HEX-Digest Hash to '%s'" % file_path) + dir_names = os.path.dirname(file_path) + file_path = os.path.join(dir_names, filenames + extension) + return file_path + + +def process_single_css_file(css_file_path, wrap=False, timestamp=False, + comments=False, sort=False, overwrite=False, + zipy=False, prefix='', add_hash=False, + output_path=None): + """Process a single CSS file.""" + print("Processing %sCSS%s file: %s" % (color['cyan'], + color['end'], + css_file_path)) + with open(css_file_path, encoding="utf-8") as css_file: + original_css = css_file.read() + + print("INPUT: Reading CSS file %s" % css_file_path) + + minified_css = css_minify(original_css, wrap=wrap, + comments=comments, sort=sort) + if timestamp: + taim = "/* {0} */ ".format(datetime.now().isoformat()[:-7].lower()) + minified_css = taim + minified_css + if output_path is None: + min_css_file_path = prefixer_extensioner( + css_file_path, ".css", ".css" if overwrite else ".min.css", + original_css, prefix=prefix, add_hash=add_hash) + if zipy: + gz_file_path = prefixer_extensioner( + css_file_path, ".css", + ".css.gz" if overwrite else ".min.css.gz", original_css, + prefix=prefix, add_hash=add_hash) + print("OUTPUT: Writing ZIP CSS %s" % gz_file_path) + else: + min_css_file_path = gz_file_path = output_path + if not zipy or output_path is None: + # if specific output path is requested,write write only one output file + with open(min_css_file_path, "w", encoding="utf-8") as output_file: + output_file.write(minified_css) + if zipy: + with gzip.open(gz_file_path, "wt", encoding="utf-8") as output_gz: + output_gz.write(minified_css) + + print("OUTPUT: Writing CSS Minified %s" % min_css_file_path) + + return min_css_file_path + + +def process_single_js_file(js_file_path, timestamp=False, overwrite=False, + zipy=False, output_path=None): + """Process a single JS file.""" + print("Processing %sJS%s file: %s" % (color['green'], + color['end'], + js_file_path)) + with open(js_file_path, encoding="utf-8") as js_file: + original_js = js_file.read() + print("INPUT: Reading JS file %s" % js_file_path) + minified_js = js_minify(original_js) + if timestamp: + taim = "/* {} */ ".format(datetime.now().isoformat()[:-7].lower()) + minified_js = taim + minified_js + if output_path is None: + min_js_file_path = prefixer_extensioner( + js_file_path, ".js", ".js" if overwrite else ".min.js", + original_js) + if zipy: + gz_file_path = prefixer_extensioner( + js_file_path, ".js", ".js.gz" if overwrite else ".min.js.gz", + original_js) + print("OUTPUT: Writing ZIP JS %s" % gz_file_path) + else: + min_js_file_path = gz_file_path = output_path + if not zipy or output_path is None: + # if specific output path is requested,write write only one output file + with open(min_js_file_path, "w", encoding="utf-8") as output_file: + output_file.write(minified_js) + if zipy: + with gzip.open(gz_file_path, "wt", encoding="utf-8") as output_gz: + output_gz.write(minified_js) + print("OUTPUT: Writing JS Minified %s" % min_js_file_path) + return min_js_file_path + + +def make_arguments_parser(): + """Build and return a command line agument parser.""" + parser = ArgumentParser(description=__doc__, epilog="""CSS-JS-Minify: + Takes a file or folder full path string and process all CSS/JS found. + If argument is not file/folder will fail. Check Updates works on Python3. + Std-In to Std-Out is deprecated since it may fail with unicode characters. + SHA1 HEX-Digest 11 Chars Hash on Filenames is used for Server Cache. + CSS Properties are Alpha-Sorted, to help spot cloned ones, Selectors not. + Watch works for whole folders, with minimum of ~60 Secs between runs.""") + # parser.add_argument('--version', action='version', + # version=css_js_minify.__version__) + parser.add_argument('fullpath', metavar='fullpath', type=str, + help='Full path to local file or folder.') + parser.add_argument('--wrap', action='store_true', + help="Wrap output to ~80 chars per line, CSS only.") + parser.add_argument('--prefix', type=str, + help="Prefix string to prepend on output filenames.") + parser.add_argument('--timestamp', action='store_true', + help="Add a Time Stamp on all CSS/JS output files.") + parser.add_argument('--quiet', action='store_true', help="Quiet, Silent.") + parser.add_argument('--hash', action='store_true', + help="Add SHA1 HEX-Digest 11chars Hash to Filenames.") + parser.add_argument('--zipy', action='store_true', + help="GZIP Minified files as '*.gz', CSS/JS only.") + parser.add_argument('--sort', action='store_true', + help="Alphabetically Sort CSS Properties, CSS only.") + parser.add_argument('--comments', action='store_true', + help="Keep comments, CSS only (Not Recommended)") + parser.add_argument('--overwrite', action='store_true', + help="Force overwrite all in-place (Not Recommended)") + parser.add_argument('--after', type=str, + help="Command to execute after run (Experimental).") + parser.add_argument('--before', type=str, + help="Command to execute before run (Experimental).") + parser.add_argument('--watch', action='store_true', help="Watch changes.") + parser.add_argument('--multiple', action='store_true', + help="Allow Multiple instances (Not Recommended).") + return parser.parse_args() + + +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 main(): + """Main Loop.""" + args = make_arguments_parser() + if os.path.isfile(args.fullpath) and args.fullpath.endswith(".css"): + print("Target is a CSS File.") # Work based on if argument is + list_of_files = str(args.fullpath) # file or folder, folder is slower. + process_single_css_file( + args.fullpath, wrap=args.wrap, timestamp=args.timestamp, + comments=args.comments, sort=args.sort, overwrite=args.overwrite, + zipy=args.zipy, prefix=args.prefix, add_hash=args.hash) + elif os.path.isfile(args.fullpath) and args.fullpath.endswith(".js"): + print("Target is a JS File.") + list_of_files = str(args.fullpath) + process_single_js_file( + args.fullpath, timestamp=args.timestamp, + overwrite=args.overwrite, zipy=args.zipy) + elif os.path.isdir(args.fullpath): + print("Target is a Folder with CSS, JS files !.") + print("Processing a whole Folder may take some time...") + list_of_files = walk2list( + args.fullpath, + (".css", ".js" if args.overwrite else None), + (".min.css", ".min.js" if args.overwrite else None)) + print("Total Maximum CPUs used: ~%s Cores." % cpu_count()) + pool = Pool(cpu_count()) # Multiprocessing Async + pool.map_async(partial( + process_multiple_files, watch=args.watch, + wrap=args.wrap, timestamp=args.timestamp, + comments=args.comments, sort=args.sort, + overwrite=args.overwrite, zipy=args.zipy, + prefix=args.prefix, add_hash=args.hash), + list_of_files) + pool.close() + pool.join() + else: + print("File or folder not found,or cant be read,or I/O Error.") + sys.exit(1) + if args.after and getoutput: + print(getoutput(str(args.after))) + print("\n %s \n Files Processed: %s" % ("-" * 80, list_of_files)) + print("Number of Files Processed: %s" % + (len(list_of_files) if isinstance(list_of_files, tuple) else 1)) -- cgit v1.2.3