Module rcssmin

Source Code for Module rcssmin

  1  #!/usr/bin/env python 
  2  # -*- coding: ascii -*- 
  3  """ 
  4  :Copyright: 
  5   
  6   Copyright 2011 - 2014 
  7   Andr\xe9 Malo or his licensors, as applicable 
  8   
  9  :License: 
 10   
 11   Licensed under the Apache License, Version 2.0 (the "License"); 
 12   you may not use this file except in compliance with the License. 
 13   You may obtain a copy of the License at 
 14   
 15       http://www.apache.org/licenses/LICENSE-2.0 
 16   
 17   Unless required by applicable law or agreed to in writing, software 
 18   distributed under the License is distributed on an "AS IS" BASIS, 
 19   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 20   See the License for the specific language governing permissions and 
 21   limitations under the License. 
 22   
 23  ============== 
 24   CSS Minifier 
 25  ============== 
 26   
 27  CSS Minifier. 
 28   
 29  The minifier is based on the semantics of the `YUI compressor`_\\, which 
 30  itself is based on `the rule list by Isaac Schlueter`_\\. 
 31   
 32  This module is a re-implementation aiming for speed instead of maximum 
 33  compression, so it can be used at runtime (rather than during a preprocessing 
 34  step). RCSSmin does syntactical compression only (removing spaces, comments 
 35  and possibly semicolons). It does not provide semantic compression (like 
 36  removing empty blocks, collapsing redundant properties etc). It does, however, 
 37  support various CSS hacks (by keeping them working as intended). 
 38   
 39  Here's a feature list: 
 40   
 41  - Strings are kept, except that escaped newlines are stripped 
 42  - Space/Comments before the very end or before various characters are 
 43    stripped: ``:{});=>+],!`` (The colon (``:``) is a special case, a single 
 44    space is kept if it's outside a ruleset.) 
 45  - Space/Comments at the very beginning or after various characters are 
 46    stripped: ``{}(=:>+[,!`` 
 47  - Optional space after unicode escapes is kept, resp. replaced by a simple 
 48    space 
 49  - whitespaces inside ``url()`` definitions are stripped 
 50  - Comments starting with an exclamation mark (``!``) can be kept optionally. 
 51  - All other comments and/or whitespace characters are replaced by a single 
 52    space. 
 53  - Multiple consecutive semicolons are reduced to one 
 54  - The last semicolon within a ruleset is stripped 
 55  - CSS Hacks supported: 
 56   
 57    - IE7 hack (``>/**/``) 
 58    - Mac-IE5 hack (``/*\\*/.../**/``) 
 59    - The boxmodelhack is supported naturally because it relies on valid CSS2 
 60      strings 
 61    - Between ``:first-line`` and the following comma or curly brace a space is 
 62      inserted. (apparently it's needed for IE6) 
 63    - Same for ``:first-letter`` 
 64   
 65  rcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to 
 66  factor 50 or so (depending on the input). 
 67   
 68  Both python 2 (>= 2.4) and python 3 are supported. 
 69   
 70  .. _YUI compressor: https://github.com/yui/yuicompressor/ 
 71   
 72  .. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/tree/ 
 73  """ 
 74  if 1: 
 75      # pylint: disable = W0622 
 76      __doc__ = getattr(__doc__, 'decode', lambda x: __doc__)('latin-1') 
 77  __author__ = "Andr\xe9 Malo" 
 78  __author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1') 
 79  __docformat__ = "restructuredtext en" 
 80  __license__ = "Apache License, Version 2.0" 
 81  __version__ = '1.0.3' 
 82  __all__ = ['cssmin'] 
 83   
 84  import re as _re 
 85   
 86   
87 -def _make_cssmin(python_only=False):
88 """ 89 Generate CSS minifier. 90 91 :Parameters: 92 `python_only` : ``bool`` 93 Use only the python variant. If true, the c extension is not even 94 tried to be loaded. 95 96 :Return: Minifier 97 :Rtype: ``callable`` 98 """ 99 # pylint: disable = W0612 100 # ("unused" variables) 101 102 # pylint: disable = R0911, R0912, R0914, R0915 103 # (too many anything) 104 105 if not python_only: 106 try: 107 import _rcssmin 108 except ImportError: 109 pass 110 else: 111 return _rcssmin.cssmin 112 113 nl = r'(?:[\n\f]|\r\n?)' # pylint: disable = C0103 114 spacechar = r'[\r\n\f\040\t]' 115 116 unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?' 117 escaped = r'[^\n\r\f0-9a-fA-F]' 118 escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals() 119 120 nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]' 121 #nmstart = r'[^\000-\100\133-\136\140\173-\177]' 122 #ident = (r'(?:' 123 # r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*' 124 #r')') % locals() 125 126 comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' 127 128 # only for specific purposes. The bang is grouped: 129 _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)' 130 131 string1 = \ 132 r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)' 133 string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")' 134 strings = r'(?:%s|%s)' % (string1, string2) 135 136 nl_string1 = \ 137 r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)' 138 nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")' 139 nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2) 140 141 uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)' 142 uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")' 143 uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2) 144 145 nl_escaped = r'(?:\\%(nl)s)' % locals() 146 147 space = r'(?:%(spacechar)s|%(comment)s)' % locals() 148 149 ie7hack = r'(?:>/\*\*/)' 150 151 uri = (r'(?:' 152 r'(?:[^\000-\040"\047()\\\177]*' 153 r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)' 154 r'(?:' 155 r'(?:%(spacechar)s+|%(nl_escaped)s+)' 156 r'(?:' 157 r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)' 158 r'[^\000-\040"\047()\\\177]*' 159 r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*' 160 r')+' 161 r')*' 162 r')') % locals() 163 164 nl_unesc_sub = _re.compile(nl_escaped).sub 165 166 uri_space_sub = _re.compile(( 167 r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+' 168 ) % locals()).sub 169 uri_space_subber = lambda m: m.groups()[0] or '' 170 171 space_sub_simple = _re.compile(( 172 r'[\r\n\f\040\t;]+|(%(comment)s+)' 173 ) % locals()).sub 174 space_sub_banged = _re.compile(( 175 r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)' 176 ) % locals()).sub 177 178 post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub 179 180 main_sub = _re.compile(( 181 r'([^\\"\047u>@\r\n\f\040\t/;:{}]+)' 182 r'|(?<=[{}(=:>+[,!])(%(space)s+)' 183 r'|^(%(space)s+)' 184 r'|(%(space)s+)(?=(([:{});=>+\],!])|$)?)' 185 r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)' 186 r'|(\{)' 187 r'|(\})' 188 r'|(%(strings)s)' 189 r'|(?<!%(nmchar)s)url\(%(spacechar)s*(' 190 r'%(uri_nl_strings)s' 191 r'|%(uri)s' 192 r')%(spacechar)s*\)' 193 r'|(@(?:' 194 r'[mM][eE][dD][iI][aA]' 195 r'|[sS][uU][pP][pP][oO][rR][tT][sS]' 196 r'|[dD][oO][cC][uU][mM][eE][nN][tT]' 197 r'|(?:-(?:' 198 r'[wW][eE][bB][kK][iI][tT]|[mM][oO][zZ]|[oO]|[mM][sS]' 199 r')-)?' 200 r'[kK][eE][yY][fF][rR][aA][mM][eE][sS]' 201 r'))(?!%(nmchar)s)' 202 r'|(%(ie7hack)s)(%(space)s*)' 203 r'|(:[fF][iI][rR][sS][tT]-[lL]' 204 r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))' 205 r'(%(space)s*)(?=[{,])' 206 r'|(%(nl_strings)s)' 207 r'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}]*)' 208 ) % locals()).sub 209 210 #print main_sub.__self__.pattern 211 212 def main_subber(keep_bang_comments): 213 """ Make main subber """ 214 in_macie5, in_rule, at_group = [0], [0], [0] 215 216 if keep_bang_comments: 217 space_sub = space_sub_banged 218 def space_subber(match): 219 """ Space|Comment subber """ 220 if match.lastindex: 221 group1, group2 = match.group(1, 2) 222 if group2: 223 if group1.endswith(r'\*/'): 224 in_macie5[0] = 1 225 else: 226 in_macie5[0] = 0 227 return group1 228 elif group1: 229 if group1.endswith(r'\*/'): 230 if in_macie5[0]: 231 return '' 232 in_macie5[0] = 1 233 return r'/*\*/' 234 elif in_macie5[0]: 235 in_macie5[0] = 0 236 return '/**/' 237 return ''
238 else: 239 space_sub = space_sub_simple 240 def space_subber(match): 241 """ Space|Comment subber """ 242 if match.lastindex: 243 if match.group(1).endswith(r'\*/'): 244 if in_macie5[0]: 245 return '' 246 in_macie5[0] = 1 247 return r'/*\*/' 248 elif in_macie5[0]: 249 in_macie5[0] = 0 250 return '/**/' 251 return '' 252 253 def fn_space_post(group): 254 """ space with token after """ 255 if group(5) is None or ( 256 group(6) == ':' and not in_rule[0] and not at_group[0]): 257 return ' ' + space_sub(space_subber, group(4)) 258 return space_sub(space_subber, group(4)) 259 260 def fn_semicolon(group): 261 """ ; handler """ 262 return ';' + space_sub(space_subber, group(7)) 263 264 def fn_semicolon2(group): 265 """ ; handler """ 266 if in_rule[0]: 267 return space_sub(space_subber, group(7)) 268 return ';' + space_sub(space_subber, group(7)) 269 270 def fn_open(group): 271 """ { handler """ 272 # pylint: disable = W0613 273 if at_group[0]: 274 at_group[0] -= 1 275 else: 276 in_rule[0] = 1 277 return '{' 278 279 def fn_close(group): 280 """ } handler """ 281 # pylint: disable = W0613 282 in_rule[0] = 0 283 return '}' 284 285 def fn_at_group(group): 286 """ @xxx group handler """ 287 at_group[0] += 1 288 return group(13) 289 290 def fn_ie7hack(group): 291 """ IE7 Hack handler """ 292 if not in_rule[0] and not at_group[0]: 293 in_macie5[0] = 0 294 return group(14) + space_sub(space_subber, group(15)) 295 return '>' + space_sub(space_subber, group(15)) 296 297 table = ( 298 None, 299 None, 300 None, 301 None, 302 fn_space_post, # space with token after 303 fn_space_post, # space with token after 304 fn_space_post, # space with token after 305 fn_semicolon, # semicolon 306 fn_semicolon2, # semicolon 307 fn_open, # { 308 fn_close, # } 309 lambda g: g(11), # string 310 lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)), 311 # url(...) 312 fn_at_group, # @xxx expecting {...} 313 None, 314 fn_ie7hack, # ie7hack 315 None, 316 lambda g: g(16) + ' ' + space_sub(space_subber, g(17)), 317 # :first-line|letter followed 318 # by [{,] (apparently space 319 # needed for IE6) 320 lambda g: nl_unesc_sub('', g(18)), # nl_string 321 lambda g: post_esc_sub(' ', g(19)), # escape 322 ) 323 324 def func(match): 325 """ Main subber """ 326 idx, group = match.lastindex, match.group 327 if idx > 3: 328 return table[idx](group) 329 330 # shortcuts for frequent operations below: 331 elif idx == 1: # not interesting 332 return group(1) 333 #else: # space with token before or at the beginning 334 return space_sub(space_subber, group(idx)) 335 336 return func 337 338 def cssmin(style, keep_bang_comments=False): # pylint: disable = W0621 339 """ 340 Minify CSS. 341 342 :Parameters: 343 `style` : ``str`` 344 CSS to minify 345 346 `keep_bang_comments` : ``bool`` 347 Keep comments starting with an exclamation mark? (``/*!...*/``) 348 349 :Return: Minified style 350 :Rtype: ``str`` 351 """ 352 return main_sub(main_subber(keep_bang_comments), style) 353 354 return cssmin 355 356 cssmin = _make_cssmin() 357 358 359 if __name__ == '__main__':
360 - def main():
361 """ Main """ 362 import sys as _sys 363 keep_bang_comments = ( 364 '-b' in _sys.argv[1:] 365 or '-bp' in _sys.argv[1:] 366 or '-pb' in _sys.argv[1:] 367 ) 368 if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \ 369 or '-pb' in _sys.argv[1:]: 370 global cssmin # pylint: disable = W0603 371 cssmin = _make_cssmin(python_only=True) 372 _sys.stdout.write(cssmin( 373 _sys.stdin.read(), keep_bang_comments=keep_bang_comments 374 ))
375 main() 376