Home | History | Annotate | Download | only in rcssmin
      1 #!/usr/bin/env python
      2 # -*- coding: ascii -*-
      3 r"""
      4 ==============
      5  CSS Minifier
      6 ==============
      7 
      8 CSS Minifier.
      9 
     10 The minifier is based on the semantics of the `YUI compressor`_\\, which
     11 itself is based on `the rule list by Isaac Schlueter`_\\.
     12 
     13 :Copyright:
     14 
     15  Copyright 2011 - 2014
     16  Andr\xe9 Malo or his licensors, as applicable
     17 
     18 :License:
     19 
     20  Licensed under the Apache License, Version 2.0 (the "License");
     21  you may not use this file except in compliance with the License.
     22  You may obtain a copy of the License at
     23 
     24      http://www.apache.org/licenses/LICENSE-2.0
     25 
     26  Unless required by applicable law or agreed to in writing, software
     27  distributed under the License is distributed on an "AS IS" BASIS,
     28  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     29  See the License for the specific language governing permissions and
     30  limitations under the License.
     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 100 or so (depending on the input). docs/BENCHMARKS in the source
     67 distribution contains the details.
     68 
     69 Both python 2 (>= 2.4) and python 3 are supported.
     70 
     71 .. _YUI compressor: https://github.com/yui/yuicompressor/
     72 
     73 .. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/
     74 """
     75 if __doc__:
     76     # pylint: disable = W0622
     77     __doc__ = __doc__.encode('ascii').decode('unicode_escape')
     78 __author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape')
     79 __docformat__ = "restructuredtext en"
     80 __license__ = "Apache License, Version 2.0"
     81 __version__ = '1.0.5'
     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 = R0912, R0914, W0612
    100 
    101     if not python_only:
    102         try:
    103             import _rcssmin
    104         except ImportError:
    105             pass
    106         else:
    107             return _rcssmin.cssmin
    108 
    109     nl = r'(?:[\n\f]|\r\n?)'  # pylint: disable = C0103
    110     spacechar = r'[\r\n\f\040\t]'
    111 
    112     unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?'
    113     escaped = r'[^\n\r\f0-9a-fA-F]'
    114     escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals()
    115 
    116     nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]'
    117     #nmstart = r'[^\000-\100\133-\136\140\173-\177]'
    118     #ident = (r'(?:'
    119     #    r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'
    120     #r')') % locals()
    121 
    122     comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
    123 
    124     # only for specific purposes. The bang is grouped:
    125     _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
    126 
    127     string1 = \
    128         r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)'
    129     string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")'
    130     strings = r'(?:%s|%s)' % (string1, string2)
    131 
    132     nl_string1 = \
    133         r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)'
    134     nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")'
    135     nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2)
    136 
    137     uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)'
    138     uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")'
    139     uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2)
    140 
    141     nl_escaped = r'(?:\\%(nl)s)' % locals()
    142 
    143     space = r'(?:%(spacechar)s|%(comment)s)' % locals()
    144 
    145     ie7hack = r'(?:>/\*\*/)'
    146 
    147     uri = (r'(?:'
    148         # noqa pylint: disable = C0330
    149         r'(?:[^\000-\040"\047()\\\177]*'
    150             r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
    151         r'(?:'
    152             r'(?:%(spacechar)s+|%(nl_escaped)s+)'
    153             r'(?:'
    154                 r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)'
    155                 r'[^\000-\040"\047()\\\177]*'
    156                 r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*'
    157             r')+'
    158         r')*'
    159     r')') % locals()
    160 
    161     nl_unesc_sub = _re.compile(nl_escaped).sub
    162 
    163     uri_space_sub = _re.compile((
    164         r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
    165     ) % locals()).sub
    166     uri_space_subber = lambda m: m.groups()[0] or ''
    167 
    168     space_sub_simple = _re.compile((
    169         r'[\r\n\f\040\t;]+|(%(comment)s+)'
    170     ) % locals()).sub
    171     space_sub_banged = _re.compile((
    172         r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
    173     ) % locals()).sub
    174 
    175     post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub
    176 
    177     main_sub = _re.compile((
    178         # noqa pylint: disable = C0330
    179         r'([^\\"\047u>@\r\n\f\040\t/;:{}]+)'
    180         r'|(?<=[{}(=:>+[,!])(%(space)s+)'
    181         r'|^(%(space)s+)'
    182         r'|(%(space)s+)(?=(([:{});=>+\],!])|$)?)'
    183         r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)'
    184         r'|(\{)'
    185         r'|(\})'
    186         r'|(%(strings)s)'
    187         r'|(?<!%(nmchar)s)url\(%(spacechar)s*('
    188                 r'%(uri_nl_strings)s'
    189                 r'|%(uri)s'
    190             r')%(spacechar)s*\)'
    191         r'|(@(?:'
    192               r'[mM][eE][dD][iI][aA]'
    193               r'|[sS][uU][pP][pP][oO][rR][tT][sS]'
    194               r'|[dD][oO][cC][uU][mM][eE][nN][tT]'
    195               r'|(?:-(?:'
    196                   r'[wW][eE][bB][kK][iI][tT]|[mM][oO][zZ]|[oO]|[mM][sS]'
    197                 r')-)?'
    198                 r'[kK][eE][yY][fF][rR][aA][mM][eE][sS]'
    199             r'))(?!%(nmchar)s)'
    200         r'|(%(ie7hack)s)(%(space)s*)'
    201         r'|(:[fF][iI][rR][sS][tT]-[lL]'
    202             r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'
    203             r'(%(space)s*)(?=[{,])'
    204         r'|(%(nl_strings)s)'
    205         r'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}]*)'
    206     ) % locals()).sub
    207 
    208     #print main_sub.__self__.pattern
    209 
    210     def main_subber(keep_bang_comments):
    211         """ Make main subber """
    212         in_macie5, in_rule, at_group = [0], [0], [0]
    213 
    214         if keep_bang_comments:
    215             space_sub = space_sub_banged
    216 
    217             def space_subber(match):
    218                 """ Space|Comment subber """
    219                 if match.lastindex:
    220                     group1, group2 = match.group(1, 2)
    221                     if group2:
    222                         if group1.endswith(r'\*/'):
    223                             in_macie5[0] = 1
    224                         else:
    225                             in_macie5[0] = 0
    226                         return group1
    227                     elif group1:
    228                         if group1.endswith(r'\*/'):
    229                             if in_macie5[0]:
    230                                 return ''
    231                             in_macie5[0] = 1
    232                             return r'/*\*/'
    233                         elif in_macie5[0]:
    234                             in_macie5[0] = 0
    235                             return '/**/'
    236                 return ''
    237         else:
    238             space_sub = space_sub_simple
    239 
    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(_):
    271             """ { handler """
    272             if at_group[0]:
    273                 at_group[0] -= 1
    274             else:
    275                 in_rule[0] = 1
    276             return '{'
    277 
    278         def fn_close(_):
    279             """ } handler """
    280             in_rule[0] = 0
    281             return '}'
    282 
    283         def fn_at_group(group):
    284             """ @xxx group handler """
    285             at_group[0] += 1
    286             return group(13)
    287 
    288         def fn_ie7hack(group):
    289             """ IE7 Hack handler """
    290             if not in_rule[0] and not at_group[0]:
    291                 in_macie5[0] = 0
    292                 return group(14) + space_sub(space_subber, group(15))
    293             return '>' + space_sub(space_subber, group(15))
    294 
    295         table = (
    296             # noqa pylint: disable = C0330
    297             None,
    298             None,
    299             None,
    300             None,
    301             fn_space_post,                       # space with token after
    302             fn_space_post,                       # space with token after
    303             fn_space_post,                       # space with token after
    304             fn_semicolon,                        # semicolon
    305             fn_semicolon2,                       # semicolon
    306             fn_open,                             # {
    307             fn_close,                            # }
    308             lambda g: g(11),                     # string
    309             lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
    310                                                  # url(...)
    311             fn_at_group,                         # @xxx expecting {...}
    312             None,
    313             fn_ie7hack,                          # ie7hack
    314             None,
    315             lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
    316                                                  # :first-line|letter followed
    317                                                  # by [{,] (apparently space
    318                                                  # needed for IE6)
    319             lambda g: nl_unesc_sub('', g(18)),   # nl_string
    320             lambda g: post_esc_sub(' ', g(19)),  # escape
    321         )
    322 
    323         def func(match):
    324             """ Main subber """
    325             idx, group = match.lastindex, match.group
    326             if idx > 3:
    327                 return table[idx](group)
    328 
    329             # shortcuts for frequent operations below:
    330             elif idx == 1:     # not interesting
    331                 return group(1)
    332             #else: # space with token before or at the beginning
    333             return space_sub(space_subber, group(idx))
    334 
    335         return func
    336 
    337     def cssmin(style, keep_bang_comments=False):  # pylint: disable = W0621
    338         """
    339         Minify CSS.
    340 
    341         :Parameters:
    342           `style` : ``str``
    343             CSS to minify
    344 
    345           `keep_bang_comments` : ``bool``
    346             Keep comments starting with an exclamation mark? (``/*!...*/``)
    347 
    348         :Return: Minified style
    349         :Rtype: ``str``
    350         """
    351         return main_sub(main_subber(keep_bang_comments), style)
    352 
    353     return cssmin
    354 
    355 cssmin = _make_cssmin()
    356 
    357 
    358 if __name__ == '__main__':
    359     def main():
    360         """ Main """
    361         import sys as _sys
    362         keep_bang_comments = (
    363             '-b' in _sys.argv[1:]
    364             or '-bp' in _sys.argv[1:]
    365             or '-pb' in _sys.argv[1:]
    366         )
    367         if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \
    368                 or '-pb' in _sys.argv[1:]:
    369             global cssmin  # pylint: disable = W0603
    370             cssmin = _make_cssmin(python_only=True)
    371         _sys.stdout.write(cssmin(
    372             _sys.stdin.read(), keep_bang_comments=keep_bang_comments
    373         ))
    374     main()
    375