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