author | Giulio Cesare Solaroli <giulio.cesare@solaroli.it> | 2011-10-03 16:04:12 (UTC) |
---|---|---|
committer | Giulio Cesare Solaroli <giulio.cesare@solaroli.it> | 2011-10-03 16:04:12 (UTC) |
commit | 541bb378ddece2eab135a8066a16994e94436dea (patch) (unidiff) | |
tree | ff160ea3e26f7fe07fcfd401387c5a0232ca715e /scripts/builder/cssmin.py | |
parent | 1bf431fd3d45cbdf4afa3e12afefe5d24f4d3bc7 (diff) | |
parent | ecad5e895831337216544e81f1a467e0c68c4a6a (diff) | |
download | clipperz-541bb378ddece2eab135a8066a16994e94436dea.zip clipperz-541bb378ddece2eab135a8066a16994e94436dea.tar.gz clipperz-541bb378ddece2eab135a8066a16994e94436dea.tar.bz2 |
Merge pull request #1 from gcsolaroli/master
First version of the restructured repository
-rw-r--r-- | scripts/builder/cssmin.py | 223 |
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 | |||
7 | from StringIO import StringIO # The pure-Python StringIO supports unicode. | ||
8 | import re | ||
9 | |||
10 | |||
11 | __version__ = '0.1.4' | ||
12 | |||
13 | |||
14 | def 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 | |||
48 | def 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 | |||
91 | def remove_unnecessary_semicolons(css): | ||
92 | """Remove unnecessary semicolons.""" | ||
93 | |||
94 | return re.sub(r";+\}", "}", css) | ||
95 | |||
96 | |||
97 | def remove_empty_rules(css): | ||
98 | """Remove empty rules.""" | ||
99 | |||
100 | return re.sub(r"[^\}\{]+\{\}", "", css) | ||
101 | |||
102 | |||
103 | def 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 | |||
116 | def 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 | |||
122 | def 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 | |||
135 | def 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 | |||
141 | def 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 | |||
157 | def condense_whitespace(css): | ||
158 | """Condense multiple adjacent whitespace characters into one.""" | ||
159 | |||
160 | return re.sub(r"\s+", " ", css) | ||
161 | |||
162 | |||
163 | def condense_semicolons(css): | ||
164 | """Condense multiple adjacent semicolon characters into one.""" | ||
165 | |||
166 | return re.sub(r";;+", ";", css) | ||
167 | |||
168 | |||
169 | def 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 | |||
185 | def 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 | |||
205 | def 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 | |||
222 | if __name__ == '__main__': | ||
223 | main() | ||