''' Created on 06.06.2011 @author: christian ''' import fnmatch from optparse import make_option import os import re from django.conf import settings from django.contrib.sites.models import Site from django.core.management.base import BaseCommand from django.utils.translation import ugettext as _ class Command(BaseCommand): ''' classdocs ''' can_import_settings = True help = _("Reads raw CSS from stdin, and writes compressed CSS to stdout.") option_list = BaseCommand.option_list + ( make_option('-w', '--wrap', type='int', default=None, metavar='N', help="Wrap output to approximately N chars per line."), ) def remove_comments(self, css): """Remove all CSS comment blocks.""" iemac = False preserve = False comment_start = css.find("/*") while comment_start >= 0: # Preserve comments that look like `/*!...*/`. # Slicing is used to make sure we don"t get an IndexError. preserve = css[comment_start + 2:comment_start + 3] == "!" comment_end = css.find("*/", comment_start + 2) if comment_end < 0: if not preserve: css = css[:comment_start] break elif comment_end >= (comment_start + 2): if css[comment_end - 1] == "\\": # This is an IE Mac-specific comment; leave this one # and the following one alone. comment_start = comment_end + 2 iemac = True elif iemac: comment_start = comment_end + 2 iemac = False elif not preserve: css = css[:comment_start] + css[comment_end + 2:] else: comment_start = comment_end + 2 comment_start = css.find("/*", comment_start) return css def remove_unnecessary_whitespace(self, css): """Remove unnecessary whitespace characters.""" def pseudoclasscolon(css): """ Prevents 'p :link' from becoming 'p:link'. Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is translated back again later. """ regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)") match = regex.search(css) while match: css = ''.join([ css[:match.start()], match.group().replace(":", "___PSEUDOCLASSCOLON___"), css[match.end():]]) match = regex.search(css) return css css = pseudoclasscolon(css) # Remove spaces from before things. css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css) # If there is a `@charset`, # then only allow one, and move to the beginning. css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css) css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css) # Put the space back in for a few cases, such as `@media screen` and # `(-webkit-min-device-pixel-ratio:0)`. css = re.sub(r"\band\(", "and (", css) # Put the colons back. css = css.replace('___PSEUDOCLASSCOLON___', ':') # Remove spaces from after things. css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css) return css def remove_unnecessary_semicolons(self, css): """Remove unnecessary semicolons.""" return re.sub(r";+\}", "}", css) def remove_empty_rules(self, css): """Remove empty rules.""" return re.sub(r"[^\}\{]+\{\}", "", css) def normalize_rgb_colors_to_hex(self, css): """Convert `rgb(51,102,153)` to `#336699`.""" regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)") match = regex.search(css) while match: colors = match.group(1).split(",") hexcolor = '#%.2x%.2x%.2x' % map(int, colors) css = css.replace(match.group(), hexcolor) match = regex.search(css) return css def condense_zero_units(self, css): """Replace `0(px, em, %, etc)` with `0`.""" return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css) def condense_multidimensional_zeros(self, css): """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`.""" css = css.replace(":0 0 0 0;", ":0;") css = css.replace(":0 0 0;", ":0;") css = css.replace(":0 0;", ":0;") # Revert `background-position:0;` to the valid `background-position:0 0;`. css = css.replace("background-position:0;", "background-position:0 0;") return css def condense_floating_points(self, css): """Replace `0.6` with `.6` where possible.""" return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css) def condense_hex_colors(self, css): """Shorten colors from #AABBCC to #ABC where possible.""" 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])") match = regex.search(css) while match: first = match.group(3) + match.group(5) + match.group(7) second = match.group(4) + match.group(6) + match.group(8) if first.lower() == second.lower(): css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first) match = regex.search(css, match.end() - 3) else: match = regex.search(css, match.end()) return css def condense_whitespace(self, css): """Condense multiple adjacent whitespace characters into one.""" return re.sub(r"\s+", " ", css) def condense_semicolons(self, css): """Condense multiple adjacent semicolon characters into one.""" return re.sub(r";;+", ";", css) def wrap_css_lines(self, css, line_length): """Wrap the lines of the given CSS to an approximate length.""" lines = [] line_start = 0 for i, char in enumerate(css): # It's safe to break after `}` characters. if char == '}' and (i - line_start >= line_length): lines.append(css[line_start:i + 1]) line_start = i + 1 if line_start < len(css): lines.append(css[line_start:]) return '\n'.join(lines) def compress(self, css, wrap=None): css = self.remove_comments(css) css = self.condense_whitespace(css) # A pseudo class for the Box Model Hack # (see http://tantek.com/CSS/Examples/boxmodelhack.html) css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___") css = self.remove_unnecessary_whitespace(css) css = self.remove_unnecessary_semicolons(css) css = self.condense_zero_units(css) css = self.condense_multidimensional_zeros(css) css = self.condense_floating_points(css) css = self.normalize_rgb_colors_to_hex(css) css = self.condense_hex_colors(css) if wrap is not None: css = self.wrap_css_lines(css, wrap) css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""') css = self.condense_semicolons(css) # A Hack for IE compatiblity # These crappy browser has issues with AND Statments in @MEDIA Blocks css = css.replace('and(', 'and (') return css.strip() def handle(self, *args, **options): CSS_ROOT = os.path.join(settings.STATICFILES_DIRS[0], 'css') for site in Site.objects.all(): css_input = os.path.join(CSS_ROOT, site.domain) css_output = '%s/%s.css' % (CSS_ROOT, site.domain.replace('.', '_')) print _("Compressing CSS for %s") % site.name print "Input Dir: %s" % css_input print "Output File: %s" % css_output try: os.makedirs(css_input) except OSError: pass css = '' """Read each .css file in the css_input directory, and append their content""" for file_name in fnmatch.filter(sorted(os.listdir(css_input)), '*.css'): print ' Adding: %s' % file_name with open(os.path.join(css_input, file_name), 'r') as css_file: css += css_file.read() with open(css_output, 'w') as css_file: css_file.write(self.compress(css))