2 # -*- coding: ascii -*-
10 The minifier is based on the semantics of the `YUI compressor`_\\, which
11 itself is based on `the rule list by Isaac Schlueter`_\\.
16 Andr\xe9 Malo or his licensors, as applicable
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
24 http://www.apache.org/licenses/LICENSE-2.0
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.
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).
39 Here's a feature list:
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
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
53 - Multiple consecutive semicolons are reduced to one
54 - The last semicolon within a ruleset is stripped
55 - CSS Hacks supported:
57 - IE7 hack (``>/**/``)
58 - Mac-IE5 hack (``/*\\*/.../**/``)
59 - The boxmodelhack is supported naturally because it relies on valid CSS2
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``
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.
69 Supported python versions are 2.7 and 3.4+.
71 .. _YUI compressor: https://github.com/yui/yuicompressor/
73 .. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/
75 __author__
= u
"Andr\xe9 Malo"
76 __docformat__
= "restructuredtext en"
77 __license__
= "Apache License, Version 2.0"
84 def _make_cssmin(python_only
=False):
86 Generate CSS minifier.
89 `python_only` : ``bool``
90 Use only the python variant. If true, the c extension is not even
96 # pylint: disable = too-many-locals
104 # Ensure that the C version is in sync
105 if getattr(_rcssmin
, '__version__', None) == __version__
:
106 return _rcssmin
.cssmin
108 nl
= r
'(?:[\n\f]|\r\n?)' # pylint: disable = invalid-name
109 spacechar
= r
'[\r\n\f\040\t]'
111 unicoded
= r
'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?'
112 escaped
= r
'[^\n\r\f0-9a-fA-F]'
113 escape
= r
'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals()
115 nmchar
= r
'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]'
116 # nmstart = r'[^\000-\100\133-\136\140\173-\177]'
118 # r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'
121 comment
= r
'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
123 # only for specific purposes. The bang is grouped:
124 _bang_comment
= r
'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
127 r
'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)'
128 string2
= r
'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")'
129 strings
= r
'(?:%s|%s)' % (string1
, string2
)
132 r
'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)'
133 nl_string2
= r
'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")'
134 nl_strings
= r
'(?:%s|%s)' % (nl_string1
, nl_string2
)
136 uri_nl_string1
= r
'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)'
137 uri_nl_string2
= r
'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")'
138 uri_nl_strings
= r
'(?:%s|%s)' % (uri_nl_string1
, uri_nl_string2
)
140 nl_escaped
= r
'(?:\\%(nl)s)' % locals()
142 space
= r
'(?:%(spacechar)s|%(comment)s)' % locals()
144 ie7hack
= r
'(?:>/\*\*/)'
147 # noqa pylint: disable = bad-continuation
151 r
'(?:[^\000-\040"\047()\\\177]*'
152 r
'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
154 r
'(?:%(spacechar)s+|%(nl_escaped)s+)'
156 r
'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)'
157 r
'[^\000-\040"\047()\\\177]*'
158 r
'(?:%(escape)s[^\000-\040"\047()\\\177]*)*'
165 nl_unesc_sub
= _re
.compile(nl_escaped
).sub
167 uri_space_sub
= _re
.compile((
168 r
'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
170 uri_space_subber
= lambda m
: m
.groups()[0] or ''
172 space_sub_simple
= _re
.compile((
173 r
'[\r\n\f\040\t;]+|(%(comment)s+)'
175 space_sub_banged
= _re
.compile((
176 r
'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
179 post_esc_sub
= _re
.compile(r
'[\r\n\f\t]+').sub
181 main_sub
= _re
.compile((
182 # noqa pylint: disable = bad-continuation
184 r
'([^\\"\047u>@\r\n\f\040\t/;:{}+]+)' # 1
185 r
'|(?<=[{}(=:>[,!])(%(space)s+)' # 2
186 r
'|^(%(space)s+)' # 3
187 r
'|(%(space)s+)(?=(([:{});=>\],!])|$)?)' # 4, 5, 6
188 r
'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)' # 7, 8
191 r
'|(%(strings)s)' # 11
192 r
'|(?<!%(nmchar)s)url\(%(spacechar)s*(' # 12
193 r
'%(uri_nl_strings)s'
197 r
'[mM][eE][dD][iI][aA]'
198 r
'|[sS][uU][pP][pP][oO][rR][tT][sS]'
199 r
'|[dD][oO][cC][uU][mM][eE][nN][tT]'
201 r
'[wW][eE][bB][kK][iI][tT]|[mM][oO][zZ]|[oO]|[mM][sS]'
203 r
'[kK][eE][yY][fF][rR][aA][mM][eE][sS]'
205 r
'|(%(ie7hack)s)(%(space)s*)' # 14, 15
206 r
'|(:[fF][iI][rR][sS][tT]-[lL]' # 16
207 r
'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'
208 r
'(%(space)s*)(?=[{,])' # 17
209 r
'|(%(nl_strings)s)' # 18
210 r
'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}+]*)' # 19
213 # print(main_sub.__self__.pattern)
215 def main_subber(keep_bang_comments
):
216 """ Make main subber """
217 in_macie5
, in_rule
, at_group
= [0], [0], [0]
219 if keep_bang_comments
:
220 space_sub
= space_sub_banged
222 def space_subber(match
):
223 """ Space|Comment subber """
225 group1
, group2
= match
.group(1, 2)
227 if group1
.endswith(r
'\*/'):
233 if group1
.endswith(r
'\*/'):
243 space_sub
= space_sub_simple
245 def space_subber(match
):
246 """ Space|Comment subber """
248 if match
.group(1).endswith(r
'\*/'):
258 def fn_space_post(group
):
259 """ space with token after """
260 if group(5) is None or (
261 group(6) == ':' and not in_rule
[0] and not at_group
[0]):
262 return ' ' + space_sub(space_subber
, group(4))
263 return space_sub(space_subber
, group(4))
265 def fn_semicolon(group
):
267 return ';' + space_sub(space_subber
, group(7))
269 def fn_semicolon2(group
):
272 return space_sub(space_subber
, group(7))
273 return ';' + space_sub(space_subber
, group(7))
288 def fn_at_group(group
):
289 """ @xxx group handler """
293 def fn_ie7hack(group
):
294 """ IE7 Hack handler """
295 if not in_rule
[0] and not at_group
[0]:
297 return group(14) + space_sub(space_subber
, group(15))
298 return '>' + space_sub(space_subber
, group(15))
301 # noqa pylint: disable = bad-continuation
307 fn_space_post
, # space with token after
308 fn_space_post
, # space with token after
309 fn_space_post
, # space with token after
310 fn_semicolon
, # semicolon
311 fn_semicolon2
, # semicolon
314 lambda g
: g(11), # string
315 lambda g
: 'url(%s)' % uri_space_sub(uri_space_subber
, g(12)),
317 fn_at_group
, # @xxx expecting {...}
319 fn_ie7hack
, # ie7hack
321 lambda g
: g(16) + ' ' + space_sub(space_subber
, g(17)),
322 # :first-line|letter followed
323 # by [{,] (apparently space
325 lambda g
: nl_unesc_sub('', g(18)), # nl_string
326 lambda g
: post_esc_sub(' ', g(19)), # escape
331 idx
, group
= match
.lastindex
, match
.group
333 return table
[idx
](group
)
335 # shortcuts for frequent operations below:
336 elif idx
== 1: # not interesting
338 # else: # space with token before or at the beginning
339 return space_sub(space_subber
, group(idx
))
343 def cssmin(style
, keep_bang_comments
=False):
351 `keep_bang_comments` : ``bool``
352 Keep comments starting with an exclamation mark? (``/*!...*/``)
354 :Return: Minified style
357 # pylint: disable = redefined-outer-name
359 is_bytes
, style
= _as_str(style
)
360 style
= main_sub(main_subber(keep_bang_comments
), style
)
362 return style
.encode('latin-1')
367 cssmin
= _make_cssmin()
371 """ Make sure the style is a text string """
374 if not isinstance(style
, basestring
): # noqa pylint: disable = undefined-variable
375 raise TypeError("Unexpected type")
376 elif isinstance(style
, (bytes
, bytearray
)):
378 style
= style
.decode('latin-1')
379 elif not isinstance(style
, str):
380 raise TypeError("Unexpected type")
382 return is_bytes
, style
385 if __name__
== '__main__':
390 keep_bang_comments
= (
391 '-b' in _sys
.argv
[1:]
392 or '-bp' in _sys
.argv
[1:]
393 or '-pb' in _sys
.argv
[1:]
395 if '-p' in _sys
.argv
[1:] or '-bp' in _sys
.argv
[1:] \
396 or '-pb' in _sys
.argv
[1:]:
397 xcssmin
= _make_cssmin(python_only
=True)
400 _sys
.stdout
.write(xcssmin(
401 _sys
.stdin
.read(), keep_bang_comments
=keep_bang_comments