1
0
mirror of https://github.com/s00500/ESPUI.git synced 2025-07-04 06:10:18 +00:00

2 Commits

View File

@ -1,15 +1,42 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from jsmin import jsmin as jsminify ### import always available modules
from htmlmin import minify as htmlminify
from csscompressor import compress as cssminify
import gzip
import sys import sys
import os.path import os.path
import argparse import argparse
import re import re
from glob import glob from glob import glob
### import not always installed modules
missing = []
try:
from jsmin import jsmin as jsminify
except ModuleNotFoundError as e:
missing.append(e)
try:
from htmlmin import minify as htmlminify
except ModuleNotFoundError as e:
missing.append(e)
try:
from csscompressor import compress as cssminify
except ModuleNotFoundError as e:
missing.append(e)
try:
import gzip
except ModuleNotFoundError as e:
missing.append(e)
if len(missing) > 0:
# ERROR: at least one module is missing
for m in missing:
print("Cannot find module '%s'." % m.name)
print("Can't find %s required python module%s. Please install %s. If you're not sure how, a web search" % (len(missing), "s" if len(missing) > 1 else "", "them" if len(missing) > 1 else "it"))
print("for 'python', your operating system/python distribution and 'install modules' should help.")
print("For most people on unix-y systems, this line should work (possibly w/o the '3'):\n pip3 install %s" % " ".join(m.name for m in missing))
print("Here's the long documentation: https://packaging.python.org/tutorials/installing-packages/")
sys.exit(0)
### String template for C header files
TARGET_TEMPLATE = '''const char {constant}[] PROGMEM = R"=====( TARGET_TEMPLATE = '''const char {constant}[] PROGMEM = R"=====(
{minidata} {minidata}
)====="; )=====";
@ -17,7 +44,9 @@ TARGET_TEMPLATE = '''const char {constant}[] PROGMEM = R"=====(
const uint8_t {constant}_GZIP[{gziplen}] PROGMEM = {{ {gzipdata} }}; const uint8_t {constant}_GZIP[{gziplen}] PROGMEM = {{ {gzipdata} }};
''' '''
def parse_arguments(args=None): def parse_arguments(args=None):
""" Command line argument parser definitions """
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Prepares ESPUI header files by minifying and gzipping HTML, JS and CSS source files.") description="Prepares ESPUI header files by minifying and gzipping HTML, JS and CSS source files.")
parser.add_argument("--auto", "--all", "-a", dest="auto", action="store_true", parser.add_argument("--auto", "--all", "-a", dest="auto", action="store_true",
@ -36,6 +65,20 @@ def parse_arguments(args=None):
return args return args
def get_context(infile, outfile): def get_context(infile, outfile):
""" This function creates a 'context object': a dictionary containing all the data needed.
Dictionary members:
infile: Full path to the input file or directory as given or autodetected
indir: Full path to the infile parent.
dir: Full path to the input directory tree (indir or parent of indir).
name: Filename of infile excluding extension (i.e. filename up until the first dot)
type: Lowercase extension of infile; one of: html, js, css.
outfile: Full path to the output file or directory as given or autodetected.
outdir: Full path to output directory.
outfilename: Filename of outfile.
minifile: Full path and filename of the intermediary minified file.
constant: C header file constant name derived from infile.
If infile == minifile, the input file already is minified (contains ".min.")
"""
infile = os.path.realpath(infile) infile = os.path.realpath(infile)
dir, name, type = (os.path.basename(os.path.dirname(infile)), os.path.basename(infile).split(os.path.extsep)[0], os.path.basename(infile).split(os.path.extsep)[-1] ) dir, name, type = (os.path.basename(os.path.dirname(infile)), os.path.basename(infile).split(os.path.extsep)[0], os.path.basename(infile).split(os.path.extsep)[-1] )
type = type.strip(".").lower() type = type.strip(".").lower()
@ -58,6 +101,11 @@ def get_context(infile, outfile):
return locals() return locals()
def perform_gzip(c): def perform_gzip(c):
""" Performs GZIP on c['minidata'].
The returned context object will contain additional fields:
gzipdata: Comma-separated string of decimal byte values representing gzipped data.
gziplen: Count of decimal byte values in gzipdata.
"""
compressed = gzip.compress(bytes(c['minidata'], 'utf-8')) compressed = gzip.compress(bytes(c['minidata'], 'utf-8'))
c['gzipdata'] = ','.join([ str(b) for b in compressed ]) c['gzipdata'] = ','.join([ str(b) for b in compressed ])
c['gziplen'] = len(compressed) c['gziplen'] = len(compressed)
@ -65,6 +113,9 @@ def perform_gzip(c):
return c return c
def perform_minify(c): def perform_minify(c):
""" Performs minification on c['infile'].
The returned context object contains the additional field minidata: A string of minified file contents.
"""
with open(c['infile']) as infile: with open(c['infile']) as infile:
minifier = { minifier = {
'css': cssminify, 'css': cssminify,
@ -73,19 +124,27 @@ def perform_minify(c):
}.get(c['type']) or htmlminify }.get(c['type']) or htmlminify
print(" Using %s minifier" % c['type']) print(" Using %s minifier" % c['type'])
c['minidata'] = minifier(infile.read()) c['minidata'] = minifier(infile.read())
return perform_gzip(c) return c
def process_file(infile, outdir, storemini=True): def process_file(infile, outdir, storemini=True):
""" Processes one file """
print("Processing file %s" % infile) print("Processing file %s" % infile)
# Evaluate file and target context
c = get_context(infile, outdir) c = get_context(infile, outdir)
# Minify file data
c = perform_minify(c) c = perform_minify(c)
# Gzip minified data
c = perform_gzip(c)
if storemini: if storemini:
# Write intermediary minified file
if c['infile'] == c['minifile']: if c['infile'] == c['minifile']:
print(" Original file is already minified, refusing to overwrite it") print(" Original file is already minified, refusing to overwrite it")
else: else:
print(" Writing minified file %s" % c['minifile']) print(" Writing minified file %s" % c['minifile'])
with open(c['minifile'], 'w+') as minifile: with open(c['minifile'], 'w+') as minifile:
minifile.write(c['minidata']) minifile.write(c['minidata'])
# Write minified and gzipped data to C header file
with open(c['outfile'], 'w+') as outfile: with open(c['outfile'], 'w+') as outfile:
print(" Using C constant names %s and %s_GZIP" % (c['constant'], c['constant'])) print(" Using C constant names %s and %s_GZIP" % (c['constant'], c['constant']))
print(" Writing C header file %s" % c['outfile']) print(" Writing C header file %s" % c['outfile'])
@ -95,6 +154,10 @@ def filenamefilter(pattern, strings):
return filter(re.compile(pattern).search, strings) return filter(re.compile(pattern).search, strings)
def process_dir(sourcedir, outdir, recursive=True, storemini=True): def process_dir(sourcedir, outdir, recursive=True, storemini=True):
""" Processes a directory tree, recursively. Calls process_file on each HTML/CSS/JS file found.
Skips intermediary minified files. Standalone minified files (i.e. files containing ".min." that
do not have a full version) are processed without minifying again.
"""
pattern = r'/*\.(css|js|htm|html)$' pattern = r'/*\.(css|js|htm|html)$'
files = glob(sourcedir + "/**/*", recursive=True)+glob(sourcedir + "/*") if recursive else glob(sourcedir + "/*") files = glob(sourcedir + "/**/*", recursive=True)+glob(sourcedir + "/*") if recursive else glob(sourcedir + "/*")
files = filenamefilter(pattern, files) files = filenamefilter(pattern, files)
@ -105,6 +168,7 @@ def process_dir(sourcedir, outdir, recursive=True, storemini=True):
process_file(f, outdir, storemini) process_file(f, outdir, storemini)
def check_args(args): def check_args(args):
""" Checks argumental sanity and exits if the arguments are insane. """
abort = 0 abort = 0
if not os.path.exists(args.sources): if not os.path.exists(args.sources):
print("ERROR: Source %s does not exist" % args.sources) print("ERROR: Source %s does not exist" % args.sources)
@ -120,8 +184,12 @@ def check_args(args):
sys.exit(abort) sys.exit(abort)
def main(args): def main(args):
""" main entry point. """
# default source if not given: realpath(../examples/gui/data)
args.sources = os.path.realpath(args.sources or os.sep.join((os.path.dirname(os.path.realpath(__file__)), "..", "examples", "gui", "data"))) args.sources = os.path.realpath(args.sources or os.sep.join((os.path.dirname(os.path.realpath(__file__)), "..", "examples", "gui", "data")))
# default target if not given: realpath(../src)
args.target = os.path.realpath(args.target or os.sep.join((os.path.dirname(os.path.realpath(__file__)), "..", "src"))) args.target = os.path.realpath(args.target or os.sep.join((os.path.dirname(os.path.realpath(__file__)), "..", "src")))
# check arguments
check_args(args) check_args(args)
if os.path.isfile(args.sources): if os.path.isfile(args.sources):
print("Source %s is a file, will process one file only." % args.sources) print("Source %s is a file, will process one file only." % args.sources)