Package tdi :: Package tools :: Module rcssmin
[frames] | no frames]

Source Code for Module tdi.tools.rcssmin

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