1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.2'
75 __all__ = ['cssmin']
76
77 import re as _re
78
79
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.
88
89 :Return: Minifier
90 :Rtype: ``callable``
91 """
92
93
94
95
96
97
98 if not python_only:
99 try:
100 import _rcssmin
101 except ImportError:
102 pass
103 else:
104 return _rcssmin.cssmin
105
106 nl = r'(?:[\n\f]|\r\n?)'
107 spacechar = r'[\r\n\f\040\t]'
108
109 unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?'
110 escaped = r'[^\n\r\f0-9a-fA-F]'
111 escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals()
112
113 nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]'
114
115
116
117
118
119 comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
120
121
122 _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
123
124 string1 = \
125 r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)'
126 string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")'
127 strings = r'(?:%s|%s)' % (string1, string2)
128
129 nl_string1 = \
130 r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)'
131 nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")'
132 nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2)
133
134 uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)'
135 uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")'
136 uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2)
137
138 nl_escaped = r'(?:\\%(nl)s)' % locals()
139
140 space = r'(?:%(spacechar)s|%(comment)s)' % locals()
141
142 ie7hack = r'(?:>/\*\*/)'
143
144 uri = (r'(?:'
145 r'(?:[^\000-\040"\047()\\\177]*'
146 r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
147 r'(?:'
148 r'(?:%(spacechar)s+|%(nl_escaped)s+)'
149 r'(?:'
150 r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)'
151 r'[^\000-\040"\047()\\\177]*'
152 r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*'
153 r')+'
154 r')*'
155 r')') % locals()
156
157 nl_unesc_sub = _re.compile(nl_escaped).sub
158
159 uri_space_sub = _re.compile((
160 r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
161 ) % locals()).sub
162 uri_space_subber = lambda m: m.groups()[0] or ''
163
164 space_sub_simple = _re.compile((
165 r'[\r\n\f\040\t;]+|(%(comment)s+)'
166 ) % locals()).sub
167 space_sub_banged = _re.compile((
168 r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
169 ) % locals()).sub
170
171 post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub
172
173 main_sub = _re.compile((
174 r'([^\\"\047u>@\r\n\f\040\t/;:{}]+)'
175 r'|(?<=[{}(=:>+[,!])(%(space)s+)'
176 r'|^(%(space)s+)'
177 r'|(%(space)s+)(?=(([:{});=>+\],!])|$)?)'
178 r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)'
179 r'|(\{)'
180 r'|(\})'
181 r'|(%(strings)s)'
182 r'|(?<!%(nmchar)s)url\(%(spacechar)s*('
183 r'%(uri_nl_strings)s'
184 r'|%(uri)s'
185 r')%(spacechar)s*\)'
186 r'|(@[mM][eE][dD][iI][aA])(?!%(nmchar)s)'
187 r'|(%(ie7hack)s)(%(space)s*)'
188 r'|(:[fF][iI][rR][sS][tT]-[lL]'
189 r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'
190 r'(%(space)s*)(?=[{,])'
191 r'|(%(nl_strings)s)'
192 r'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}]*)'
193 ) % locals()).sub
194
195
196
197 def main_subber(keep_bang_comments):
198 """ Make main subber """
199 in_macie5, in_rule, at_media = [0], [0], [0]
200
201 if keep_bang_comments:
202 space_sub = space_sub_banged
203 def space_subber(match):
204 """ Space|Comment subber """
205 if match.lastindex:
206 group1, group2 = match.group(1, 2)
207 if group2:
208 if group1.endswith(r'\*/'):
209 in_macie5[0] = 1
210 else:
211 in_macie5[0] = 0
212 return group1
213 elif group1:
214 if group1.endswith(r'\*/'):
215 if in_macie5[0]:
216 return ''
217 in_macie5[0] = 1
218 return r'/*\*/'
219 elif in_macie5[0]:
220 in_macie5[0] = 0
221 return '/**/'
222 return ''
223 else:
224 space_sub = space_sub_simple
225 def space_subber(match):
226 """ Space|Comment subber """
227 if match.lastindex:
228 if match.group(1).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
238 def fn_space_post(group):
239 """ space with token after """
240 if group(5) is None or (
241 group(6) == ':' and not in_rule[0] and not at_media[0]):
242 return ' ' + space_sub(space_subber, group(4))
243 return space_sub(space_subber, group(4))
244
245 def fn_semicolon(group):
246 """ ; handler """
247 return ';' + space_sub(space_subber, group(7))
248
249 def fn_semicolon2(group):
250 """ ; handler """
251 if in_rule[0]:
252 return space_sub(space_subber, group(7))
253 return ';' + space_sub(space_subber, group(7))
254
255 def fn_open(group):
256 """ { handler """
257
258 if at_media[0]:
259 at_media[0] -= 1
260 else:
261 in_rule[0] = 1
262 return '{'
263
264 def fn_close(group):
265 """ } handler """
266
267 in_rule[0] = 0
268 return '}'
269
270 def fn_media(group):
271 """ @media handler """
272 at_media[0] += 1
273 return group(13)
274
275 def fn_ie7hack(group):
276 """ IE7 Hack handler """
277 if not in_rule[0] and not at_media[0]:
278 in_macie5[0] = 0
279 return group(14) + space_sub(space_subber, group(15))
280 return '>' + space_sub(space_subber, group(15))
281
282 table = (
283 None,
284 None,
285 None,
286 None,
287 fn_space_post,
288 fn_space_post,
289 fn_space_post,
290 fn_semicolon,
291 fn_semicolon2,
292 fn_open,
293 fn_close,
294 lambda g: g(11),
295 lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
296
297 fn_media,
298 None,
299 fn_ie7hack,
300 None,
301 lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
302
303
304
305 lambda g: nl_unesc_sub('', g(18)),
306 lambda g: post_esc_sub(' ', g(19)),
307 )
308
309 def func(match):
310 """ Main subber """
311 idx, group = match.lastindex, match.group
312 if idx > 3:
313 return table[idx](group)
314
315
316 elif idx == 1:
317 return group(1)
318
319 return space_sub(space_subber, group(idx))
320
321 return func
322
323 def cssmin(style, keep_bang_comments=False):
324 """
325 Minify CSS.
326
327 :Parameters:
328 `style` : ``str``
329 CSS to minify
330
331 `keep_bang_comments` : ``bool``
332 Keep comments starting with an exclamation mark? (``/*!...*/``)
333
334 :Return: Minified style
335 :Rtype: ``str``
336 """
337 return main_sub(main_subber(keep_bang_comments), style)
338
339 return cssmin
340
341 cssmin = _make_cssmin()
342
343
344 if __name__ == '__main__':
346 """ Main """
347 import sys as _sys
348 keep_bang_comments = (
349 '-b' in _sys.argv[1:]
350 or '-bp' in _sys.argv[1:]
351 or '-pb' in _sys.argv[1:]
352 )
353 if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \
354 or '-pb' in _sys.argv[1:]:
355 global cssmin
356 cssmin = _make_cssmin(python_only=True)
357 _sys.stdout.write(cssmin(
358 _sys.stdin.read(), keep_bang_comments=keep_bang_comments
359 ))
360 main()
361