10
10
11
11
12
12
import gzip
13
+ import itertools
13
14
import logging as log
14
15
import os
15
16
import re
104
105
'turquoise' : (64 , 224 , 208 ), 'violet' : (238 , 130 , 238 ),
105
106
'wheat' : (245 , 222 , 179 )
106
107
}
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
+ '''
107
189
108
190
109
191
###############################################################################
110
192
# CSS minify
111
193
112
194
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
+
113
274
def remove_comments (css ):
114
275
"""Remove all CSS comment blocks."""
115
276
log .debug ("Removing all Comments." )
@@ -257,7 +418,7 @@ def condense_semicolons(css):
257
418
return re .sub (r";;+" , ";" , css )
258
419
259
420
260
- def wrap_css_lines (css , line_length ):
421
+ def wrap_css_lines (css , line_length = 80 ):
261
422
"""Wrap the lines of the given CSS to an approximate length."""
262
423
log .debug ("Wrapping lines to ~{} max line lenght." .format (line_length ))
263
424
lines , line_start = [], 0
@@ -328,10 +489,11 @@ def unquote_selectors(css):
328
489
return re .compile ('([a-zA-Z]+)="([a-zA-Z0-9-_\.]+)"]' ).sub (r'\1=\2]' , css )
329
490
330
491
331
- def cssmin (css , wrap = None , comments = False ):
492
+ def css_minify (css , wrap = False , comments = False ):
332
493
"""Minify CSS main function."""
333
494
log .info ("Compressing CSS..." )
334
495
css = remove_comments (css ) if not comments else css
496
+ css = sort_properties (css )
335
497
css = unquote_selectors (css )
336
498
css = condense_whitespace (css )
337
499
css = remove_url_quotes (css )
@@ -346,7 +508,7 @@ def cssmin(css, wrap=None, comments=False):
346
508
css = normalize_rgb_colors_to_hex (css )
347
509
css = condense_hex_colors (css )
348
510
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
350
512
css = condense_semicolons (css )
351
513
css = add_encoding (css )
352
514
css = restore_needed_space (css )
@@ -463,10 +625,10 @@ def unquote_html_attributes(html):
463
625
return html .strip ()
464
626
465
627
466
- def htmlmin (html , comments = False ):
628
+ def html_minify (html , comments = False ):
467
629
"""Minify HTML main function.
468
630
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> ')
470
632
'<p width=9 height=5 > b c <br>'
471
633
"""
472
634
log .info ("Compressing HTML..." )
@@ -491,7 +653,7 @@ def simple_replacer_js(js):
491
653
";}" , "}" ).replace ("; " , ";" ).replace (" ;" , ";" ).rstrip ("\n ;" ))
492
654
493
655
494
- def jsmin (js ):
656
+ def js_minify (js ):
495
657
"""Return a minified version of the Javascript string."""
496
658
log .info ("Compressing Javascript..." )
497
659
ins , outs = StringIO (js ), StringIO ()
@@ -510,7 +672,7 @@ def force_single_line_js(js):
510
672
function_start_regex = re .compile ('(function(\s+\w+|)\s*\(([^\)]*)\)\s*{)' )
511
673
512
674
513
- def _findFunctions (whole ):
675
+ def _find_functions (whole ):
514
676
"""Find function() on Javascript code."""
515
677
for res in function_start_regex .findall (whole ):
516
678
function_start , function_name , params = res
@@ -538,7 +700,7 @@ def slim_params(code):
538
700
'function f(_0,_1){_0*_1}'
539
701
"""
540
702
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 ):
542
704
params_split_use = [x for x in params_split if len (x ) > 1 ]
543
705
_param_regex = '|' .join ([r'\b%s\b' % x for x in params_split_use ])
544
706
param_regex , new_params = re .compile (_param_regex ), {}
@@ -610,11 +772,11 @@ def slim_func_names(js):
610
772
return js + js_function_name_replacements
611
773
612
774
613
- def slim (js ):
775
+ def js_minify2 (js ):
614
776
"""Compress variables and functions names.
615
777
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);};'
618
780
"""
619
781
log .debug ("Obfuscating Javascript variables names inside functions." )
620
782
# 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):
801
963
try : # Python3
802
964
with open (css_file_path , encoding = "utf-8-sig" ) as css_file :
803
965
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 )
806
968
except : # Python2
807
969
with open (css_file_path ) as css_file :
808
970
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 )
811
973
if args .timestamp :
812
974
taim = "/* {} */ " .format (datetime .now ().isoformat ()[:- 7 ].lower ())
813
975
minified_css = taim + minified_css
@@ -840,10 +1002,12 @@ def process_single_html_file(html_file_path):
840
1002
log .info ("Processing HTML file: {}" .format (html_file_path ))
841
1003
try : # Python3
842
1004
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 )
844
1007
except : # Python2
845
1008
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 )
847
1011
html_file_path = prefixer_extensioner (html_file_path , ".htm" , ".html" )
848
1012
try : # Python3
849
1013
with open (html_file_path , "w" , encoding = "utf-8" ) as output_file :
@@ -859,11 +1023,13 @@ def process_single_js_file(js_file_path):
859
1023
try : # Python3
860
1024
with open (js_file_path , encoding = "utf-8-sig" ) as js_file :
861
1025
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 )))
863
1028
except : # Python2
864
1029
with open (js_file_path ) as js_file :
865
1030
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 )))
867
1033
if args .timestamp :
868
1034
taim = "/* {} */ " .format (datetime .now ().isoformat ()[:- 7 ].lower ())
869
1035
minified_js = taim + minified_js
@@ -954,7 +1120,8 @@ def new(*args):
954
1120
Takes a file or folder full path string and process all CSS/HTML/JS found.
955
1121
If argument is not file/folder will fail. Check Updates works on Python3.
956
1122
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.""" )
958
1125
parser .add_argument ('--version' , action = 'version' , version = __version__ )
959
1126
parser .add_argument ('fullpath' , metavar = 'fullpath' , type = str ,
960
1127
help = 'Full path to local file or folder.' )
0 commit comments