Merge pull request #700 from karyon/deal_with_dependencies

Deal with dependencies
This commit is contained in:
Mathieu Pillard
2015-12-04 23:15:22 +01:00
19 changed files with 79 additions and 1542 deletions

View File

@@ -10,7 +10,7 @@ runtests:
coverage run --branch --source=compressor `which django-admin.py` test --settings=compressor.test_settings compressor
coveragereport:
coverage report --omit=compressor/test*,compressor/filters/jsmin/rjsmin*,compressor/filters/cssmin/cssmin*,compressor/utils/stringformat*
coverage report --omit=compressor/test*
test: flake8 runtests coveragereport

View File

@@ -50,10 +50,10 @@ default. As an alternative Django Compressor provides a BeautifulSoup_ and a
html5lib_ based parser, as well as an abstract base class that makes it easy to
write a custom parser.
Django Compressor also comes with built-in support for `CSS Tidy`_,
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
@@ -72,13 +72,11 @@ The in-development version of Django Compressor can be installed with
.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
.. _lxml: http://lxml.de/
.. _html5lib: http://code.google.com/p/html5lib/
.. _CSS Tidy: http://csstidy.sourceforge.net/
.. _YUI CSS and JS: http://developer.yahoo.com/yui/compressor/
.. _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

@@ -38,8 +38,6 @@ class CompressorConf(AppConf):
CACHEABLE_PRECOMPILERS = ()
CLOSURE_COMPILER_BINARY = 'java -jar compiler.jar'
CLOSURE_COMPILER_ARGUMENTS = ''
CSSTIDY_BINARY = 'csstidy'
CSSTIDY_ARGUMENTS = '--template=highest'
YUI_BINARY = 'java -jar yuicompressor.jar'
YUI_CSS_ARGUMENTS = ''
YUI_JS_ARGUMENTS = ''

View File

@@ -1,16 +1,18 @@
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):
callback = "compressor.filters.cssmin.rcssmin.cssmin"
callback = "rcssmin.cssmin"
dependencies = ["rcssmin"]
kwargs = {
"keep_bang_comments": True
}

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

@@ -1,374 +0,0 @@
#!/usr/bin/env python
# -*- coding: ascii -*-
r"""
==============
CSS Minifier
==============
CSS Minifier.
The minifier is based on the semantics of the `YUI compressor`_\\, which
itself is based on `the rule list by Isaac Schlueter`_\\.
:Copyright:
Copyright 2011 - 2015
Andr\xe9 Malo or his licensors, as applicable
:License:
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This module is a re-implementation aiming for speed instead of maximum
compression, so it can be used at runtime (rather than during a preprocessing
step). RCSSmin does syntactical compression only (removing spaces, comments
and possibly semicolons). It does not provide semantic compression (like
removing empty blocks, collapsing redundant properties etc). It does, however,
support various CSS hacks (by keeping them working as intended).
Here's a feature list:
- Strings are kept, except that escaped newlines are stripped
- Space/Comments before the very end or before various characters are
stripped: ``:{});=>],!`` (The colon (``:``) is a special case, a single
space is kept if it's outside a ruleset.)
- Space/Comments at the very beginning or after various characters are
stripped: ``{}(=:>[,!``
- Optional space after unicode escapes is kept, resp. replaced by a simple
space
- whitespaces inside ``url()`` definitions are stripped
- Comments starting with an exclamation mark (``!``) can be kept optionally.
- All other comments and/or whitespace characters are replaced by a single
space.
- Multiple consecutive semicolons are reduced to one
- The last semicolon within a ruleset is stripped
- CSS Hacks supported:
- IE7 hack (``>/**/``)
- Mac-IE5 hack (``/*\\*/.../**/``)
- The boxmodelhack is supported naturally because it relies on valid CSS2
strings
- Between ``:first-line`` and the following comma or curly brace a space is
inserted. (apparently it's needed for IE6)
- Same for ``:first-letter``
rcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to
factor 100 or so (depending on the input). docs/BENCHMARKS in the source
distribution contains the details.
Both python 2 (>= 2.4) and python 3 are supported.
.. _YUI compressor: https://github.com/yui/yuicompressor/
.. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/
"""
if __doc__:
# pylint: disable = W0622
__doc__ = __doc__.encode('ascii').decode('unicode_escape')
__author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape')
__docformat__ = "restructuredtext en"
__license__ = "Apache License, Version 2.0"
__version__ = '1.0.6'
__all__ = ['cssmin']
import re as _re
def _make_cssmin(python_only=False):
"""
Generate CSS minifier.
:Parameters:
`python_only` : ``bool``
Use only the python variant. If true, the c extension is not even
tried to be loaded.
:Return: Minifier
:Rtype: ``callable``
"""
# pylint: disable = R0912, R0914, W0612
if not python_only:
try:
import _rcssmin
except ImportError:
pass
else:
return _rcssmin.cssmin
nl = r'(?:[\n\f]|\r\n?)' # pylint: disable = C0103
spacechar = r'[\r\n\f\040\t]'
unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?'
escaped = r'[^\n\r\f0-9a-fA-F]'
escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals()
nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]'
# nmstart = r'[^\000-\100\133-\136\140\173-\177]'
# ident = (r'(?:'
# r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'
# r')') % locals()
comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
# only for specific purposes. The bang is grouped:
_bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
string1 = \
r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)'
string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")'
strings = r'(?:%s|%s)' % (string1, string2)
nl_string1 = \
r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)'
nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")'
nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2)
uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)'
uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")'
uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2)
nl_escaped = r'(?:\\%(nl)s)' % locals()
space = r'(?:%(spacechar)s|%(comment)s)' % locals()
ie7hack = r'(?:>/\*\*/)'
uri = (r'(?:'
# noqa pylint: disable = C0330
r'(?:[^\000-\040"\047()\\\177]*'
r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
r'(?:'
r'(?:%(spacechar)s+|%(nl_escaped)s+)'
r'(?:'
r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)'
r'[^\000-\040"\047()\\\177]*'
r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*'
r')+'
r')*'
r')') % locals()
nl_unesc_sub = _re.compile(nl_escaped).sub
uri_space_sub = _re.compile((
r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
) % locals()).sub
uri_space_subber = lambda m: m.groups()[0] or ''
space_sub_simple = _re.compile((
r'[\r\n\f\040\t;]+|(%(comment)s+)'
) % locals()).sub
space_sub_banged = _re.compile((
r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
) % locals()).sub
post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub
main_sub = _re.compile((
# noqa pylint: disable = C0330
r'([^\\"\047u>@\r\n\f\040\t/;:{}+]+)' # 1
r'|(?<=[{}(=:>[,!])(%(space)s+)' # 2
r'|^(%(space)s+)' # 3
r'|(%(space)s+)(?=(([:{});=>\],!])|$)?)' # 4, 5, 6
r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)' # 7, 8
r'|(\{)' # 9
r'|(\})' # 10
r'|(%(strings)s)' # 11
r'|(?<!%(nmchar)s)url\(%(spacechar)s*(' # 12
r'%(uri_nl_strings)s'
r'|%(uri)s'
r')%(spacechar)s*\)'
r'|(@(?:' # 13
r'[mM][eE][dD][iI][aA]'
r'|[sS][uU][pP][pP][oO][rR][tT][sS]'
r'|[dD][oO][cC][uU][mM][eE][nN][tT]'
r'|(?:-(?:'
r'[wW][eE][bB][kK][iI][tT]|[mM][oO][zZ]|[oO]|[mM][sS]'
r')-)?'
r'[kK][eE][yY][fF][rR][aA][mM][eE][sS]'
r'))(?!%(nmchar)s)'
r'|(%(ie7hack)s)(%(space)s*)' # 14, 15
r'|(:[fF][iI][rR][sS][tT]-[lL]' # 16
r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'
r'(%(space)s*)(?=[{,])' # 17
r'|(%(nl_strings)s)' # 18
r'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}+]*)' # 19
) % locals()).sub
# print main_sub.__self__.pattern
def main_subber(keep_bang_comments):
""" Make main subber """
in_macie5, in_rule, at_group = [0], [0], [0]
if keep_bang_comments:
space_sub = space_sub_banged
def space_subber(match):
""" Space|Comment subber """
if match.lastindex:
group1, group2 = match.group(1, 2)
if group2:
if group1.endswith(r'\*/'):
in_macie5[0] = 1
else:
in_macie5[0] = 0
return group1
elif group1:
if group1.endswith(r'\*/'):
if in_macie5[0]:
return ''
in_macie5[0] = 1
return r'/*\*/'
elif in_macie5[0]:
in_macie5[0] = 0
return '/**/'
return ''
else:
space_sub = space_sub_simple
def space_subber(match):
""" Space|Comment subber """
if match.lastindex:
if match.group(1).endswith(r'\*/'):
if in_macie5[0]:
return ''
in_macie5[0] = 1
return r'/*\*/'
elif in_macie5[0]:
in_macie5[0] = 0
return '/**/'
return ''
def fn_space_post(group):
""" space with token after """
if group(5) is None or (
group(6) == ':' and not in_rule[0] and not at_group[0]):
return ' ' + space_sub(space_subber, group(4))
return space_sub(space_subber, group(4))
def fn_semicolon(group):
""" ; handler """
return ';' + space_sub(space_subber, group(7))
def fn_semicolon2(group):
""" ; handler """
if in_rule[0]:
return space_sub(space_subber, group(7))
return ';' + space_sub(space_subber, group(7))
def fn_open(_):
""" { handler """
if at_group[0]:
at_group[0] -= 1
else:
in_rule[0] = 1
return '{'
def fn_close(_):
""" } handler """
in_rule[0] = 0
return '}'
def fn_at_group(group):
""" @xxx group handler """
at_group[0] += 1
return group(13)
def fn_ie7hack(group):
""" IE7 Hack handler """
if not in_rule[0] and not at_group[0]:
in_macie5[0] = 0
return group(14) + space_sub(space_subber, group(15))
return '>' + space_sub(space_subber, group(15))
table = (
# noqa pylint: disable = C0330
None,
None,
None,
None,
fn_space_post, # space with token after
fn_space_post, # space with token after
fn_space_post, # space with token after
fn_semicolon, # semicolon
fn_semicolon2, # semicolon
fn_open, # {
fn_close, # }
lambda g: g(11), # string
lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
# url(...)
fn_at_group, # @xxx expecting {...}
None,
fn_ie7hack, # ie7hack
None,
lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
# :first-line|letter followed
# by [{,] (apparently space
# needed for IE6)
lambda g: nl_unesc_sub('', g(18)), # nl_string
lambda g: post_esc_sub(' ', g(19)), # escape
)
def func(match):
""" Main subber """
idx, group = match.lastindex, match.group
if idx > 3:
return table[idx](group)
# shortcuts for frequent operations below:
elif idx == 1: # not interesting
return group(1)
# else: # space with token before or at the beginning
return space_sub(space_subber, group(idx))
return func
def cssmin(style, keep_bang_comments=False): # pylint: disable = W0621
"""
Minify CSS.
:Parameters:
`style` : ``str``
CSS to minify
`keep_bang_comments` : ``bool``
Keep comments starting with an exclamation mark? (``/*!...*/``)
:Return: Minified style
:Rtype: ``str``
"""
return main_sub(main_subber(keep_bang_comments), style)
return cssmin
cssmin = _make_cssmin()
if __name__ == '__main__':
def main():
""" Main """
import sys as _sys
keep_bang_comments = (
'-b' in _sys.argv[1:]
or '-bp' in _sys.argv[1:]
or '-pb' in _sys.argv[1:]
)
if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \
or '-pb' in _sys.argv[1:]:
global cssmin # pylint: disable = W0603
cssmin = _make_cssmin(python_only=True)
_sys.stdout.write(cssmin(
_sys.stdin.read(), keep_bang_comments=keep_bang_comments
))
main()

View File

@@ -1,10 +0,0 @@
from compressor.conf import settings
from compressor.filters import CompilerFilter
class CSSTidyFilter(CompilerFilter):
command = "{binary} {infile} {args} {outfile}"
options = (
("binary", settings.COMPRESS_CSSTIDY_BINARY),
("args", settings.COMPRESS_CSSTIDY_ARGUMENTS),
)

View File

@@ -1,13 +1,21 @@
from __future__ import absolute_import
from compressor.filters import CallbackOutputFilter
from compressor.filters.jsmin.slimit import SlimItFilter # noqa
class rJSMinFilter(CallbackOutputFilter):
callback = "compressor.filters.jsmin.rjsmin.jsmin"
callback = "rjsmin.jsmin"
dependencies = ["rjsmin"]
kwargs = {
"keep_bang_comments": True
}
# This is for backwards compatibility
JSMinFilter = rJSMinFilter
class SlimItFilter(CallbackOutputFilter):
dependencies = ["slimit"]
callback = "slimit.minify"
kwargs = {
"mangle": True,
}

View File

@@ -1,514 +0,0 @@
#!/usr/bin/env python
# -*- coding: ascii -*-
r"""
=====================
Javascript Minifier
=====================
rJSmin is a javascript minifier written in python.
The minifier is based on the semantics of `jsmin.c by Douglas Crockford`_\\.
:Copyright:
Copyright 2011 - 2015
Andr\xe9 Malo or his licensors, as applicable
:License:
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
The module is a re-implementation aiming for speed, so it can be used at
runtime (rather than during a preprocessing step). Usually it produces the
same results as the original ``jsmin.c``. It differs in the following ways:
- there is no error detection: unterminated string, regex and comment
literals are treated as regular javascript code and minified as such.
- Control characters inside string and regex literals are left untouched; they
are not converted to spaces (nor to \\n)
- Newline characters are not allowed inside string and regex literals, except
for line continuations in string literals (ECMA-5).
- "return /regex/" is recognized correctly.
- Line terminators after regex literals are handled more sensibly
- "+ +" and "- -" sequences are not collapsed to '++' or '--'
- Newlines before ! operators are removed more sensibly
- Comments starting with an exclamation mark (``!``) can be kept optionally
- rJSmin does not handle streams, but only complete strings. (However, the
module provides a "streamy" interface).
Since most parts of the logic are handled by the regex engine it's way faster
than the original python port of ``jsmin.c`` by Baruch Even. The speed factor
varies between about 6 and 55 depending on input and python version (it gets
faster the more compressed the input already is). Compared to the
speed-refactored python port by Dave St.Germain the performance gain is less
dramatic but still between 3 and 50 (for huge inputs). See the docs/BENCHMARKS
file for details.
rjsmin.c is a reimplementation of rjsmin.py in C and speeds it up even more.
Both python 2 and python 3 are supported.
.. _jsmin.c by Douglas Crockford:
http://www.crockford.com/javascript/jsmin.c
"""
if __doc__:
# pylint: disable = redefined-builtin
__doc__ = __doc__.encode('ascii').decode('unicode_escape')
__author__ = r"Andr\xe9 Malo".encode('ascii').decode('unicode_escape')
__docformat__ = "restructuredtext en"
__license__ = "Apache License, Version 2.0"
__version__ = '1.0.12'
__all__ = ['jsmin']
import re as _re
def _make_jsmin(python_only=False):
"""
Generate JS minifier based on `jsmin.c by Douglas Crockford`_
.. _jsmin.c by Douglas Crockford:
http://www.crockford.com/javascript/jsmin.c
:Parameters:
`python_only` : ``bool``
Use only the python variant. If true, the c extension is not even
tried to be loaded.
:Return: Minifier
:Rtype: ``callable``
"""
# pylint: disable = unused-variable
# pylint: disable = too-many-locals
if not python_only:
try:
import _rjsmin
except ImportError:
pass
else:
return _rjsmin.jsmin
try:
xrange
except NameError:
xrange = range # pylint: disable = redefined-builtin
space_chars = r'[\000-\011\013\014\016-\040]'
line_comment = r'(?://[^\r\n]*)'
space_comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
space_comment_nobang = r'(?:/\*(?!!)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
bang_comment = r'(?:/\*![^*]*\*+(?:[^/*][^*]*\*+)*/)'
string1 = \
r'(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)'
string2 = r'(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^"\\\r\n]*)*")'
strings = r'(?:%s|%s)' % (string1, string2)
charclass = r'(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\])'
nospecial = r'[^/\\\[\r\n]'
regex = r'(?:/(?![\r\n/*])%s*(?:(?:\\[^\r\n]|%s)%s*)*/)' % (
nospecial, charclass, nospecial
)
space = r'(?:%s|%s)' % (space_chars, space_comment)
newline = r'(?:%s?[\r\n])' % line_comment
def fix_charclass(result):
""" Fixup string of chars to fit into a regex char class """
pos = result.find('-')
if pos >= 0:
result = r'%s%s-' % (result[:pos], result[pos + 1:])
def sequentize(string):
"""
Notate consecutive characters as sequence
(1-4 instead of 1234)
"""
first, last, result = None, None, []
for char in map(ord, string):
if last is None:
first = last = char
elif last + 1 == char:
last = char
else:
result.append((first, last))
first = last = char
if last is not None:
result.append((first, last))
return ''.join(['%s%s%s' % (
chr(first),
last > first + 1 and '-' or '',
last != first and chr(last) or ''
) for first, last in result]) # noqa
return _re.sub(
r'([\000-\040\047])', # \047 for better portability
lambda m: '\\%03o' % ord(m.group(1)), (
sequentize(result)
.replace('\\', '\\\\')
.replace('[', '\\[')
.replace(']', '\\]')
)
)
def id_literal_(what):
""" Make id_literal like char class """
match = _re.compile(what).match
result = ''.join([
chr(c) for c in xrange(127) if not match(chr(c))
])
return '[^%s]' % fix_charclass(result)
def not_id_literal_(keep):
""" Make negated id_literal like char class """
match = _re.compile(id_literal_(keep)).match
result = ''.join([
chr(c) for c in xrange(127) if not match(chr(c))
])
return r'[%s]' % fix_charclass(result)
not_id_literal = not_id_literal_(r'[a-zA-Z0-9_$]')
preregex1 = r'[(,=:\[!&|?{};\r\n]'
preregex2 = r'%(not_id_literal)sreturn' % locals()
id_literal = id_literal_(r'[a-zA-Z0-9_$]')
id_literal_open = id_literal_(r'[a-zA-Z0-9_${\[(!+-]')
id_literal_close = id_literal_(r'[a-zA-Z0-9_$}\])"\047+-]')
post_regex_off = id_literal_(r'[^\000-\040}\])?:|,;.&=+-]')
dull = r'[^\047"/\000-\040]'
space_sub_simple = _re.compile((
# noqa pylint: disable = bad-continuation
r'(%(dull)s+)' # 0
r'|(%(strings)s%(dull)s*)' # 1
r'|(?<=%(preregex1)s)'
r'%(space)s*(?:%(newline)s%(space)s*)*'
r'(%(regex)s)' # 2
r'(%(space)s*(?:%(newline)s%(space)s*)+' # 3
r'(?=%(post_regex_off)s))?'
r'|(?<=%(preregex2)s)'
r'%(space)s*(?:(%(newline)s)%(space)s*)*' # 4
r'(%(regex)s)' # 5
r'(%(space)s*(?:%(newline)s%(space)s*)+' # 6
r'(?=%(post_regex_off)s))?'
r'|(?<=%(id_literal_close)s)'
r'%(space)s*(?:(%(newline)s)%(space)s*)+' # 7
r'(?=%(id_literal_open)s)'
r'|(?<=%(id_literal)s)(%(space)s)+(?=%(id_literal)s)' # 8
r'|(?<=\+)(%(space)s)+(?=\+)' # 9
r'|(?<=-)(%(space)s)+(?=-)' # 10
r'|%(space)s+'
r'|(?:%(newline)s%(space)s*)+'
) % locals()).sub
# print space_sub_simple.__self__.pattern
def space_subber_simple(match):
""" Substitution callback """
# pylint: disable = too-many-return-statements
groups = match.groups()
if groups[0]:
return groups[0]
elif groups[1]:
return groups[1]
elif groups[2]:
if groups[3]:
return groups[2] + '\n'
return groups[2]
elif groups[5]:
return "%s%s%s" % (
groups[4] and '\n' or '',
groups[5],
groups[6] and '\n' or '',
)
elif groups[7]:
return '\n'
elif groups[8] or groups[9] or groups[10]:
return ' '
else:
return ''
space_sub_banged = _re.compile((
# noqa pylint: disable = bad-continuation
r'(%(dull)s+)' # 0
r'|(%(strings)s%(dull)s*)' # 1
r'|(?<=%(preregex1)s)'
r'(%(space)s*(?:%(newline)s%(space)s*)*)' # 2
r'(%(regex)s)' # 3
r'(%(space)s*(?:%(newline)s%(space)s*)+' # 4
r'(?=%(post_regex_off)s))?'
r'|(?<=%(preregex2)s)'
r'(%(space)s*(?:(%(newline)s)%(space)s*)*)' # 5, 6
r'(%(regex)s)' # 7
r'(%(space)s*(?:%(newline)s%(space)s*)+' # 8
r'(?=%(post_regex_off)s))?'
r'|(?<=%(id_literal_close)s)'
r'(%(space)s*(?:%(newline)s%(space)s*)+)' # 9
r'(?=%(id_literal_open)s)'
r'|(?<=%(id_literal)s)(%(space)s+)(?=%(id_literal)s)' # 10
r'|(?<=\+)(%(space)s+)(?=\+)' # 11
r'|(?<=-)(%(space)s+)(?=-)' # 12
r'|(%(space)s+)' # 13
r'|((?:%(newline)s%(space)s*)+)' # 14
) % locals()).sub
# print space_sub_banged.__self__.pattern
keep = _re.compile((
r'%(space_chars)s+|%(space_comment_nobang)s+|%(newline)s+'
r'|(%(bang_comment)s+)'
) % locals()).sub
keeper = lambda m: m.groups()[0] or ''
# print keep.__self__.pattern
def space_subber_banged(match):
""" Substitution callback """
# pylint: disable = too-many-return-statements
groups = match.groups()
if groups[0]:
return groups[0]
elif groups[1]:
return groups[1]
elif groups[3]:
return "%s%s%s%s" % (
keep(keeper, groups[2]),
groups[3],
keep(keeper, groups[4] or ''),
groups[4] and '\n' or '',
)
elif groups[7]:
return "%s%s%s%s%s" % (
keep(keeper, groups[5]),
groups[6] and '\n' or '',
groups[7],
keep(keeper, groups[8] or ''),
groups[8] and '\n' or '',
)
elif groups[9]:
return keep(keeper, groups[9]) + '\n'
elif groups[10] or groups[11] or groups[12]:
return keep(keeper, groups[10] or groups[11] or groups[12]) or ' '
else:
return keep(keeper, groups[13] or groups[14])
def jsmin(script, keep_bang_comments=False):
r"""
Minify javascript based on `jsmin.c by Douglas Crockford`_\.
Instead of parsing the stream char by char, it uses a regular
expression approach which minifies the whole script with one big
substitution regex.
.. _jsmin.c by Douglas Crockford:
http://www.crockford.com/javascript/jsmin.c
:Parameters:
`script` : ``str``
Script to minify
`keep_bang_comments` : ``bool``
Keep comments starting with an exclamation mark? (``/*!...*/``)
:Return: Minified script
:Rtype: ``str``
"""
# pylint: disable = redefined-outer-name
if keep_bang_comments:
return space_sub_banged(
space_subber_banged, '\n%s\n' % script
).strip()
else:
return space_sub_simple(
space_subber_simple, '\n%s\n' % script
).strip()
return jsmin
jsmin = _make_jsmin()
def jsmin_for_posers(script, keep_bang_comments=False):
r"""
Minify javascript based on `jsmin.c by Douglas Crockford`_\.
Instead of parsing the stream char by char, it uses a regular
expression approach which minifies the whole script with one big
substitution regex.
.. _jsmin.c by Douglas Crockford:
http://www.crockford.com/javascript/jsmin.c
:Warning: This function is the digest of a _make_jsmin() call. It just
utilizes the resulting regexes. It's here for fun and may
vanish any time. Use the `jsmin` function instead.
:Parameters:
`script` : ``str``
Script to minify
`keep_bang_comments` : ``bool``
Keep comments starting with an exclamation mark? (``/*!...*/``)
:Return: Minified script
:Rtype: ``str``
"""
if not keep_bang_comments:
rex = (
r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]'
r'|\r?\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]'
r'|\r?\n|\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?<=[(,=:\[!&|?'
r'{};\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*'
r'][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\0'
r'14\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*((?:/(?![\r'
r'\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r'
r'\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/))((?:[\000-\011\013\014'
r'\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:(?:(?://[^\r'
r'\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:'
r'[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)+,.:;=?\]|}-]))?|(?<=[\00'
r'0-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\016-\040]|(?'
r':/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]'
r'))(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*'
r'\*+)*/))*)*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\['
r'[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/))(('
r'?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)'
r'*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\04'
r'0]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)+,.:;'
r'=?\]|}-]))?|(?<=[^\000-!#%&(*,./:-@\[\\^`{|~])(?:[\000-\011\01'
r'3\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:((?:(?:'
r'//[^\r\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]'
r'*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#%-\047)*,./:-@\\-^'
r'`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-\011\013\014\0'
r'16-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^\000-#%-,./'
r':-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|(?:/\*['
r'^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\013'
r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:['
r'\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
r')+|(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]'
r'|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+'
)
def subber(match):
""" Substitution callback """
groups = match.groups()
return (
groups[0] or
groups[1] or
(groups[3] and (groups[2] + '\n')) or
groups[2] or
(groups[5] and "%s%s%s" % (
groups[4] and '\n' or '',
groups[5],
groups[6] and '\n' or '',
)) or
(groups[7] and '\n') or
(groups[8] and ' ') or
(groups[9] and ' ') or
(groups[10] and ' ') or
''
)
else:
rex = (
r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]'
r'|\r?\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]'
r'|\r?\n|\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?<=[(,=:\[!&|?'
r'{};\r\n])((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
r'*][^*]*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013'
r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*)((?:/(?!'
r'[\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^'
r'\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/))((?:[\000-\011\013\01'
r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:(?:(?://[^'
r'\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+('
r'?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)+,.:;=?\]|}-]))?|(?<=['
r'\000-#%-,./:-@\[-^`{-~-]return)((?:[\000-\011\013\014\016-\040'
r']|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?['
r'\r\n]))(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*]['
r'^*]*\*+)*/))*)*)((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|'
r'(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*'
r'/))((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]'
r'*\*+)*/))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\01'
r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040&)'
r'+,.:;=?\]|}-]))?|(?<=[^\000-!#%&(*,./:-@\[\\^`{|~])((?:[\000-'
r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*(?:'
r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/'
r'\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+)(?=[^\000-\040"#%-\047)*,./'
r':-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-\011\01'
r'3\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+)(?=[^\000'
r'-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+)(?=\+)|(?<=-)((?:[\000-\0'
r'11\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+)(?=-'
r')|((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*'
r'\*+)*/))+)|((?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014'
r'\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+)'
)
keep = _re.compile((
r'[\000-\011\013\014\016-\040]+|(?:/\*(?!!)[^*]*\*+(?:[^/*][^*]*'
r'\*+)*/)+|(?:(?://[^\r\n]*)?[\r\n])+|((?:/\*![^*]*\*+(?:[^/*][^'
r'*]*\*+)*/)+)'
) % locals()).sub
keeper = lambda m: m.groups()[0] or ''
def subber(match):
""" Substitution callback """
groups = match.groups()
return (
groups[0] or
groups[1] or
(groups[3] and "%s%s%s%s" % (
keep(keeper, groups[2]),
groups[3],
keep(keeper, groups[4] or ''),
groups[4] and '\n' or '',
)) or
(groups[7] and "%s%s%s%s%s" % (
keep(keeper, groups[5]),
groups[6] and '\n' or '',
groups[7],
keep(keeper, groups[8] or ''),
groups[8] and '\n' or '',
)) or
(groups[9] and keep(keeper, groups[9] + '\n')) or
(groups[10] and keep(keeper, groups[10]) or ' ') or
(groups[11] and keep(keeper, groups[11]) or ' ') or
(groups[12] and keep(keeper, groups[12]) or ' ') or
keep(keeper, groups[13] or groups[14])
)
return _re.sub(rex, subber, '\n%s\n' % script).strip()
if __name__ == '__main__':
def main():
""" Main """
import sys as _sys
argv = _sys.argv[1:]
keep_bang_comments = '-b' in argv or '-bp' in argv or '-pb' in argv
if '-p' in argv or '-bp' in argv or '-pb' in argv:
xjsmin = _make_jsmin(python_only=True)
else:
xjsmin = jsmin
_sys.stdout.write(xjsmin(
_sys.stdin.read(), keep_bang_comments=keep_bang_comments
))
main()

View File

@@ -1,10 +0,0 @@
from __future__ import absolute_import
from compressor.filters import CallbackOutputFilter
class SlimItFilter(CallbackOutputFilter):
dependencies = ["slimit"]
callback = "slimit.minify"
kwargs = {
"mangle": True,
}

View File

@@ -11,27 +11,15 @@ class BeautifulSoupParser(ParserBase):
super(BeautifulSoupParser, self).__init__(content)
try:
from bs4 import BeautifulSoup
self.use_bs4 = True
self.soup = BeautifulSoup(self.content, "html.parser")
except ImportError:
try:
from BeautifulSoup import BeautifulSoup
self.use_bs4 = False
self.soup = BeautifulSoup(self.content)
except ImportError as err:
raise ImproperlyConfigured("Error while importing BeautifulSoup: %s" % err)
except ImportError as err:
raise ImproperlyConfigured("Error while importing BeautifulSoup: %s" % err)
def css_elems(self):
if self.use_bs4:
return self.soup.find_all({'link': True, 'style': True})
else:
return self.soup.findAll({'link': True, 'style': True})
return self.soup.find_all({'link': True, 'style': True})
def js_elems(self):
if self.use_bs4:
return self.soup.find_all('script')
else:
return self.soup.findAll('script')
return self.soup.find_all('script')
def elem_attribs(self, elem):
attrs = dict(elem.attrs)

View File

@@ -4,12 +4,7 @@ import re
from tempfile import mkdtemp
from shutil import rmtree, copytree
try:
from bs4 import BeautifulSoup
use_bs4 = True
except ImportError:
from BeautifulSoup import BeautifulSoup
use_bs4 = False
from bs4 import BeautifulSoup
from django.core.cache.backends import locmem
from django.test import SimpleTestCase
@@ -26,17 +21,7 @@ from compressor.storage import DefaultStorage
def make_soup(markup):
if use_bs4:
return BeautifulSoup(markup, "html.parser")
else:
return BeautifulSoup(markup)
def soup_find_all(markup, name):
if use_bs4:
return make_soup(markup).find_all(name)
else:
return make_soup(markup).findAll(name)
return BeautifulSoup(markup, "html.parser")
def css_tag(href, **kwargs):
@@ -302,7 +287,7 @@ class CssMediaTestCase(SimpleTestCase):
def test_css_output(self):
css_node = CssCompressor(self.css)
links = soup_find_all(css_node.output(), 'link')
links = make_soup(css_node.output()).find_all('link')
media = ['screen', 'print', 'all', None]
self.assertEqual(len(links), 4)
self.assertEqual(media, [l.get('media', None) for l in links])
@@ -311,7 +296,7 @@ class CssMediaTestCase(SimpleTestCase):
css = self.css + '<style type="text/css" media="print">p { border:10px solid red;}</style>'
css_node = CssCompressor(css)
media = ['screen', 'print', 'all', None, 'print']
links = soup_find_all(css_node.output(), 'link')
links = make_soup(css_node.output()).find_all('link')
self.assertEqual(media, [l.get('media', None) for l in links])
@override_settings(COMPRESS_PRECOMPILERS=(
@@ -323,7 +308,7 @@ class CssMediaTestCase(SimpleTestCase):
<link rel="stylesheet" href="/static/css/two.css" type="text/css" media="screen">
<style type="text/foobar" media="screen">h1 { border:5px solid green;}</style>"""
css_node = CssCompressor(css)
output = soup_find_all(css_node.output(), ['link', 'style'])
output = make_soup(css_node.output()).find_all(['link', 'style'])
self.assertEqual(['/static/css/one.css', '/static/css/two.css', None],
[l.get('href', None) for l in output])
self.assertEqual(['screen', 'screen', 'screen'],
@@ -363,11 +348,8 @@ class JsAsyncDeferTestCase(SimpleTestCase):
return 'defer'
js_node = JsCompressor(self.js)
output = [None, 'async', 'defer', None, 'async', None]
scripts = soup_find_all(js_node.output(), 'script')
if use_bs4:
attrs = [extract_attr(i) for i in scripts]
else:
attrs = [s.get('async') or s.get('defer') for s in scripts]
scripts = make_soup(js_node.output()).find_all('script')
attrs = [extract_attr(s) for s in scripts]
self.assertEqual(output, attrs)

View File

@@ -3,24 +3,20 @@ from collections import defaultdict
import io
import os
import sys
import textwrap
from django.utils import six
from django.test import TestCase
from django.utils import unittest
from django.test.utils import override_settings
from compressor.cache import cache, get_hashed_mtime, get_hashed_content
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
from compressor.filters.closure import ClosureCompilerFilter
from compressor.filters.csstidy import CSSTidyFilter
from compressor.filters.yuglify import YUglifyCSSFilter, YUglifyJSFilter
from compressor.filters.yui import YUICSSFilter, YUIJSFilter
from compressor.filters.cleancss import CleanCSSFilter
@@ -31,22 +27,6 @@ def blankdict(*args, **kwargs):
return defaultdict(lambda: '', *args, **kwargs)
@unittest.skipIf(find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY)
class CssTidyTestCase(TestCase):
def test_tidy(self):
content = textwrap.dedent("""\
/* Some comment */
font,th,td,p{
color: black;
}
""")
ret = CSSTidyFilter(content).input()
self.assertIsInstance(ret, six.text_type)
self.assertEqual(
"font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
@override_settings(COMPRESS_CACHEABLE_PRECOMPILERS=('text/css',))
class PrecompilerTestCase(TestCase):
def setUp(self):
@@ -144,8 +124,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
@@ -158,8 +138,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):
@@ -440,10 +423,6 @@ class SpecializedFiltersTest(TestCase):
filter = ClosureCompilerFilter('')
self.assertEqual(filter.options, (('binary', six.text_type('java -jar compiler.jar')), ('args', six.text_type(''))))
def test_csstidy_filter(self):
filter = CSSTidyFilter('')
self.assertEqual(filter.options, (('binary', six.text_type('csstidy')), ('args', six.text_type('--template=highest'))))
def test_yuglify_filters(self):
filter = YUglifyCSSFilter('')
self.assertEqual(filter.command, '{binary} {args} --type=css')

View File

@@ -1,260 +0,0 @@
# -*- coding: utf-8 -*-
"""Advanced string formatting for Python >= 2.4.
An implementation of the advanced string formatting (PEP 3101).
Author: Florent Xicluna
"""
from __future__ import unicode_literals
import re
from django.utils import six
_format_str_re = re.compile(
r'((?<!{)(?:{{)+' # '{{'
r'|(?:}})+(?!})' # '}}
r'|{(?:[^{](?:[^{}]+|{[^{}]*})*)?})' # replacement field
)
_format_sub_re = re.compile(r'({[^{}]*})') # nested replacement field
_format_spec_re = re.compile(
r'((?:[^{}]?[<>=^])?)' # alignment
r'([-+ ]?)' # sign
r'(#?)' r'(\d*)' r'(,?)' # base prefix, minimal width, thousands sep
r'((?:\.\d+)?)' # precision
r'(.?)$' # type
)
_field_part_re = re.compile(
r'(?:(\[)|\.|^)' # start or '.' or '['
r'((?(1)[^]]*|[^.[]*))' # part
r'(?(1)(?:\]|$)([^.[]+)?)' # ']' and invalid tail
)
_format_str_sub = _format_str_re.sub
def _is_integer(value):
return hasattr(value, '__index__')
def _strformat(value, format_spec=""):
"""Internal string formatter.
It implements the Format Specification Mini-Language.
"""
m = _format_spec_re.match(str(format_spec))
if not m:
raise ValueError('Invalid conversion specification')
align, sign, prefix, width, comma, precision, conversion = m.groups()
is_numeric = hasattr(value, '__float__')
is_integer = is_numeric and _is_integer(value)
if prefix and not is_integer:
raise ValueError('Alternate form (#) not allowed in %s format '
'specifier' % (is_numeric and 'float' or 'string'))
if is_numeric and conversion == 'n':
# Default to 'd' for ints and 'g' for floats
conversion = is_integer and 'd' or 'g'
elif sign:
if not is_numeric:
raise ValueError("Sign not allowed in string format specifier")
if conversion == 'c':
raise ValueError("Sign not allowed with integer "
"format specifier 'c'")
if comma:
# TODO: thousand separator
pass
try:
if ((is_numeric and conversion == 's') or (not is_integer and conversion in set('cdoxX'))):
raise ValueError
if conversion == 'c':
conversion = 's'
value = chr(value % 256)
rv = ('%' + prefix + precision + (conversion or 's')) % (value,)
except ValueError:
raise ValueError("Unknown format code %r for object of type %r" %
(conversion, value.__class__.__name__))
if sign not in '-' and value >= 0:
# sign in (' ', '+')
rv = sign + rv
if width:
zero = (width[0] == '0')
width = int(width)
else:
zero = False
width = 0
# Fastpath when alignment is not required
if width <= len(rv):
if not is_numeric and (align == '=' or (zero and not align)):
raise ValueError("'=' alignment not allowed in string format "
"specifier")
return rv
fill, align = align[:-1], align[-1:]
if not fill:
fill = zero and '0' or ' '
if align == '^':
padding = width - len(rv)
# tweak the formatting if the padding is odd
if padding % 2:
rv += fill
rv = rv.center(width, fill)
elif align == '=' or (zero and not align):
if not is_numeric:
raise ValueError("'=' alignment not allowed in string format "
"specifier")
if value < 0 or sign not in '-':
rv = rv[0] + rv[1:].rjust(width - 1, fill)
else:
rv = rv.rjust(width, fill)
elif align in ('>', '=') or (is_numeric and not align):
# numeric value right aligned by default
rv = rv.rjust(width, fill)
else:
rv = rv.ljust(width, fill)
return rv
def _format_field(value, parts, conv, spec, want_bytes=False):
"""Format a replacement field."""
for k, part, _ in parts:
if k:
if part.isdigit():
value = value[int(part)]
else:
value = value[part]
else:
value = getattr(value, part)
if conv:
value = ((conv == 'r') and '%r' or '%s') % (value,)
if hasattr(value, '__format__'):
value = value.__format__(spec)
elif hasattr(value, 'strftime') and spec:
value = value.strftime(str(spec))
else:
value = _strformat(value, spec)
if want_bytes and isinstance(value, six.text_type):
return str(value)
return value
class FormattableString(object):
"""Class which implements method format().
The method format() behaves like str.format() in python 2.6+.
>>> FormattableString('{a:5}').format(a=42)
... # Same as '{a:5}'.format(a=42)
' 42'
"""
__slots__ = '_index', '_kwords', '_nested', '_string', 'format_string'
def __init__(self, format_string):
self._index = 0
self._kwords = {}
self._nested = {}
self.format_string = format_string
self._string = _format_str_sub(self._prepare, format_string)
def __eq__(self, other):
if isinstance(other, FormattableString):
return self.format_string == other.format_string
# Compare equal with the original string.
return self.format_string == other
def _prepare(self, match):
# Called for each replacement field.
part = match.group(0)
if part[0] == part[-1]:
# '{{' or '}}'
assert part == part[0] * len(part)
return part[:len(part) // 2]
repl = part[1:-1]
field, _, format_spec = repl.partition(':')
literal, sep, conversion = field.partition('!')
if sep and not conversion:
raise ValueError("end of format while looking for "
"conversion specifier")
if len(conversion) > 1:
raise ValueError("expected ':' after format specifier")
if conversion not in 'rsa':
raise ValueError("Unknown conversion specifier %s" %
str(conversion))
name_parts = _field_part_re.findall(literal)
if literal[:1] in '.[':
# Auto-numbering
if self._index is None:
raise ValueError("cannot switch from manual field "
"specification to automatic field numbering")
name = str(self._index)
self._index += 1
if not literal:
del name_parts[0]
else:
name = name_parts.pop(0)[1]
if name.isdigit() and self._index is not None:
# Manual specification
if self._index:
raise ValueError("cannot switch from automatic field "
"numbering to manual field specification")
self._index = None
empty_attribute = False
for k, v, tail in name_parts:
if not v:
empty_attribute = True
if tail:
raise ValueError("Only '.' or '[' may follow ']' "
"in format field specifier")
if name_parts and k == '[' and not literal[-1] == ']':
raise ValueError("Missing ']' in format string")
if empty_attribute:
raise ValueError("Empty attribute in format string")
if '{' in format_spec:
format_spec = _format_sub_re.sub(self._prepare, format_spec)
rv = (name_parts, conversion, format_spec)
self._nested.setdefault(name, []).append(rv)
else:
rv = (name_parts, conversion, format_spec)
self._kwords.setdefault(name, []).append(rv)
return r'%%(%s)s' % id(rv)
def format(self, *args, **kwargs):
"""Same as str.format() and unicode.format() in Python 2.6+."""
if args:
kwargs.update(dict((str(i), value)
for (i, value) in enumerate(args)))
# Encode arguments to ASCII, if format string is bytes
want_bytes = isinstance(self._string, str)
params = {}
for name, items in self._kwords.items():
value = kwargs[name]
for item in items:
parts, conv, spec = item
params[str(id(item))] = _format_field(value, parts, conv, spec,
want_bytes)
for name, items in self._nested.items():
value = kwargs[name]
for item in items:
parts, conv, spec = item
spec = spec % params
params[str(id(item))] = _format_field(value, parts, conv, spec,
want_bytes)
return self._string % params
def selftest():
import datetime
F = FormattableString
assert F("{0:{width}.{precision}s}").format('hello world',
width=8, precision=5) == 'hello '
d = datetime.date(2010, 9, 7)
assert F("The year is {0.year}").format(d) == "The year is 2010"
assert F("Tested on {0:%Y-%m-%d}").format(d) == "Tested on 2010-09-07"
print('Test successful')
if __name__ == '__main__':
selftest()

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/
@@ -39,27 +39,9 @@ Installation
.. _dependencies:
Dependencies
Optional Dependencies
------------
Required
^^^^^^^^
In case you're installing Django Compressor differently
(e.g. from the Git repo), make sure to install the following
dependencies.
- django-appconf_
Used internally to handle Django's settings, this is
automatically installed when following the above
installation instructions.
pip install django-appconf
Optional
^^^^^^^^
- BeautifulSoup_
For the :attr:`parser <django.conf.settings.COMPRESS_PARSER>`
@@ -89,6 +71,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

@@ -87,18 +87,6 @@ Backend settings
feature, and the ``'content'`` in case you're using multiple servers
to serve your content.
- ``compressor.filters.csstidy.CSSTidyFilter``
A filter that passes the CSS content to the CSSTidy_ tool.
.. attribute:: COMPRESS_CSSTIDY_BINARY
The CSSTidy binary filesystem path.
.. attribute:: COMPRESS_CSSTIDY_ARGUMENTS
The arguments passed to CSSTidy.
- ``compressor.filters.datauri.CssDataUriFilter``
A filter for embedding media as `data: URIs`_ in the CSS.
@@ -132,15 +120,17 @@ 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``
A filter that uses the cssmin implementation rCSSmin_ to compress CSS
(included, no external dependencies).
(installed by default).
- ``compressor.filters.cleancss.CleanCSSFilter``
@@ -155,9 +145,8 @@ Backend settings
The arguments passed to clean-css.
.. _CSSTidy: http://csstidy.sourceforge.net/
.. _`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/
@@ -184,7 +173,7 @@ Backend settings
- ``compressor.filters.jsmin.JSMinFilter``
A filter that uses the jsmin implementation rJSmin_ to compress
JavaScript code (included, no external dependencies).
JavaScript code (installed by default).
.. _slimit_filter:
@@ -280,8 +269,8 @@ Backend settings
.. note::
Depending on the implementation, some precompilers might not support
outputting to something else than ``stdout``, so you'll need to omit the
``{outfile}`` parameter when working with those. For instance, if you
are using the Ruby version of lessc, you'll need to set up the
``{outfile}`` parameter when working with those. For instance, if you
are using the Ruby version of lessc, you'll need to set up the
precompiler like this::
('text/less', 'lessc {infile}'),
@@ -453,11 +442,11 @@ Caching settings
and the ``django.core.context_processors.request`` context processor.
.. _RequestContext: http://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.RequestContext
.. attribute:: COMPRESS_CACHE_KEY_FUNCTION
:Default: ``'compressor.cache.simple_cachekey'``
The function to use when generating the cache key. The function must take
one argument which is the partial key based on the source's hex digest.
It must return the full key as a string.

View File

@@ -10,3 +10,6 @@ coffin==0.4.0
jingo==0.7
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
rcssmin==1.0.6
rjsmin==1.0.12

View File

@@ -142,5 +142,7 @@ setup(
zip_safe=False,
install_requires=[
'django-appconf >= 0.4',
'rcssmin == 1.0.6',
'rjsmin == 1.0.12',
],
)

14
tox.ini
View File

@@ -12,6 +12,9 @@ two =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
rcssmin==1.0.6
rjsmin==1.0.12
two_six =
flake8==2.4.0
coverage==3.7.1
@@ -19,12 +22,15 @@ two_six =
mock==1.0.1
Jinja2==2.7.3
lxml==3.4.2
BeautifulSoup==3.2.1
beautifulsoup4==4.4.0
unittest2==1.0.0
jingo==0.7
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
rcssmin==1.0.6
rjsmin==1.0.12
three =
flake8==2.4.0
coverage==3.7.1
@@ -37,6 +43,9 @@ three =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
rcssmin==1.0.6
rjsmin==1.0.12
three_two =
flake8==2.4.0
coverage==3.7.1
@@ -49,6 +58,9 @@ three_two =
coffin==0.4.0
django-sekizai==0.8.2
django-overextends==0.4.0
csscompressor==0.9.4
rcssmin==1.0.6
rjsmin==1.0.12
[tox]
envlist =
{py26,py27}-1.4.X,