Skip to content

Commit 4caa91f

Browse files
committed
Fixed django-compressor#28 -- Improved CssMinFilter by including cssmin (which prevents local imports).
1 parent dbaf995 commit 4caa91f

File tree

3 files changed

+264
-7
lines changed

3 files changed

+264
-7
lines changed
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
from compressor.filters import FilterBase, FilterError
2+
from compressor.filters.cssmin.cssmin import cssmin
23

34
class CSSMinFilter(FilterBase):
45
"""
56
A filter that utilizes Zachary Voase's Python port of
67
the YUI CSS compression algorithm: http://pypi.python.org/pypi/cssmin/
78
"""
89
def output(self, **kwargs):
9-
try:
10-
import cssmin
11-
except ImportError, e:
12-
if self.verbose:
13-
raise FilterError('Failed to import cssmin: %s' % e)
14-
return self.content
15-
return cssmin.cssmin(self.content)
10+
return cssmin(self.content)

compressor/filters/cssmin/cssmin.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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()

tests/core/tests.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,20 @@ def test_avoid_reordering_css(self):
200200
self.assertEqual(media, [l.get('media', None) for l in links])
201201

202202

203+
class CssMinTestCase(TestCase):
204+
def test_cssmin_filter(self):
205+
from compressor.filters.cssmin import CSSMinFilter
206+
content = """p {
207+
208+
209+
background: rgb(51,102,153) url(http://github.com/'../../images/image.gif');
210+
211+
212+
}
213+
"""
214+
output = "p{background:#369 url(http://github.com/'../../images/image.gif')}"
215+
self.assertEqual(output, CSSMinFilter(content).output())
216+
203217
def render(template_string, context_dict=None):
204218
"""A shortcut for testing template output."""
205219
if context_dict is None:

0 commit comments

Comments
 (0)