Home | History | Annotate | Download | only in bench
      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 
     27 try:
     28     from StringIO import StringIO # The pure-Python StringIO supports unicode.
     29 except ImportError:
     30     from io import StringIO
     31 import re
     32 
     33 
     34 __version__ = '0.2.0'
     35 
     36 
     37 def 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 
     71 def 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 
    114 def remove_unnecessary_semicolons(css):
    115     """Remove unnecessary semicolons."""
    116 
    117     return re.sub(r";+\}", "}", css)
    118 
    119 
    120 def remove_empty_rules(css):
    121     """Remove empty rules."""
    122 
    123     return re.sub(r"[^\}\{]+\{\}", "", css)
    124 
    125 
    126 def 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 
    139 def 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 
    145 def 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 
    158 def 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 
    164 def 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 
    180 def condense_whitespace(css):
    181     """Condense multiple adjacent whitespace characters into one."""
    182 
    183     return re.sub(r"\s+", " ", css)
    184 
    185 
    186 def condense_semicolons(css):
    187     """Condense multiple adjacent semicolon characters into one."""
    188 
    189     return re.sub(r";;+", ";", css)
    190 
    191 
    192 def 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 
    208 def 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 
    228 def 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 
    245 if __name__ == '__main__':
    246     main()
    247