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