├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── setup.py └── src └── cssmin.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *.pyo 4 | .DS_Store 5 | MANIFEST 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | `cssmin.py` - A Python port of the YUI CSS compressor. 2 | 3 | Copyright (c) 2010 Zachary Voase 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | ------------------------------------------------------------------------------- 27 | 28 | This software contains portions of the YUI CSS Compressor, notably some regular 29 | expressions for reducing the size of CSS. The YUI Compressor source code can be 30 | found at , and is licensed as follows: 31 | 32 | > YUI Compressor Copyright License Agreement (BSD License) 33 | > 34 | > Copyright (c) 2009, Yahoo! Inc. 35 | > All rights reserved. 36 | > 37 | > Redistribution and use of this software in source and binary forms, 38 | > with or without modification, are permitted provided that the following 39 | > conditions are met: 40 | > 41 | > * Redistributions of source code must retain the above 42 | > copyright notice, this list of conditions and the 43 | > following disclaimer. 44 | > 45 | > * Redistributions in binary form must reproduce the above 46 | > copyright notice, this list of conditions and the 47 | > following disclaimer in the documentation and/or other 48 | > materials provided with the distribution. 49 | > 50 | > * Neither the name of Yahoo! Inc. nor the names of its 51 | > contributors may be used to endorse or promote products 52 | > derived from this software without specific prior 53 | > written permission of Yahoo! Inc. 54 | > 55 | > THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 56 | > AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 57 | > IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 58 | > DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 59 | > FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 60 | > DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 61 | > SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 62 | > CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 63 | > OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 64 | > OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 65 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | global-exclude .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cssmin` 2 | 3 | This is a Python port of the [YUI CSS Compressor][yuicompressor]. Install it: 4 | 5 | [yuicompressor]: http://developer.yahoo.com/yui/compressor/ 6 | 7 | $ easy_install cssmin # OR 8 | $ pip install cssmin 9 | 10 | Use it from the command-line: 11 | 12 | $ cssmin --help 13 | $ cat file1.css file2.css file3.css | cssmin > output.min.css 14 | $ cssmin --wrap 1000 < input.css > output.css 15 | 16 | Or use it from Python: 17 | 18 | >>> import cssmin 19 | >>> output = cssmin.cssmin(open('input.css').read()) 20 | >>> print output 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | 7 | setup( 8 | name = 'cssmin', 9 | version = '0.2.0', 10 | author = "Zachary Voase", 11 | author_email = "zacharyvoase@me.com", 12 | url = 'http://github.com/zacharyvoase/cssmin', 13 | description = "A Python port of the YUI CSS compression algorithm.", 14 | py_modules = ['cssmin'], 15 | package_dir = {'': 'src'}, 16 | entry_points = {'console_scripts': ['cssmin = cssmin:main']}, 17 | ) 18 | -------------------------------------------------------------------------------- /src/cssmin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """`cssmin` - A Python port of the YUI CSS compressor.""" 5 | 6 | try: 7 | from StringIO import StringIO # The pure-Python StringIO supports unicode. 8 | except ImportError: 9 | from io import StringIO 10 | import re 11 | 12 | 13 | __version__ = '0.2.0' 14 | 15 | 16 | def remove_comments(css): 17 | """Remove all CSS comment blocks.""" 18 | 19 | iemac = False 20 | preserve = False 21 | comment_start = css.find("/*") 22 | while comment_start >= 0: 23 | # Preserve comments that look like `/*!...*/`. 24 | # Slicing is used to make sure we don"t get an IndexError. 25 | preserve = css[comment_start + 2:comment_start + 3] == "!" 26 | 27 | comment_end = css.find("*/", comment_start + 2) 28 | if comment_end < 0: 29 | if not preserve: 30 | css = css[:comment_start] 31 | break 32 | elif comment_end >= (comment_start + 2): 33 | if css[comment_end - 1] == "\\": 34 | # This is an IE Mac-specific comment; leave this one and the 35 | # following one alone. 36 | comment_start = comment_end + 2 37 | iemac = True 38 | elif iemac: 39 | comment_start = comment_end + 2 40 | iemac = False 41 | elif not preserve: 42 | css = css[:comment_start] + css[comment_end + 2:] 43 | else: 44 | comment_start = comment_end + 2 45 | comment_start = css.find("/*", comment_start) 46 | 47 | return css 48 | 49 | 50 | def remove_unnecessary_whitespace(css): 51 | """Remove unnecessary whitespace characters.""" 52 | 53 | def pseudoclasscolon(css): 54 | 55 | """ 56 | Prevents 'p :link' from becoming 'p:link'. 57 | 58 | Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is 59 | translated back again later. 60 | """ 61 | 62 | regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)") 63 | match = regex.search(css) 64 | while match: 65 | css = ''.join([ 66 | css[:match.start()], 67 | match.group().replace(":", "___PSEUDOCLASSCOLON___"), 68 | css[match.end():]]) 69 | match = regex.search(css) 70 | return css 71 | 72 | css = pseudoclasscolon(css) 73 | # Remove spaces from before things. 74 | css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css) 75 | 76 | # If there is a `@charset`, then only allow one, and move to the beginning. 77 | css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css) 78 | css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css) 79 | 80 | # Put the space back in for a few cases, such as `@media screen` and 81 | # `(-webkit-min-device-pixel-ratio:0)`. 82 | css = re.sub(r"\band\(", "and (", css) 83 | 84 | # Put the colons back. 85 | css = css.replace('___PSEUDOCLASSCOLON___', ':') 86 | 87 | # Remove spaces from after things. 88 | css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css) 89 | 90 | return css 91 | 92 | 93 | def remove_unnecessary_semicolons(css): 94 | """Remove unnecessary semicolons.""" 95 | 96 | return re.sub(r";+\}", "}", css) 97 | 98 | 99 | def remove_empty_rules(css): 100 | """Remove empty rules.""" 101 | 102 | return re.sub(r"[^\}\{]+\{\}", "", css) 103 | 104 | 105 | def normalize_rgb_colors_to_hex(css): 106 | """Convert `rgb(51,102,153)` to `#336699`.""" 107 | 108 | regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)") 109 | match = regex.search(css) 110 | while match: 111 | colors = map(lambda s: s.strip(), match.group(1).split(",")) 112 | hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors)) 113 | css = css.replace(match.group(), hexcolor) 114 | match = regex.search(css) 115 | return css 116 | 117 | 118 | def condense_zero_units(css): 119 | """Replace `0(px, em, %, etc)` with `0`.""" 120 | 121 | return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css) 122 | 123 | 124 | def condense_multidimensional_zeros(css): 125 | """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`.""" 126 | 127 | css = css.replace(":0 0 0 0;", ":0;") 128 | css = css.replace(":0 0 0;", ":0;") 129 | css = css.replace(":0 0;", ":0;") 130 | 131 | # Revert `background-position:0;` to the valid `background-position:0 0;`. 132 | css = css.replace("background-position:0;", "background-position:0 0;") 133 | 134 | return css 135 | 136 | 137 | def condense_floating_points(css): 138 | """Replace `0.6` with `.6` where possible.""" 139 | 140 | return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css) 141 | 142 | 143 | def condense_hex_colors(css): 144 | """Shorten colors from #AABBCC to #ABC where possible.""" 145 | 146 | 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])") 147 | match = regex.search(css) 148 | while match: 149 | first = match.group(3) + match.group(5) + match.group(7) 150 | second = match.group(4) + match.group(6) + match.group(8) 151 | if first.lower() == second.lower(): 152 | css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first) 153 | match = regex.search(css, match.end() - 3) 154 | else: 155 | match = regex.search(css, match.end()) 156 | return css 157 | 158 | 159 | def condense_whitespace(css): 160 | """Condense multiple adjacent whitespace characters into one.""" 161 | 162 | return re.sub(r"\s+", " ", css) 163 | 164 | 165 | def condense_semicolons(css): 166 | """Condense multiple adjacent semicolon characters into one.""" 167 | 168 | return re.sub(r";;+", ";", css) 169 | 170 | 171 | def wrap_css_lines(css, line_length): 172 | """Wrap the lines of the given CSS to an approximate length.""" 173 | 174 | lines = [] 175 | line_start = 0 176 | for i, char in enumerate(css): 177 | # It's safe to break after `}` characters. 178 | if char == '}' and (i - line_start >= line_length): 179 | lines.append(css[line_start:i + 1]) 180 | line_start = i + 1 181 | 182 | if line_start < len(css): 183 | lines.append(css[line_start:]) 184 | return '\n'.join(lines) 185 | 186 | 187 | def cssmin(css, wrap=None): 188 | css = remove_comments(css) 189 | css = condense_whitespace(css) 190 | # A pseudo class for the Box Model Hack 191 | # (see http://tantek.com/CSS/Examples/boxmodelhack.html) 192 | css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___") 193 | css = remove_unnecessary_whitespace(css) 194 | css = remove_unnecessary_semicolons(css) 195 | css = condense_zero_units(css) 196 | css = condense_multidimensional_zeros(css) 197 | css = condense_floating_points(css) 198 | css = normalize_rgb_colors_to_hex(css) 199 | css = condense_hex_colors(css) 200 | if wrap is not None: 201 | css = wrap_css_lines(css, wrap) 202 | css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""') 203 | css = condense_semicolons(css) 204 | return css.strip() 205 | 206 | 207 | def main(): 208 | import optparse 209 | import sys 210 | 211 | p = optparse.OptionParser( 212 | prog="cssmin", version=__version__, 213 | usage="%prog [--wrap N]", 214 | description="""Reads raw CSS from stdin, and writes compressed CSS to stdout.""") 215 | 216 | p.add_option( 217 | '-w', '--wrap', type='int', default=None, metavar='N', 218 | help="Wrap output to approximately N chars per line.") 219 | 220 | options, args = p.parse_args() 221 | sys.stdout.write(cssmin(sys.stdin.read(), wrap=options.wrap)) 222 | 223 | 224 | if __name__ == '__main__': 225 | main() 226 | --------------------------------------------------------------------------------