Replace cssmin by csscompressor. ref #664

This commit is contained in:
Johannes Linke
2015-12-04 21:09:58 +01:00
parent 21d90f684b
commit 7eca920548
8 changed files with 34 additions and 261 deletions

View File

@@ -53,7 +53,7 @@ write a custom parser.
Django Compressor also comes with built-in support for
`YUI CSS and JS`_ compressor, `yUglify CSS and JS`_ compressor, the Google's
`Closure Compiler`_, a Python port of Douglas Crockford's JSmin_, a Python port
of the YUI CSS Compressor cssmin_ and a filter to convert (some) images into
of the YUI CSS Compressor csscompressor_ and a filter to convert (some) images into
`data URIs`_.
If your setup requires a different compressor or other post-processing
@@ -76,7 +76,7 @@ The in-development version of Django Compressor can be installed with
.. _yUglify CSS and JS: https://github.com/yui/yuglify
.. _Closure Compiler: http://code.google.com/closure/compiler/
.. _JSMin: http://www.crockford.com/javascript/jsmin.html
.. _cssmin: https://github.com/zacharyvoase/cssmin
.. _csscompressor: https://github.com/sprymix/csscompressor
.. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme
.. _django-compressor.readthedocs.org: http://django-compressor.readthedocs.org/en/latest/
.. _github.com/django-compressor/django-compressor: https://github.com/django-compressor/django-compressor

View File

@@ -1,12 +1,13 @@
from compressor.filters import CallbackOutputFilter
class CSSMinFilter(CallbackOutputFilter):
class CSSCompressorFilter(CallbackOutputFilter):
"""
A filter that utilizes Zachary Voase's Python port of
the YUI CSS compression algorithm: http://pypi.python.org/pypi/cssmin/
A filter that utilizes Yury Selivanov's Python port of
the YUI CSS compression algorithm: https://pypi.python.org/pypi/csscompressor
"""
callback = "compressor.filters.cssmin.cssmin.cssmin"
callback = "csscompressor.compress"
dependencies = ["csscompressor"]
class rCSSMinFilter(CallbackOutputFilter):

View File

@@ -1,245 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# `cssmin.py` - A Python port of the YUI CSS compressor.
#
# Copyright (c) 2010 Zachary Voase
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
"""`cssmin` - A Python port of the YUI CSS compressor."""
import re
__version__ = '0.1.4'
def remove_comments(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(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(css):
"""Remove unnecessary semicolons."""
return re.sub(r";+\}", "}", css)
def remove_empty_rules(css):
"""Remove empty rules."""
return re.sub(r"[^\}\{]+\{\}", "", css)
def normalize_rgb_colors_to_hex(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 = map(lambda s: s.strip(), match.group(1).split(","))
hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors))
css = css.replace(match.group(), hexcolor)
match = regex.search(css)
return css
def condense_zero_units(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(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(css):
"""Replace `0.6` with `.6` where possible."""
return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
def condense_hex_colors(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(css):
"""Condense multiple adjacent whitespace characters into one."""
return re.sub(r"\s+", " ", css)
def condense_semicolons(css):
"""Condense multiple adjacent semicolon characters into one."""
return re.sub(r";;+", ";", css)
def wrap_css_lines(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 cssmin(css, wrap=None):
css = remove_comments(css)
css = condense_whitespace(css)
# A pseudo class for the Box Model Hack
# (see http://tantek.com/CSS/Examples/boxmodelhack.html)
css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___")
css = remove_unnecessary_whitespace(css)
css = remove_unnecessary_semicolons(css)
css = condense_zero_units(css)
css = condense_multidimensional_zeros(css)
css = condense_floating_points(css)
css = normalize_rgb_colors_to_hex(css)
css = condense_hex_colors(css)
if wrap is not None:
css = wrap_css_lines(css, wrap)
css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""')
css = condense_semicolons(css)
return css.strip()
def main():
import optparse
import sys
p = optparse.OptionParser(
prog="cssmin", version=__version__,
usage="%prog [--wrap N]",
description="""Reads raw CSS from stdin, and writes compressed CSS to stdout.""")
p.add_option(
'-w', '--wrap', type='int', default=None, metavar='N',
help="Wrap output to approximately N chars per line.")
options, args = p.parse_args()
sys.stdout.write(cssmin(sys.stdin.read(), wrap=options.wrap))
if __name__ == '__main__':
main()

View File

@@ -15,7 +15,7 @@ from compressor.conf import settings
from compressor.css import CssCompressor
from compressor.utils import find_command
from compressor.filters.base import CompilerFilter, CachedCompilerFilter
from compressor.filters.cssmin import CSSMinFilter, rCSSMinFilter
from compressor.filters.cssmin import CSSCompressorFilter, rCSSMinFilter
from compressor.filters.css_default import CssAbsoluteFilter
from compressor.filters.jsmin import JSMinFilter
from compressor.filters.template import TemplateFilter
@@ -127,8 +127,8 @@ class PrecompilerTestCase(TestCase):
self.assertEqual("", compiler.input())
class CssMinTestCase(TestCase):
def test_cssmin_filter(self):
class CSSCompressorTestCase(TestCase):
def test_csscompressor_filter(self):
content = """/*!
* django-compressor
* Copyright (c) 2009-2014 Django Compressor authors
@@ -141,8 +141,11 @@ class CssMinTestCase(TestCase):
}
"""
output = "/*!* django-compressor * Copyright(c) 2009-2014 Django Compressor authors */ p{background:#369 url('../../images/image.gif')}"
self.assertEqual(output, CSSMinFilter(content).output())
output = """/*!
* django-compressor
* Copyright (c) 2009-2014 Django Compressor authors
*/p{background:#369 url('../../images/image.gif')}"""
self.assertEqual(output, CSSCompressorFilter(content).output())
class rCssMinTestCase(TestCase):

View File

@@ -31,7 +31,7 @@ Installation
)
* Define :attr:`COMPRESS_ROOT <django.conf.settings.COMPRESS_ROOT>` in settings
if you don't have already ``STATIC_ROOT`` or if you want it in a different
if you don't have already ``STATIC_ROOT`` or if you want it in a different
folder.
.. _staticfiles: http://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
@@ -89,6 +89,13 @@ Optional
pip install slimit
- `csscompressor`_
For the :ref:`csscompressor filter <csscompressor_filter>`
``compressor.filters.cssmin.CSSCompressorFilter``::
pip install csscompressor
.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
.. _lxml: http://lxml.de/
.. _libxml2: http://xmlsoft.org/

View File

@@ -120,10 +120,12 @@ Backend settings
The arguments passed to the compressor. Defaults to --terminal.
- ``compressor.filters.cssmin.CSSMinFilter``
.. _csscompressor_filter:
A filter that uses Zachary Voase's Python port of the YUI CSS compression
algorithm cssmin_ (included, no external dependencies).
- ``compressor.filters.cssmin.CSSCompressorFilter``
A filter that uses Yury Selivanov's Python port of the YUI CSS compression
algorithm csscompressor_.
- ``compressor.filters.cssmin.rCSSMinFilter``
@@ -144,7 +146,7 @@ Backend settings
.. _`data: URIs`: http://en.wikipedia.org/wiki/Data_URI_scheme
.. _cssmin: http://pypi.python.org/pypi/cssmin/
.. _csscompressor: http://pypi.python.org/pypi/csscompressor/
.. _rCSSmin: http://opensource.perlig.de/rcssmin/
.. _`clean-css`: https://github.com/GoalSmashers/clean-css/

View File

@@ -10,3 +10,4 @@ coffin==0.4.0
jingo==0.7
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4

View File

@@ -12,6 +12,7 @@ two =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
two_six =
flake8==2.4.0
coverage==3.7.1
@@ -25,6 +26,7 @@ two_six =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
three =
flake8==2.4.0
coverage==3.7.1
@@ -37,6 +39,7 @@ three =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
three_two =
flake8==2.4.0
coverage==3.7.1
@@ -49,6 +52,7 @@ three_two =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
[tox]
envlist =
{py26,py27}-1.4.X,