Skip to content

Commit c32b847

Browse files
Add AlphaSort CSS Properties
1 parent 6e619bb commit c32b847

File tree

1 file changed

+187
-20
lines changed

1 file changed

+187
-20
lines changed

css-html-js-minify.py

Lines changed: 187 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111

1212
import gzip
13+
import itertools
1314
import logging as log
1415
import os
1516
import re
@@ -104,12 +105,172 @@
104105
'turquoise': (64, 224, 208), 'violet': (238, 130, 238),
105106
'wheat': (245, 222, 179)
106107
}
108+
CSS_PROPS_TEXT = '''
109+
alignment-adjust alignment-baseline animation animation-delay
110+
animation-direction animation-duration animation-iteration-count
111+
animation-name animation-play-state animation-timing-function appearance
112+
azimuth
113+
114+
backface-visibility background background-attachment background-clip
115+
background-color background-image background-origin background-position
116+
background-repeat background-size baseline-shift bikeshedding bookmark-label
117+
bookmark-level bookmark-state bookmark-target border border-bottom
118+
border-bottom-color border-bottom-left-radius border-bottom-right-radius
119+
border-bottom-style border-bottom-width border-collapse border-color
120+
border-image border-image-outset border-image-repeat border-image-slice
121+
border-image-source border-image-width border-left border-left-color
122+
border-left-style border-left-width border-radius border-right
123+
border-right-color border-right-style border-right-width border-spacing
124+
border-style border-top border-top-color border-top-left-radius
125+
border-top-right-radius border-top-style border-top-width border-width bottom
126+
box-decoration-break box-shadow box-sizing
127+
128+
caption-side clear clip color column-count column-fill column-gap column-rule
129+
column-rule-color column-rule-style column-rule-width column-span column-width
130+
columns content counter-increment counter-reset cue cue-after cue-before
131+
cursor
132+
133+
direction display drop-initial-after-adjust drop-initial-after-align
134+
drop-initial-before-adjust drop-initial-before-align drop-initial-size
135+
drop-initial-value
136+
137+
elevation empty-cells
138+
139+
fit fit-position float font font-family font-size font-size-adjust
140+
font-stretch font-style font-variant font-weight
141+
142+
grid-columns grid-rows
143+
144+
hanging-punctuation height hyphenate-character hyphenate-resource hyphens
145+
146+
icon image-orientation image-resolution inline-box-align
147+
148+
left letter-spacing line-height line-stacking line-stacking-ruby
149+
line-stacking-shift line-stacking-strategy linear-gradient list-style
150+
list-style-image list-style-position list-style-type
151+
152+
margin margin-bottom margin-left margin-right margin-top marquee-direction
153+
marquee-loop marquee-speed marquee-style max-height max-width min-height
154+
min-width
155+
156+
nav-index
157+
158+
opacity orphans outline outline-color outline-offset outline-style
159+
outline-width overflow overflow-style overflow-x overflow-y
160+
161+
padding padding-bottom padding-left padding-right padding-top page
162+
page-break-after page-break-before page-break-inside pause pause-after
163+
pause-before perspective perspective-origin pitch pitch-range play-during
164+
position presentation-level
165+
166+
quotes
167+
168+
resize rest rest-after rest-before richness right rotation rotation-point
169+
ruby-align ruby-overhang ruby-position ruby-span
170+
171+
size speak speak-header speak-numeral speak-punctuation speech-rate src
172+
stress string-set
173+
174+
table-layout target target-name target-new target-position text-align
175+
text-align-last text-decoration text-emphasis text-indent text-justify
176+
text-outline text-shadow text-transform text-wrap top transform
177+
transform-origin transition transition-delay transition-duration
178+
transition-property transition-timing-function
179+
180+
unicode-bidi unicode-range
181+
182+
vertical-align visibility voice-balance voice-duration voice-family
183+
voice-pitch voice-range voice-rate voice-stress voice-volume volume
184+
185+
white-space widows width word-break word-spacing word-wrap
186+
187+
z-index
188+
'''
107189

108190

109191
###############################################################################
110192
# CSS minify
111193

112194

195+
def _compile_props(props_text):
196+
"""Take a list of props and prepare them."""
197+
props = []
198+
for line_of_props in props_text.strip().lower().splitlines():
199+
props += line_of_props.split(" ")
200+
props = filter(lambda line: not line.startswith('#'), props)
201+
final_props, groups, g_id = [], [], 0
202+
for prop in props:
203+
if prop.strip():
204+
final_props.append(prop)
205+
groups.append(g_id)
206+
else:
207+
g_id += 1
208+
return final_props, groups
209+
210+
211+
def _prioritify(line_buffer, pgs):
212+
"""Return args priority, priority is integer and smaller means higher."""
213+
props, groups = pgs
214+
priority, group = 9999, 0
215+
for css_property in props:
216+
if line_buffer.find(css_property + ':') != -1:
217+
priority = props.index(css_property)
218+
group = groups[priority]
219+
break
220+
return priority, group
221+
222+
223+
def _props_grouper(props, pgs):
224+
"""Return groups for properties."""
225+
if not props:
226+
return props
227+
props = [_
228+
if _.strip().endswith(";") else _.rstrip() + ";\n" for _ in props]
229+
props_pg = zip(map(lambda prop: _prioritify(prop, pgs), props), props)
230+
props_pg = sorted(props_pg, key=lambda item: item[0][1])
231+
props_by_groups = map(
232+
lambda item: list(item[1]),
233+
itertools.groupby(props_pg, key=lambda item: item[0][1]))
234+
props_by_groups = map(lambda item: sorted(
235+
item, key=lambda item: item[0][0]), props_by_groups)
236+
props = []
237+
for group in props_by_groups:
238+
group = map(lambda item: item[1], group)
239+
props += group
240+
props += ['\n']
241+
props.pop()
242+
return props
243+
244+
245+
def sort_properties(css_unsorted_string):
246+
"""CSS Property Sorter Function.
247+
248+
This function will read buffer argument, split it to a list by lines,
249+
sort it by defined rule, and return sorted buffer if it's CSS property.
250+
This function depends on '_prioritify' function.
251+
"""
252+
log.debug("Alphabetically Sorting and Grouping all CSS Properties.")
253+
css_pgs = _compile_props(CSS_PROPS_TEXT)
254+
pattern = re.compile(r'(.*?{\r?\n?)(.*?)(}.*?)|(.*)',
255+
re.DOTALL + re.MULTILINE)
256+
matched_patterns = pattern.findall(css_unsorted_string)
257+
sorted_patterns, sorted_buffer = [], css_unsorted_string
258+
RE_prop = re.compile(r'((?:.*?)(?:;)(?:.*?\n)|(?:.*))',
259+
re.DOTALL + re.MULTILINE)
260+
if len(matched_patterns) != 0:
261+
for matched_groups in matched_patterns:
262+
sorted_patterns += matched_groups[0].splitlines(True)
263+
props = map(lambda line: line.lstrip('\n'),
264+
RE_prop.findall(matched_groups[1]))
265+
props = list(filter(lambda line: line.strip('\n '), props))
266+
props = _props_grouper(props, css_pgs)
267+
sorted_patterns += props
268+
sorted_patterns += matched_groups[2].splitlines(True)
269+
sorted_patterns += matched_groups[3].splitlines(True)
270+
sorted_buffer = ''.join(sorted_patterns)
271+
return sorted_buffer
272+
273+
113274
def remove_comments(css):
114275
"""Remove all CSS comment blocks."""
115276
log.debug("Removing all Comments.")
@@ -257,7 +418,7 @@ def condense_semicolons(css):
257418
return re.sub(r";;+", ";", css)
258419

259420

260-
def wrap_css_lines(css, line_length):
421+
def wrap_css_lines(css, line_length=80):
261422
"""Wrap the lines of the given CSS to an approximate length."""
262423
log.debug("Wrapping lines to ~{} max line lenght.".format(line_length))
263424
lines, line_start = [], 0
@@ -328,10 +489,11 @@ def unquote_selectors(css):
328489
return re.compile('([a-zA-Z]+)="([a-zA-Z0-9-_\.]+)"]').sub(r'\1=\2]', css)
329490

330491

331-
def cssmin(css, wrap=None, comments=False):
492+
def css_minify(css, wrap=False, comments=False):
332493
"""Minify CSS main function."""
333494
log.info("Compressing CSS...")
334495
css = remove_comments(css) if not comments else css
496+
css = sort_properties(css)
335497
css = unquote_selectors(css)
336498
css = condense_whitespace(css)
337499
css = remove_url_quotes(css)
@@ -346,7 +508,7 @@ def cssmin(css, wrap=None, comments=False):
346508
css = normalize_rgb_colors_to_hex(css)
347509
css = condense_hex_colors(css)
348510
css = condense_border_none(css)
349-
css = wrap_css_lines(css, wrap) if wrap is not None else css
511+
css = wrap_css_lines(css, 80) if wrap else css
350512
css = condense_semicolons(css)
351513
css = add_encoding(css)
352514
css = restore_needed_space(css)
@@ -463,10 +625,10 @@ def unquote_html_attributes(html):
463625
return html.strip()
464626

465627

466-
def htmlmin(html, comments=False):
628+
def html_minify(html, comments=False):
467629
"""Minify HTML main function.
468630
469-
>>> htmlmin(' <p width="9" height="5" > <!-- a --> b </p> c <br> ')
631+
>>> html_minify(' <p width="9" height="5" > <!-- a --> b </p> c <br> ')
470632
'<p width=9 height=5 > b c <br>'
471633
"""
472634
log.info("Compressing HTML...")
@@ -491,7 +653,7 @@ def simple_replacer_js(js):
491653
";}", "}").replace("; ", ";").replace(" ;", ";").rstrip("\n;"))
492654

493655

494-
def jsmin(js):
656+
def js_minify(js):
495657
"""Return a minified version of the Javascript string."""
496658
log.info("Compressing Javascript...")
497659
ins, outs = StringIO(js), StringIO()
@@ -510,7 +672,7 @@ def force_single_line_js(js):
510672
function_start_regex = re.compile('(function(\s+\w+|)\s*\(([^\)]*)\)\s*{)')
511673

512674

513-
def _findFunctions(whole):
675+
def _find_functions(whole):
514676
"""Find function() on Javascript code."""
515677
for res in function_start_regex.findall(whole):
516678
function_start, function_name, params = res
@@ -538,7 +700,7 @@ def slim_params(code):
538700
'function f(_0,_1){_0*_1}'
539701
"""
540702
old_functions, new_code = {}, code
541-
for params, params_split, core, function_start in _findFunctions(code):
703+
for params, params_split, core, function_start in _find_functions(code):
542704
params_split_use = [x for x in params_split if len(x) > 1]
543705
_param_regex = '|'.join([r'\b%s\b' % x for x in params_split_use])
544706
param_regex, new_params = re.compile(_param_regex), {}
@@ -610,11 +772,11 @@ def slim_func_names(js):
610772
return js + js_function_name_replacements
611773

612774

613-
def slim(js):
775+
def js_minify2(js):
614776
"""Compress variables and functions names.
615777
616-
>>> slim('function foo( long_variable_name, bar ) { console.log(bar); };')
617-
'function foo(_0,_1) { console.log(_1); };'
778+
>>> js_minify2('function foo(long_variable_name,bar){console.log(bar);};')
779+
'function foo(_0,_1){console.log(_1);};'
618780
"""
619781
log.debug("Obfuscating Javascript variables names inside functions.")
620782
# if eval() or with{} is used on JS is not too Safe to Obfuscate stuff.
@@ -801,13 +963,13 @@ def process_single_css_file(css_file_path):
801963
try: # Python3
802964
with open(css_file_path, encoding="utf-8-sig") as css_file:
803965
original_css = css_file.read()
804-
minified_css = cssmin(original_css, wrap=80,
805-
comments=args.comments)
966+
minified_css = css_minify(original_css, wrap=args.wrap,
967+
comments=args.comments)
806968
except: # Python2
807969
with open(css_file_path) as css_file:
808970
original_css = css_file.read()
809-
minified_css = cssmin(original_css, wrap=80,
810-
comments=args.comments)
971+
minified_css = css_minify(original_css, wrap=args.wrap,
972+
comments=args.comments)
811973
if args.timestamp:
812974
taim = "/* {} */ ".format(datetime.now().isoformat()[:-7].lower())
813975
minified_css = taim + minified_css
@@ -840,10 +1002,12 @@ def process_single_html_file(html_file_path):
8401002
log.info("Processing HTML file: {}".format(html_file_path))
8411003
try: # Python3
8421004
with open(html_file_path, encoding="utf-8-sig") as html_file:
843-
minified_html = htmlmin(html_file.read(), comments=args.comments)
1005+
minified_html = html_minify(html_file.read(),
1006+
comments=args.comments)
8441007
except: # Python2
8451008
with open(html_file_path) as html_file:
846-
minified_html = htmlmin(html_file.read(), comments=args.comments)
1009+
minified_html = html_minify(html_file.read(),
1010+
comments=args.comments)
8471011
html_file_path = prefixer_extensioner(html_file_path, ".htm", ".html")
8481012
try: # Python3
8491013
with open(html_file_path, "w", encoding="utf-8") as output_file:
@@ -859,11 +1023,13 @@ def process_single_js_file(js_file_path):
8591023
try: # Python3
8601024
with open(js_file_path, encoding="utf-8-sig") as js_file:
8611025
original_js = js_file.read()
862-
minified_js = simple_replacer_js(jsmin(slim(original_js)))
1026+
minified_js = simple_replacer_js(js_minify(
1027+
js_minify2(original_js)))
8631028
except: # Python2
8641029
with open(js_file_path) as js_file:
8651030
original_js = js_file.read()
866-
minified_js = simple_replacer_js(jsmin(slim(original_js)))
1031+
minified_js = simple_replacer_js(js_minify(
1032+
js_minify2(original_js)))
8671033
if args.timestamp:
8681034
taim = "/* {} */ ".format(datetime.now().isoformat()[:-7].lower())
8691035
minified_js = taim + minified_js
@@ -954,7 +1120,8 @@ def new(*args):
9541120
Takes a file or folder full path string and process all CSS/HTML/JS found.
9551121
If argument is not file/folder will fail. Check Updates works on Python3.
9561122
StdIn to StdOut is deprecated since may fail with unicode characters.
957-
SHA1 HEX-Digest 11 Chars Hash on Filenames is used for Server Cache.""")
1123+
SHA1 HEX-Digest 11 Chars Hash on Filenames is used for Server Cache.
1124+
CSS Properties are AlphaSorted,to help spot cloned ones,Selectors not.""")
9581125
parser.add_argument('--version', action='version', version=__version__)
9591126
parser.add_argument('fullpath', metavar='fullpath', type=str,
9601127
help='Full path to local file or folder.')

0 commit comments

Comments
 (0)