1#!/usr/bin/env python
2# -*- coding: ascii -*-
3r"""
4==============
5 CSS Minifier
6==============
7
8CSS Minifier.
9
10The minifier is based on the semantics of the `YUI compressor`_\\, which
11itself 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
32This module is a re-implementation aiming for speed instead of maximum
33compression, so it can be used at runtime (rather than during a preprocessing
34step). RCSSmin does syntactical compression only (removing spaces, comments
35and possibly semicolons). It does not provide semantic compression (like
36removing empty blocks, collapsing redundant properties etc). It does, however,
37support various CSS hacks (by keeping them working as intended).
38
39Here'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
65rcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to
66factor 100 or so (depending on the input). docs/BENCHMARKS in the source
67distribution contains the details.
68
69Both 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"""
75if __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
84import re as _re
85
86
87def _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
355cssmin = _make_cssmin()
356
357
358if __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