summaryrefslogtreecommitdiff
path: root/scripts/builder/cssmin.py
Unidiff
Diffstat (limited to 'scripts/builder/cssmin.py') (more/less context) (ignore whitespace changes)
-rw-r--r--scripts/builder/cssmin.py223
1 files changed, 223 insertions, 0 deletions
diff --git a/scripts/builder/cssmin.py b/scripts/builder/cssmin.py
new file mode 100644
index 0000000..32ddf77
--- a/dev/null
+++ b/scripts/builder/cssmin.py
@@ -0,0 +1,223 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""`cssmin` - A Python port of the YUI CSS compressor."""
5
6
7from StringIO import StringIO # The pure-Python StringIO supports unicode.
8import re
9
10
11__version__ = '0.1.4'
12
13
14def remove_comments(css):
15 """Remove all CSS comment blocks."""
16
17 iemac = False
18 preserve = False
19 comment_start = css.find("/*")
20 while comment_start >= 0:
21 # Preserve comments that look like `/*!...*/`.
22 # Slicing is used to make sure we don"t get an IndexError.
23 preserve = css[comment_start + 2:comment_start + 3] == "!"
24
25 comment_end = css.find("*/", comment_start + 2)
26 if comment_end < 0:
27 if not preserve:
28 css = css[:comment_start]
29 break
30 elif comment_end >= (comment_start + 2):
31 if css[comment_end - 1] == "\\":
32 # This is an IE Mac-specific comment; leave this one and the
33 # following one alone.
34 comment_start = comment_end + 2
35 iemac = True
36 elif iemac:
37 comment_start = comment_end + 2
38 iemac = False
39 elif not preserve:
40 css = css[:comment_start] + css[comment_end + 2:]
41 else:
42 comment_start = comment_end + 2
43 comment_start = css.find("/*", comment_start)
44
45 return css
46
47
48def remove_unnecessary_whitespace(css):
49 """Remove unnecessary whitespace characters."""
50
51 def pseudoclasscolon(css):
52
53 """
54 Prevents 'p :link' from becoming 'p:link'.
55
56 Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is
57 translated back again later.
58 """
59
60 regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)")
61 match = regex.search(css)
62 while match:
63 css = ''.join([
64 css[:match.start()],
65 match.group().replace(":", "___PSEUDOCLASSCOLON___"),
66 css[match.end():]])
67 match = regex.search(css)
68 return css
69
70 css = pseudoclasscolon(css)
71 # Remove spaces from before things.
72 css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css)
73
74 # If there is a `@charset`, then only allow one, and move to the beginning.
75 css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css)
76 css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css)
77
78 # Put the space back in for a few cases, such as `@media screen` and
79 # `(-webkit-min-device-pixel-ratio:0)`.
80 css = re.sub(r"\band\(", "and (", css)
81
82 # Put the colons back.
83 css = css.replace('___PSEUDOCLASSCOLON___', ':')
84
85 # Remove spaces from after things.
86 css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css)
87
88 return css
89
90
91def remove_unnecessary_semicolons(css):
92 """Remove unnecessary semicolons."""
93
94 return re.sub(r";+\}", "}", css)
95
96
97def remove_empty_rules(css):
98 """Remove empty rules."""
99
100 return re.sub(r"[^\}\{]+\{\}", "", css)
101
102
103def normalize_rgb_colors_to_hex(css):
104 """Convert `rgb(51,102,153)` to `#336699`."""
105
106 regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)")
107 match = regex.search(css)
108 while match:
109 colors = map(lambda s: s.strip(), match.group(1).split(","))
110 hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors))
111 css = css.replace(match.group(), hexcolor)
112 match = regex.search(css)
113 return css
114
115
116def condense_zero_units(css):
117 """Replace `0(px, em, %, etc)` with `0`."""
118
119 return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css)
120
121
122def condense_multidimensional_zeros(css):
123 """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
124
125 css = css.replace(":0 0 0 0;", ":0;")
126 css = css.replace(":0 0 0;", ":0;")
127 css = css.replace(":0 0;", ":0;")
128
129 # Revert `background-position:0;` to the valid `background-position:0 0;`.
130 css = css.replace("background-position:0;", "background-position:0 0;")
131
132 return css
133
134
135def condense_floating_points(css):
136 """Replace `0.6` with `.6` where possible."""
137
138 return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
139
140
141def condense_hex_colors(css):
142 """Shorten colors from #AABBCC to #ABC where possible."""
143
144 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])")
145 match = regex.search(css)
146 while match:
147 first = match.group(3) + match.group(5) + match.group(7)
148 second = match.group(4) + match.group(6) + match.group(8)
149 if first.lower() == second.lower():
150 css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first)
151 match = regex.search(css, match.end() - 3)
152 else:
153 match = regex.search(css, match.end())
154 return css
155
156
157def condense_whitespace(css):
158 """Condense multiple adjacent whitespace characters into one."""
159
160 return re.sub(r"\s+", " ", css)
161
162
163def condense_semicolons(css):
164 """Condense multiple adjacent semicolon characters into one."""
165
166 return re.sub(r";;+", ";", css)
167
168
169def wrap_css_lines(css, line_length):
170 """Wrap the lines of the given CSS to an approximate length."""
171
172 lines = []
173 line_start = 0
174 for i, char in enumerate(css):
175 # It's safe to break after `}` characters.
176 if char == '}' and (i - line_start >= line_length):
177 lines.append(css[line_start:i + 1])
178 line_start = i + 1
179
180 if line_start < len(css):
181 lines.append(css[line_start:])
182 return '\n'.join(lines)
183
184
185def cssmin(css, wrap=None):
186 css = remove_comments(css)
187 css = condense_whitespace(css)
188 # A pseudo class for the Box Model Hack
189 # (see http://tantek.com/CSS/Examples/boxmodelhack.html)
190 css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___")
191 css = remove_unnecessary_whitespace(css)
192 css = remove_unnecessary_semicolons(css)
193 css = condense_zero_units(css)
194 css = condense_multidimensional_zeros(css)
195 css = condense_floating_points(css)
196 css = normalize_rgb_colors_to_hex(css)
197 css = condense_hex_colors(css)
198 if wrap is not None:
199 css = wrap_css_lines(css, wrap)
200 css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""')
201 css = condense_semicolons(css)
202 return css.strip()
203
204
205def main():
206 import optparse
207 import sys
208
209 p = optparse.OptionParser(
210 prog="cssmin", version=__version__,
211 usage="%prog [--wrap N]",
212 description="""Reads raw CSS from stdin, and writes compressed CSS to stdout.""")
213
214 p.add_option(
215 '-w', '--wrap', type='int', default=None, metavar='N',
216 help="Wrap output to approximately N chars per line.")
217
218 options, args = p.parse_args()
219 sys.stdout.write(cssmin(sys.stdin.read(), wrap=options.wrap))
220
221
222if __name__ == '__main__':
223 main()