Replace cssmin by csscompressor. ref #664
This commit is contained in:
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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()
|
@@ -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):
|
||||
|
@@ -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/
|
||||
|
@@ -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/
|
||||
|
||||
|
@@ -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
|
||||
|
4
tox.ini
4
tox.ini
@@ -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,
|
||||
|
Reference in New Issue
Block a user