1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # `cssmin.py` - A Python port of the YUI CSS compressor.
5
+ #
6
+ # Copyright (c) 2010 Zachary Voase
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person
9
+ # obtaining a copy of this software and associated documentation
10
+ # files (the "Software"), to deal in the Software without
11
+ # restriction, including without limitation the rights to use,
12
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the
14
+ # Software is furnished to do so, subject to the following
15
+ # conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ # OTHER DEALINGS IN THE SOFTWARE.
28
+ #
29
+ """`cssmin` - A Python port of the YUI CSS compressor."""
30
+
31
+
32
+ from StringIO import StringIO # The pure-Python StringIO supports unicode.
33
+ import re
34
+
35
+
36
+ __version__ = '0.1.4'
37
+
38
+
39
+ def remove_comments (css ):
40
+ """Remove all CSS comment blocks."""
41
+
42
+ iemac = False
43
+ preserve = False
44
+ comment_start = css .find ("/*" )
45
+ while comment_start >= 0 :
46
+ # Preserve comments that look like `/*!...*/`.
47
+ # Slicing is used to make sure we don"t get an IndexError.
48
+ preserve = css [comment_start + 2 :comment_start + 3 ] == "!"
49
+
50
+ comment_end = css .find ("*/" , comment_start + 2 )
51
+ if comment_end < 0 :
52
+ if not preserve :
53
+ css = css [:comment_start ]
54
+ break
55
+ elif comment_end >= (comment_start + 2 ):
56
+ if css [comment_end - 1 ] == "\\ " :
57
+ # This is an IE Mac-specific comment; leave this one and the
58
+ # following one alone.
59
+ comment_start = comment_end + 2
60
+ iemac = True
61
+ elif iemac :
62
+ comment_start = comment_end + 2
63
+ iemac = False
64
+ elif not preserve :
65
+ css = css [:comment_start ] + css [comment_end + 2 :]
66
+ else :
67
+ comment_start = comment_end + 2
68
+ comment_start = css .find ("/*" , comment_start )
69
+
70
+ return css
71
+
72
+
73
+ def remove_unnecessary_whitespace (css ):
74
+ """Remove unnecessary whitespace characters."""
75
+
76
+ def pseudoclasscolon (css ):
77
+
78
+ """
79
+ Prevents 'p :link' from becoming 'p:link'.
80
+
81
+ Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is
82
+ translated back again later.
83
+ """
84
+
85
+ regex = re .compile (r"(^|\})(([^\{\:])+\:)+([^\{]*\{)" )
86
+ match = regex .search (css )
87
+ while match :
88
+ css = '' .join ([
89
+ css [:match .start ()],
90
+ match .group ().replace (":" , "___PSEUDOCLASSCOLON___" ),
91
+ css [match .end ():]])
92
+ match = regex .search (css )
93
+ return css
94
+
95
+ css = pseudoclasscolon (css )
96
+ # Remove spaces from before things.
97
+ css = re .sub (r"\s+([!{};:>+\(\)\],])" , r"\1" , css )
98
+
99
+ # If there is a `@charset`, then only allow one, and move to the beginning.
100
+ css = re .sub (r"^(.*)(@charset \"[^\"]*\";)" , r"\2\1" , css )
101
+ css = re .sub (r"^(\s*@charset [^;]+;\s*)+" , r"\1" , css )
102
+
103
+ # Put the space back in for a few cases, such as `@media screen` and
104
+ # `(-webkit-min-device-pixel-ratio:0)`.
105
+ css = re .sub (r"\band\(" , "and (" , css )
106
+
107
+ # Put the colons back.
108
+ css = css .replace ('___PSEUDOCLASSCOLON___' , ':' )
109
+
110
+ # Remove spaces from after things.
111
+ css = re .sub (r"([!{}:;>+\(\[,])\s+" , r"\1" , css )
112
+
113
+ return css
114
+
115
+
116
+ def remove_unnecessary_semicolons (css ):
117
+ """Remove unnecessary semicolons."""
118
+
119
+ return re .sub (r";+\}" , "}" , css )
120
+
121
+
122
+ def remove_empty_rules (css ):
123
+ """Remove empty rules."""
124
+
125
+ return re .sub (r"[^\}\{]+\{\}" , "" , css )
126
+
127
+
128
+ def normalize_rgb_colors_to_hex (css ):
129
+ """Convert `rgb(51,102,153)` to `#336699`."""
130
+
131
+ regex = re .compile (r"rgb\s*\(\s*([0-9,\s]+)\s*\)" )
132
+ match = regex .search (css )
133
+ while match :
134
+ colors = map (lambda s : s .strip (), match .group (1 ).split ("," ))
135
+ hexcolor = '#%.2x%.2x%.2x' % tuple (map (int , colors ))
136
+ css = css .replace (match .group (), hexcolor )
137
+ match = regex .search (css )
138
+ return css
139
+
140
+
141
+ def condense_zero_units (css ):
142
+ """Replace `0(px, em, %, etc)` with `0`."""
143
+
144
+ return re .sub (r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)" , r"\1\2" , css )
145
+
146
+
147
+ def condense_multidimensional_zeros (css ):
148
+ """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
149
+
150
+ css = css .replace (":0 0 0 0;" , ":0;" )
151
+ css = css .replace (":0 0 0;" , ":0;" )
152
+ css = css .replace (":0 0;" , ":0;" )
153
+
154
+ # Revert `background-position:0;` to the valid `background-position:0 0;`.
155
+ css = css .replace ("background-position:0;" , "background-position:0 0;" )
156
+
157
+ return css
158
+
159
+
160
+ def condense_floating_points (css ):
161
+ """Replace `0.6` with `.6` where possible."""
162
+
163
+ return re .sub (r"(:|\s)0+\.(\d+)" , r"\1.\2" , css )
164
+
165
+
166
+ def condense_hex_colors (css ):
167
+ """Shorten colors from #AABBCC to #ABC where possible."""
168
+
169
+ regex = re .compile (r"([^\"'=\s])(\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])" )
170
+ match = regex .search (css )
171
+ while match :
172
+ first = match .group (3 ) + match .group (5 ) + match .group (7 )
173
+ second = match .group (4 ) + match .group (6 ) + match .group (8 )
174
+ if first .lower () == second .lower ():
175
+ css = css .replace (match .group (), match .group (1 ) + match .group (2 ) + '#' + first )
176
+ match = regex .search (css , match .end () - 3 )
177
+ else :
178
+ match = regex .search (css , match .end ())
179
+ return css
180
+
181
+
182
+ def condense_whitespace (css ):
183
+ """Condense multiple adjacent whitespace characters into one."""
184
+
185
+ return re .sub (r"\s+" , " " , css )
186
+
187
+
188
+ def condense_semicolons (css ):
189
+ """Condense multiple adjacent semicolon characters into one."""
190
+
191
+ return re .sub (r";;+" , ";" , css )
192
+
193
+
194
+ def wrap_css_lines (css , line_length ):
195
+ """Wrap the lines of the given CSS to an approximate length."""
196
+
197
+ lines = []
198
+ line_start = 0
199
+ for i , char in enumerate (css ):
200
+ # It's safe to break after `}` characters.
201
+ if char == '}' and (i - line_start >= line_length ):
202
+ lines .append (css [line_start :i + 1 ])
203
+ line_start = i + 1
204
+
205
+ if line_start < len (css ):
206
+ lines .append (css [line_start :])
207
+ return '\n ' .join (lines )
208
+
209
+
210
+ def cssmin (css , wrap = None ):
211
+ css = remove_comments (css )
212
+ css = condense_whitespace (css )
213
+ # A pseudo class for the Box Model Hack
214
+ # (see http://tantek.com/CSS/Examples/boxmodelhack.html)
215
+ css = css .replace ('"\\ "}\\ ""' , "___PSEUDOCLASSBMH___" )
216
+ css = remove_unnecessary_whitespace (css )
217
+ css = remove_unnecessary_semicolons (css )
218
+ css = condense_zero_units (css )
219
+ css = condense_multidimensional_zeros (css )
220
+ css = condense_floating_points (css )
221
+ css = normalize_rgb_colors_to_hex (css )
222
+ css = condense_hex_colors (css )
223
+ if wrap is not None :
224
+ css = wrap_css_lines (css , wrap )
225
+ css = css .replace ("___PSEUDOCLASSBMH___" , '"\\ "}\\ ""' )
226
+ css = condense_semicolons (css )
227
+ return css .strip ()
228
+
229
+
230
+ def main ():
231
+ import optparse
232
+ import sys
233
+
234
+ p = optparse .OptionParser (
235
+ prog = "cssmin" , version = __version__ ,
236
+ usage = "%prog [--wrap N]" ,
237
+ description = """Reads raw CSS from stdin, and writes compressed CSS to stdout.""" )
238
+
239
+ p .add_option (
240
+ '-w' , '--wrap' , type = 'int' , default = None , metavar = 'N' ,
241
+ help = "Wrap output to approximately N chars per line." )
242
+
243
+ options , args = p .parse_args ()
244
+ sys .stdout .write (cssmin (sys .stdin .read (), wrap = options .wrap ))
245
+
246
+
247
+ if __name__ == '__main__' :
248
+ main ()
0 commit comments