1
2
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
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
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
100
101
102
103
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?)'
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
122
123
124
125
126 comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
127
128
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
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
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
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,
303 fn_space_post,
304 fn_space_post,
305 fn_semicolon,
306 fn_semicolon2,
307 fn_open,
308 fn_close,
309 lambda g: g(11),
310 lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
311
312 fn_at_group,
313 None,
314 fn_ie7hack,
315 None,
316 lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
317
318
319
320 lambda g: nl_unesc_sub('', g(18)),
321 lambda g: post_esc_sub(' ', g(19)),
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
331 elif idx == 1:
332 return group(1)
333
334 return space_sub(space_subber, group(idx))
335
336 return func
337
338 def cssmin(style, keep_bang_comments=False):
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__':
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
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