Merge branch '2.0.x-maintenance' into master-merge-commit
[ExpressLRS.git] / src / python / minify / rcssmin.py
bloba8f7637b461005ff029a30ee19203671304d3df5
1 #!/usr/bin/env python
2 # -*- coding: ascii -*-
3 u"""
4 ==============
5 CSS Minifier
6 ==============
8 CSS Minifier.
10 The minifier is based on the semantics of the `YUI compressor`_\\, which
11 itself is based on `the rule list by Isaac Schlueter`_\\.
13 :Copyright:
15 Copyright 2011 - 2019
16 Andr\xe9 Malo or his licensors, as applicable
18 :License:
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
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:
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``
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/
74 """
75 __author__ = u"Andr\xe9 Malo"
76 __docformat__ = "restructuredtext en"
77 __license__ = "Apache License, Version 2.0"
78 __version__ = '1.0.6'
79 __all__ = ['cssmin']
81 import re as _re
84 def _make_cssmin(python_only=False):
85 """
86 Generate CSS minifier.
88 :Parameters:
89 `python_only` : ``bool``
90 Use only the python variant. If true, the c extension is not even
91 tried to be loaded.
93 :Return: Minifier
94 :Rtype: ``callable``
95 """
96 # pylint: disable = too-many-locals
98 if not python_only:
99 try:
100 import _rcssmin
101 except ImportError:
102 pass
103 else:
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]'
117 # ident = (r'(?:'
118 # r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'
119 # r')') % locals()
121 comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
123 # only for specific purposes. The bang is grouped:
124 _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
126 string1 = \
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)
131 nl_string1 = \
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'(?:>/\*\*/)'
146 uri = (
147 # noqa pylint: disable = bad-continuation
149 r'(?:'
151 r'(?:[^\000-\040"\047()\\\177]*'
152 r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
153 r'(?:'
154 r'(?:%(spacechar)s+|%(nl_escaped)s+)'
155 r'(?:'
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]*)*'
159 r')+'
160 r')*'
162 r')'
163 ) % locals()
165 nl_unesc_sub = _re.compile(nl_escaped).sub
167 uri_space_sub = _re.compile((
168 r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
169 ) % locals()).sub
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+)'
174 ) % locals()).sub
175 space_sub_banged = _re.compile((
176 r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
177 ) % locals()).sub
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
189 r'|(\{)' # 9
190 r'|(\})' # 10
191 r'|(%(strings)s)' # 11
192 r'|(?<!%(nmchar)s)url\(%(spacechar)s*(' # 12
193 r'%(uri_nl_strings)s'
194 r'|%(uri)s'
195 r')%(spacechar)s*\)'
196 r'|(@(?:' # 13
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]'
200 r'|(?:-(?:'
201 r'[wW][eE][bB][kK][iI][tT]|[mM][oO][zZ]|[oO]|[mM][sS]'
202 r')-)?'
203 r'[kK][eE][yY][fF][rR][aA][mM][eE][sS]'
204 r'))(?!%(nmchar)s)'
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
211 ) % locals()).sub
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 """
224 if match.lastindex:
225 group1, group2 = match.group(1, 2)
226 if group2:
227 if group1.endswith(r'\*/'):
228 in_macie5[0] = 1
229 else:
230 in_macie5[0] = 0
231 return group1
233 if group1.endswith(r'\*/'):
234 if in_macie5[0]:
235 return ''
236 in_macie5[0] = 1
237 return r'/*\*/'
238 elif in_macie5[0]:
239 in_macie5[0] = 0
240 return '/**/'
241 return ''
242 else:
243 space_sub = space_sub_simple
245 def space_subber(match):
246 """ Space|Comment subber """
247 if match.lastindex:
248 if match.group(1).endswith(r'\*/'):
249 if in_macie5[0]:
250 return ''
251 in_macie5[0] = 1
252 return r'/*\*/'
253 elif in_macie5[0]:
254 in_macie5[0] = 0
255 return '/**/'
256 return ''
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):
266 """ ; handler """
267 return ';' + space_sub(space_subber, group(7))
269 def fn_semicolon2(group):
270 """ ; handler """
271 if in_rule[0]:
272 return space_sub(space_subber, group(7))
273 return ';' + space_sub(space_subber, group(7))
275 def fn_open(_):
276 """ { handler """
277 if at_group[0]:
278 at_group[0] -= 1
279 else:
280 in_rule[0] = 1
281 return '{'
283 def fn_close(_):
284 """ } handler """
285 in_rule[0] = 0
286 return '}'
288 def fn_at_group(group):
289 """ @xxx group handler """
290 at_group[0] += 1
291 return group(13)
293 def fn_ie7hack(group):
294 """ IE7 Hack handler """
295 if not in_rule[0] and not at_group[0]:
296 in_macie5[0] = 0
297 return group(14) + space_sub(space_subber, group(15))
298 return '>' + space_sub(space_subber, group(15))
300 table = (
301 # noqa pylint: disable = bad-continuation
303 None,
304 None,
305 None,
306 None,
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
312 fn_open, # {
313 fn_close, # }
314 lambda g: g(11), # string
315 lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
316 # url(...)
317 fn_at_group, # @xxx expecting {...}
318 None,
319 fn_ie7hack, # ie7hack
320 None,
321 lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
322 # :first-line|letter followed
323 # by [{,] (apparently space
324 # needed for IE6)
325 lambda g: nl_unesc_sub('', g(18)), # nl_string
326 lambda g: post_esc_sub(' ', g(19)), # escape
329 def func(match):
330 """ Main subber """
331 idx, group = match.lastindex, match.group
332 if idx > 3:
333 return table[idx](group)
335 # shortcuts for frequent operations below:
336 elif idx == 1: # not interesting
337 return group(1)
338 # else: # space with token before or at the beginning
339 return space_sub(space_subber, group(idx))
341 return func
343 def cssmin(style, keep_bang_comments=False):
345 Minify CSS.
347 :Parameters:
348 `style` : ``str``
349 CSS to minify
351 `keep_bang_comments` : ``bool``
352 Keep comments starting with an exclamation mark? (``/*!...*/``)
354 :Return: Minified style
355 :Rtype: ``str``
357 # pylint: disable = redefined-outer-name
359 is_bytes, style = _as_str(style)
360 style = main_sub(main_subber(keep_bang_comments), style)
361 if is_bytes:
362 return style.encode('latin-1')
363 return style
365 return cssmin
367 cssmin = _make_cssmin()
370 def _as_str(style):
371 """ Make sure the style is a text string """
372 is_bytes = False
373 if str is bytes:
374 if not isinstance(style, basestring): # noqa pylint: disable = undefined-variable
375 raise TypeError("Unexpected type")
376 elif isinstance(style, (bytes, bytearray)):
377 is_bytes = True
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__':
386 def main():
387 """ Main """
388 import sys as _sys
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)
398 else:
399 xcssmin = cssmin
400 _sys.stdout.write(xcssmin(
401 _sys.stdin.read(), keep_bang_comments=keep_bang_comments
404 main()