123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- # -*- 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))
|