--- /dev/null
+python-django-compressor (1.4-0u~u14.04+mos2) mos7.0; urgency=medium
+ * Satisfy global requirements [1]
+ * [1] https://github.com/openstack/requirements/blob/stable/kilo/global-requirements.txt#L25
+ * Remove watch file
+ * Update python version
+
+ -- Ivan Udovichenko <iudovichenko@mirantis.com> Thu, 16 Apr 2015 23:11:22 +0300
+
+python-django-compressor (1.4-0u~u14.04+mos1) mos6.1; urgency=medium
+
+ * Adjust the package revision in order to avoid breaking packages
+ depending on python-django-compressor (>= 1.4)
+
+ -- Vasyl Saienko <vsaienko@mirantis.com> Wed, 15 Apr 2015 17:35:32 +0200
+
+python-django-compressor (1.4-0~u14.04+mos1) mos6.1; urgency=medium
+
+ * Adjust the package revision according to the versioning policy
+ stated in the separate-mos-from-linux blueprint.
+
+ -- Alexei Sheplyakov <asheplyakov@mirantis.com> Thu, 09 Apr 2015 14:35:32 +0300
+
+python-django-compressor (1.4-0ubuntu1~cloud0~mos6.1) trusty; urgency=low
+
+ * Build python-django-compressor for Ubuntu 14.04
+
+ -- Sergey Kolekonov <skolekonov@mirantis.com> Thu, 26 Feb 2015 13:13:11 +0300
+
+python-django-compressor (1.4-0ubuntu1~cloud0) precise-juno; urgency=low
+
+ * Update version for MOS
+
+ -- Sergey Otpouschennikov <sotpuschennikov@mirantis.com> Tue, 13 Aug 2013 15:40:41 +0300
+
+python-django-compressor (1.3-1ubuntu3) trusty; urgency=medium
+
+ * d/control: Drop python-beautifulsoup from BD's (LP: #1252627), its
+ only required to run tests and is a optional parser at runtime.
+ * d/rules: Correct path for django-admin.py so tests actually run,
+ even if the result is ignored.
+
+ -- James Page <james.page@ubuntu.com> Fri, 07 Mar 2014 13:09:39 +0000
+
+python-django-compressor (1.3-1ubuntu2) trusty; urgency=medium
+
+ * Rebuild to drop files installed into /usr/share/pyshared.
+
+ -- Matthias Klose <doko@ubuntu.com> Sun, 23 Feb 2014 13:51:17 +0000
+
+python-django-compressor (1.3-1ubuntu1) trusty; urgency=medium
+
+ * Drop use of external discover-runner as this is included in
+ django >= 1.6 (LP: #1252627):
+ - d/patches/django-1.6-compat.patch: Patch out use of discover_runner.
+ - d/control: Drop BD on python-django-discover-runner, version BD on
+ python-django >= 1.6.
+
+ -- James Page <james.page@ubuntu.com> Wed, 08 Jan 2014 10:32:15 +0000
+
+python-django-compressor (1.3-1) unstable; urgency=low
+
+ * New upstream release.
+ * Added unit tests build-depends and ran wrap-and-sort.
+
+ -- Thomas Goirand <zigo@debian.org> Wed, 26 Jun 2013 14:29:00 +0800
+
+python-django-compressor (1.2-2) unstable; urgency=low
+
+ * Uploading to unstable.
+
+ -- Thomas Goirand <zigo@debian.org> Sun, 12 May 2013 15:20:14 +0000
+
+python-django-compressor (1.2-1) experimental; urgency=low
+
+ * Initial release.
+
+ -- Thomas Goirand <zigo@debian.org> Sun, 14 Oct 2012 10:51:47 +0000
--- /dev/null
+Source: python-django-compressor
+Section: python
+Priority: optional
+Maintainer: MOS Horizon Team <mos-horizon@mirantis.com>
+Build-Depends: debhelper (>= 9),
+ openstack-pkg-tools,
+ python-all (>= 2.7.1),
+ python-setuptools
+Build-Depends-Indep: python-appconf,
+ python-coverage,
+ python-django (>= 1.6),
+ python-html5lib,
+ python-jinja2,
+ python-lxml,
+ python-mock,
+ python-nose,
+ python-unittest2
+Standards-Version: 3.9.4
+Homepage: http://pypi.python.org/pypi/django_compressor/
+
+Package: python-compressor
+Architecture: all
+Pre-Depends: dpkg (>= 1.15.6~)
+Depends: python-appconf,
+ python-django (>= 1.1),
+ ${misc:Depends},
+ ${python:Depends}
+Provides: ${python:Provides}
+Description: Compresses linked and inline JavaScript or CSS into single cached files
+ Django Compressor combines and compresses linked and inline Javascript or CSS
+ in a Django templates into cacheable static files by using the compress
+ template tag.
--- /dev/null
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: python-django-compressor
+Source: http://pypi.python.org/pypi/django_compressor/
+
+Files: debian/*
+Copyright: (c) 2012, Thomas Goirand <zigo@debian.org>
+License: MIT
+
+Files: compressor/filters/jsmin/rjsmin.py
+Copyright: 2006-2011, André Malo
+License: Apache-2.0
+
+Files: compressor/filters/cssmin/*
+Copyright: (c) 2010 Zachary Voase
+License: MIT
+
+Files: compressor/utils/decorators.py
+Copyright: 2009-2011, Ask Solem and contributors
+License: BSD-2-clauses
+
+Files: compressor/utils/stringformat.py
+Copyright: 2010, Florent Xicluna
+License: BSD-3-clauses
+
+Files: *
+Copyright: 2009-2011 django_compressor authors (see AUTHORS file)
+ 2008 Andreas Pelme <andreas@pelme.se>
+ 2006-2011 André Malo or his licensors, as applicable
+ 2009-2011 Ask Solem and contributors.
+ 2010 by Florent Xicluna.
+License: MIT
+
+License: MIT
+ 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.
+
+License: BSD-3-clauses
+ Redistribution and use in source and binary forms of the software as well as
+ documentation, with or without modification, are permitted provided that the
+ following conditions are met:
+ .
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * The names of the contributors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+ .
+ THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+
+License: BSD-2-clauses
+ Redistribution and use in source and binary forms of the software as well as
+ documentation, with or without modification, are permitted provided that the
+ following conditions are met:
+ .
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ .
+ THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+
+License: Apache-2.0
+ 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.
+ .
+ On Debian based systems, the full text of the Apache-2.0 license is available
+ in this file: /usr/share/common-licenses/Apache-2.0
--- /dev/null
+django_appconf python-appconf
--- /dev/null
+#!/usr/bin/make -f
+
+#export DH_VERBOSE=1
+
+UPSTREAM_GIT = git://github.com/jezdez/django_compressor.git
+
+include /usr/share/openstack-pkg-tools/pkgos.make
+
+%:
+ dh $@ --with python2
+
+PYDEF=$(shell pyversions -d)
+
+ifeq (,$(findstring nocheck, $(DEB_BUILD_OPTIONS)))
+override_dh_auto_test:
+ PYTHONPATH=$PYTHONPATH:. python /usr/lib/$(PYDEF)/dist-packages/django/bin/django-admin.py test --settings=compressor.test_settings compressor || true
+ rm -rf $(CURDIR)/compressor/tests/static/CACHE
+endif
+
+override_dh_auto_build:
+
+override_dh_install:
+ set -e ; for i in `pyversions -s` ; do \
+ $$i setup.py install --install-layout=deb --root=debian/python-compressor ; \
+ rm -f $(CURDIR)/debian/usr/lib/$$i/dist-packages/compressor/tests/static/CACHE/css/* ; \
+ rm -f $(CURDIR)/debian/usr/lib/$$i/dist-packages/compressor/tests/static/CACHE/js/* ; \
+ done
+ find debian/python-compressor -iname '*.pyc' -delete
+
+override_dh_usrlocal:
+ rm -f $(CURDIR)/debian/usr/share/pyshared/compressor/tests/static/CACHE/css/*
+ rm -f $(CURDIR)/debian/usr/share/pyshared/compressor/tests/static/CACHE/js/*
--- /dev/null
+3.0 (quilt)
--- /dev/null
+Christian Metts
+Carl Meyer
+Jannis Leidel
+Mathieu Pillard
+
+
+Django Compressor's filters started life as the filters from Andreas Pelme's
+django-compress.
+
+Contributors:
+
+Aaron Godfrey
+Adam "Cezar" Jenkins
+Adrian Holovaty
+Alen Mujezinovic
+Alex Kessinger
+Andreas Pelme
+Antti Hirvonen
+Apostolos Bessas
+Ashley Camba Garrido
+Atamert Ölçgen
+Aymeric Augustin
+Bartek Ciszkowski
+Ben Firshman
+Ben Spaulding
+Benjamin Gilbert
+Benjamin Wohlwend
+Bojan Mihelac
+Boris Shemigon
+Brad Whittington
+Bruno Renié
+Cassus Adam Banko
+Chris Adams
+Chris Streeter
+Clay McClure
+David Medina
+David Ziegler
+Eugene Mirotin
+Fenn Bailey
+Francisco Souza
+Gert Van Gool
+Greg McGuire
+Harro van der Klauw
+Isaac Bythewood
+Iván Raskovsky
+Jaap Roes
+James Roe
+Jason Davies
+Jens Diemer
+Jeremy Dunck
+Jervis Whitley
+John-Scott Atlakson
+Jonas von Poser
+Jonathan Lukens
+Julian Scheid
+Julien Phalip
+Justin Lilly
+Lucas Tan
+Luis Nell
+Lukas Lehner
+Łukasz Balcerzak
+Łukasz Langa
+Maciek Szczesniak
+Maor Ben-Dayan
+Mark Lavin
+Marsel Mavletkulov
+Matt Schick
+Matthew Tretter
+Mehmet S. Catalbas
+Michael van de Waeter
+Mike Yumatov
+Nicolas Charlot
+Niran Babalola
+Paul McMillan
+Petar Radosevic
+Peter Bengtsson
+Peter Lundberg
+Philipp Bosch
+Philipp Wollermann
+Rich Leland
+Sam Dornan
+Saul Shanabrook
+Selwin Ong
+Shabda Raaj
+Stefano Brentegani
+Sébastien Piquemal
+Thom Linton
+Thomas Schreiber
+Tino de Bruijn
+Ulrich Petri
+Ulysses V
+Vladislav Poluhin
+wesleyb
+Wilson Júnior
--- /dev/null
+django_compressor
+-----------------
+Copyright (c) 2009-2014 Django Compressor authors (see AUTHORS file)
+
+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.
+
+
+django_compressor contains code from Andreas Pelme's django-compress
+--------------------------------------------------------------------
+Copyright (c) 2008 Andreas Pelme <andreas@pelme.se>
+
+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.
+
+
+rjsmin.py (License-information from the file)
+---------------------------------------------
+Copyright 2006, 2007, 2008, 2009, 2010, 2011
+André Malo or his licensors, as applicable
+
+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.
+
+
+utils.cache.cached_property extracted from Celery
+-------------------------------------------
+Copyright (c) 2009-2011, Ask Solem and contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+Neither the name of Ask Solem nor the names of its contributors may be used
+to endorse or promote products derived from this software without specific
+prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+utils.FormattableString
+-----------------------
+Copyright (c) 2010 by Florent Xicluna.
+
+Some rights reserved.
+
+Redistribution and use in source and binary forms of the software as well
+as documentation, with or without modification, are permitted provided
+that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+* The names of the contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
+NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGE.
--- /dev/null
+include AUTHORS
+include README.rst
+include LICENSE
+include Makefile
+include tox.ini
+recursive-include docs *
+recursive-include requirements *
+recursive-include compressor/templates/compressor *.html
+recursive-include compressor/tests/media *.js *.css *.png *.coffee
+recursive-include compressor/tests/test_templates *.html
--- /dev/null
+testenv:
+ pip install -e .
+ pip install -r requirements/tests.txt
+ pip install Django
+
+test:
+ flake8 compressor --ignore=E501,E128,E701,E261,E301,E126,E127,E131
+ coverage run --branch --source=compressor `which django-admin.py` test --settings=compressor.test_settings compressor
+ coverage report --omit=compressor/test*,compressor/filters/jsmin/rjsmin*,compressor/filters/cssmin/cssmin*,compressor/utils/stringformat*
+
+.PHONY: test
--- /dev/null
+Metadata-Version: 1.1
+Name: django_compressor
+Version: 1.4
+Summary: Compresses linked and inline JavaScript or CSS into single cached files.
+Home-page: http://django-compressor.readthedocs.org/en/latest/
+Author: Jannis Leidel
+Author-email: jannis@leidel.info
+License: MIT
+Description: Django Compressor
+ =================
+
+ .. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop
+ :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop
+
+ .. image:: https://pypip.in/v/django_compressor/badge.png
+ :target: https://pypi.python.org/pypi/django_compressor
+
+ .. image:: https://pypip.in/d/django_compressor/badge.png
+ :target: https://pypi.python.org/pypi/django_compressor
+
+ .. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop
+ :alt: Build Status
+ :target: http://travis-ci.org/django-compressor/django-compressor
+
+ Django Compressor combines and compresses linked and inline Javascript
+ or CSS in a Django template into cacheable static files by using the
+ ``compress`` template tag.
+
+ HTML in between ``{% compress js/css %}`` and ``{% endcompress %}`` is
+ parsed and searched for CSS or JS. These styles and scripts are subsequently
+ processed with optional, configurable compilers and filters.
+
+ The default filter for CSS rewrites paths to static files to be absolute
+ and adds a cache busting timestamp. For Javascript the default filter
+ compresses it using ``jsmin``.
+
+ As the final result the template tag outputs a ``<script>`` or ``<link>``
+ tag pointing to the optimized file. These files are stored inside a folder
+ and given a unique name based on their content. Alternatively it can also
+ return the resulting content to the original template directly.
+
+ Since the file name is dependent on the content these files can be given
+ a far future expiration date without worrying about stale browser caches.
+
+ The concatenation and compressing process can also be jump started outside
+ of the request/response cycle by using the Django management command
+ ``manage.py compress``.
+
+ Configurability & Extendibility
+ -------------------------------
+
+ Django Compressor is highly configurable and extendible. The HTML parsing
+ is done using lxml_ or if it's not available Python's built-in HTMLParser by
+ 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`_,
+ `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
+ `data URIs`_.
+
+ If your setup requires a different compressor or other post-processing
+ tool it will be fairly easy to implement a custom filter. Simply extend
+ from one of the available base classes.
+
+ More documentation about the usage and settings of Django Compressor can be
+ found on `django-compressor.readthedocs.org`_.
+
+ The source code for Django Compressor can be found and contributed to on
+ `github.com/django-compressor/django-compressor`_. There you can also file tickets.
+
+ The in-development version of Django Compressor can be installed with
+ ``pip install http://github.com/django-compressor/django-compressor/tarball/develop``.
+
+ .. _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
+ .. _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
+
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Topic :: Internet :: WWW/HTTP
--- /dev/null
+Django Compressor
+=================
+
+.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop
+ :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop
+
+.. image:: https://pypip.in/v/django_compressor/badge.png
+ :target: https://pypi.python.org/pypi/django_compressor
+
+.. image:: https://pypip.in/d/django_compressor/badge.png
+ :target: https://pypi.python.org/pypi/django_compressor
+
+.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop
+ :alt: Build Status
+ :target: http://travis-ci.org/django-compressor/django-compressor
+
+Django Compressor combines and compresses linked and inline Javascript
+or CSS in a Django template into cacheable static files by using the
+``compress`` template tag.
+
+HTML in between ``{% compress js/css %}`` and ``{% endcompress %}`` is
+parsed and searched for CSS or JS. These styles and scripts are subsequently
+processed with optional, configurable compilers and filters.
+
+The default filter for CSS rewrites paths to static files to be absolute
+and adds a cache busting timestamp. For Javascript the default filter
+compresses it using ``jsmin``.
+
+As the final result the template tag outputs a ``<script>`` or ``<link>``
+tag pointing to the optimized file. These files are stored inside a folder
+and given a unique name based on their content. Alternatively it can also
+return the resulting content to the original template directly.
+
+Since the file name is dependent on the content these files can be given
+a far future expiration date without worrying about stale browser caches.
+
+The concatenation and compressing process can also be jump started outside
+of the request/response cycle by using the Django management command
+``manage.py compress``.
+
+Configurability & Extendibility
+-------------------------------
+
+Django Compressor is highly configurable and extendible. The HTML parsing
+is done using lxml_ or if it's not available Python's built-in HTMLParser by
+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`_,
+`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
+`data URIs`_.
+
+If your setup requires a different compressor or other post-processing
+tool it will be fairly easy to implement a custom filter. Simply extend
+from one of the available base classes.
+
+More documentation about the usage and settings of Django Compressor can be
+found on `django-compressor.readthedocs.org`_.
+
+The source code for Django Compressor can be found and contributed to on
+`github.com/django-compressor/django-compressor`_. There you can also file tickets.
+
+The in-development version of Django Compressor can be installed with
+``pip install http://github.com/django-compressor/django-compressor/tarball/develop``.
+
+.. _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
+.. _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
+
--- /dev/null
+# following PEP 386
+__version__ = "1.4"
--- /dev/null
+from __future__ import with_statement, unicode_literals
+import os
+import codecs
+
+from django.core.files.base import ContentFile
+from django.template import Context
+from django.template.loader import render_to_string
+from django.utils.importlib import import_module
+from django.utils.safestring import mark_safe
+
+try:
+ from urllib.request import url2pathname
+except ImportError:
+ from urllib import url2pathname
+
+from compressor.cache import get_hexdigest, get_mtime
+from compressor.conf import settings
+from compressor.exceptions import (CompressorError, UncompressableFileError,
+ FilterDoesNotExist)
+from compressor.filters import CompilerFilter
+from compressor.storage import compressor_file_storage
+from compressor.signals import post_compress
+from compressor.utils import get_class, get_mod_func, staticfiles
+from compressor.utils.decorators import cached_property
+
+# Some constants for nicer handling.
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
+METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
+
+
+class Compressor(object):
+ """
+ Base compressor object to be subclassed for content type
+ depending implementations details.
+ """
+ type = None
+
+ def __init__(self, content=None, output_prefix=None, context=None, *args, **kwargs):
+ self.content = content or "" # rendered contents of {% compress %} tag
+ self.output_prefix = output_prefix or "compressed"
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ self.charset = settings.DEFAULT_CHARSET
+ self.split_content = []
+ self.context = context or {}
+ self.extra_context = {}
+ self.all_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
+ self.finders = staticfiles.finders
+ self._storage = None
+
+ @cached_property
+ def storage(self):
+ from compressor.storage import default_storage
+ return default_storage
+
+ def split_contents(self):
+ """
+ To be implemented in a subclass, should return an
+ iterable with four values: kind, value, basename, element
+ """
+ raise NotImplementedError
+
+ def get_template_name(self, mode):
+ """
+ Returns the template path for the given mode.
+ """
+ try:
+ template = getattr(self, "template_name_%s" % mode)
+ if template:
+ return template
+ except AttributeError:
+ pass
+ return "compressor/%s_%s.html" % (self.type, mode)
+
+ def get_basename(self, url):
+ """
+ Takes full path to a static file (eg. "/static/css/style.css") and
+ returns path with storage's base url removed (eg. "css/style.css").
+ """
+ try:
+ base_url = self.storage.base_url
+ except AttributeError:
+ base_url = settings.COMPRESS_URL
+ if not url.startswith(base_url):
+ raise UncompressableFileError("'%s' isn't accessible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
+ basename = url.replace(base_url, "", 1)
+ # drop the querystring, which is used for non-compressed cache-busting.
+ return basename.split("?", 1)[0]
+
+ def get_filepath(self, content, basename=None):
+ """
+ Returns file path for an output file based on contents.
+
+ Returned path is relative to compressor storage's base url, for
+ example "CACHE/css/e41ba2cc6982.css".
+
+ When `basename` argument is provided then file name (without extension)
+ will be used as a part of returned file name, for example:
+
+ get_filepath(content, "my_file.css") -> 'CACHE/css/my_file.e41ba2cc6982.css'
+ """
+ parts = []
+ if basename:
+ filename = os.path.split(basename)[1]
+ parts.append(os.path.splitext(filename)[0])
+ parts.extend([get_hexdigest(content, 12), self.type])
+ return os.path.join(self.output_dir, self.output_prefix, '.'.join(parts))
+
+ def get_filename(self, basename):
+ """
+ Returns full path to a file, for example:
+
+ get_filename('css/one.css') -> '/full/path/to/static/css/one.css'
+ """
+ filename = None
+ # first try finding the file in the root
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles (in debug mode)
+ if not filename and self.finders:
+ filename = self.finders.find(url2pathname(basename))
+ if filename:
+ return filename
+ # or just raise an exception as the last resort
+ raise UncompressableFileError(
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ with codecs.open(filename, 'r', charset) as fd:
+ try:
+ return fd.read()
+ except IOError as e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError as e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
+
+ @cached_property
+ def parser(self):
+ return get_class(settings.COMPRESS_PARSER)(self.content)
+
+ @cached_property
+ def cached_filters(self):
+ return [get_class(filter_cls) for filter_cls in self.filters]
+
+ @cached_property
+ def mtimes(self):
+ return [str(get_mtime(value))
+ for kind, value, basename, elem in self.split_contents()
+ if kind == SOURCE_FILE]
+
+ @cached_property
+ def cachekey(self):
+ return get_hexdigest(''.join(
+ [self.content] + self.mtimes).encode(self.charset), 12)
+
+ def hunks(self, forced=False):
+ """
+ The heart of content parsing, iterates over the
+ list of split contents and looks at its kind
+ to decide what to do with it. Should yield a
+ bunch of precompiled and/or rendered hunks.
+ """
+ enabled = settings.COMPRESS_ENABLED or forced
+
+ for kind, value, basename, elem in self.split_contents():
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ 'charset': charset,
+ }
+
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.all_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ yield self.filter(value, **options)
+ else:
+ if precompiled:
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
+ else:
+ yield self.parser.elem_str(elem)
+
+ def filter_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, method=METHOD_OUTPUT)
+
+ def filter_input(self, forced=False):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ content = []
+ for hunk in self.hunks(forced):
+ content.append(hunk)
+ return content
+
+ def precompile(self, content, kind=None, elem=None, filename=None,
+ charset=None, **kwargs):
+ """
+ Processes file using a pre compiler.
+
+ This is the place where files like coffee script are processed.
+ """
+ if not kind:
+ return False, content
+ attrs = self.parser.elem_attribs(elem)
+ mimetype = attrs.get("type", None)
+ if mimetype:
+ filter_or_command = self.all_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype not in ("text/css", "text/javascript"):
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+ else:
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except ImportError:
+ filter = CompilerFilter(
+ content, filter_type=self.type, filename=filename,
+ charset=charset, command=filter_or_command)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' %
+ filter_or_command)
+ else:
+ filter = precompiler_class(
+ content, attrs, filter_type=self.type, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ return False, content
+
+ def filter(self, content, method, **kwargs):
+ for filter_cls in self.cached_filters:
+ filter_func = getattr(
+ filter_cls(content, filter_type=self.type), method)
+ try:
+ if callable(filter_func):
+ content = filter_func(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ def output(self, mode='file', forced=False):
+ """
+ The general output method, override in subclass if you need to do
+ any custom modification. Calls other mode specific methods or simply
+ returns the content directly.
+ """
+ output = '\n'.join(self.filter_input(forced))
+
+ if not output:
+ return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_output = self.filter_output(output)
+ return self.handle_output(mode, filtered_output, forced)
+
+ return output
+
+ def handle_output(self, mode, content, forced, basename=None):
+ # Then check for the appropriate output method and call it
+ output_func = getattr(self, "output_%s" % mode, None)
+ if callable(output_func):
+ return output_func(mode, content, forced, basename)
+ # Total failure, raise a general exception
+ raise CompressorError(
+ "Couldn't find output method for mode '%s'" % mode)
+
+ def output_file(self, mode, content, forced=False, basename=None):
+ """
+ The output method that saves the content to a file and renders
+ the appropriate template with the file's URL.
+ """
+ new_filepath = self.get_filepath(content, basename=basename)
+ if not self.storage.exists(new_filepath) or forced:
+ self.storage.save(new_filepath, ContentFile(content.encode(self.charset)))
+ url = mark_safe(self.storage.url(new_filepath))
+ return self.render_output(mode, {"url": url})
+
+ def output_inline(self, mode, content, forced=False, basename=None):
+ """
+ The output method that directly returns the content for inline
+ display.
+ """
+ return self.render_output(mode, {"content": content})
+
+ def render_output(self, mode, context=None):
+ """
+ Renders the compressor output with the appropriate template for
+ the given mode and template context.
+ """
+ # Just in case someone renders the compressor outside
+ # the usual template rendering cycle
+ if 'compressed' not in self.context:
+ self.context['compressed'] = {}
+
+ self.context['compressed'].update(context or {})
+ self.context['compressed'].update(self.extra_context)
+ final_context = Context(self.context)
+ post_compress.send(sender=self.__class__, type=self.type,
+ mode=mode, context=final_context)
+ template_name = self.get_template_name(mode)
+ return render_to_string(template_name, context_instance=final_context)
--- /dev/null
+import json
+import hashlib
+import os
+import socket
+import time
+
+from django.core.cache import get_cache
+from django.core.files.base import ContentFile
+from django.utils.encoding import force_text, smart_bytes
+from django.utils.functional import SimpleLazyObject
+from django.utils.importlib import import_module
+
+from compressor.conf import settings
+from compressor.storage import default_storage
+from compressor.utils import get_mod_func
+
+_cachekey_func = None
+
+
+def get_hexdigest(plaintext, length=None):
+ digest = hashlib.md5(smart_bytes(plaintext)).hexdigest()
+ if length:
+ return digest[:length]
+ return digest
+
+
+def simple_cachekey(key):
+ return 'django_compressor.%s' % force_text(key)
+
+
+def socket_cachekey(key):
+ return 'django_compressor.%s.%s' % (socket.gethostname(), force_text(key))
+
+
+def get_cachekey(*args, **kwargs):
+ global _cachekey_func
+ if _cachekey_func is None:
+ try:
+ mod_name, func_name = get_mod_func(
+ settings.COMPRESS_CACHE_KEY_FUNCTION)
+ _cachekey_func = getattr(import_module(mod_name), func_name)
+ except (AttributeError, ImportError) as e:
+ raise ImportError("Couldn't import cache key function %s: %s" %
+ (settings.COMPRESS_CACHE_KEY_FUNCTION, e))
+ return _cachekey_func(*args, **kwargs)
+
+
+def get_mtime_cachekey(filename):
+ return get_cachekey("mtime.%s" % get_hexdigest(filename))
+
+
+def get_offline_hexdigest(render_template_string):
+ return get_hexdigest(render_template_string)
+
+
+def get_offline_cachekey(source):
+ return get_cachekey("offline.%s" % get_offline_hexdigest(source))
+
+
+def get_offline_manifest_filename():
+ output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ return os.path.join(output_dir, settings.COMPRESS_OFFLINE_MANIFEST)
+
+
+_offline_manifest = None
+
+
+def get_offline_manifest():
+ global _offline_manifest
+ if _offline_manifest is None:
+ filename = get_offline_manifest_filename()
+ if default_storage.exists(filename):
+ with default_storage.open(filename) as fp:
+ _offline_manifest = json.loads(fp.read().decode('utf8'))
+ else:
+ _offline_manifest = {}
+ return _offline_manifest
+
+
+def flush_offline_manifest():
+ global _offline_manifest
+ _offline_manifest = None
+
+
+def write_offline_manifest(manifest):
+ filename = get_offline_manifest_filename()
+ content = json.dumps(manifest, indent=2).encode('utf8')
+ default_storage.save(filename, ContentFile(content))
+ flush_offline_manifest()
+
+
+def get_templatetag_cachekey(compressor, mode, kind):
+ return get_cachekey(
+ "templatetag.%s.%s.%s" % (compressor.cachekey, mode, kind))
+
+
+def get_mtime(filename):
+ if settings.COMPRESS_MTIME_DELAY:
+ key = get_mtime_cachekey(filename)
+ mtime = cache.get(key)
+ if mtime is None:
+ mtime = os.path.getmtime(filename)
+ cache.set(key, mtime, settings.COMPRESS_MTIME_DELAY)
+ return mtime
+ return os.path.getmtime(filename)
+
+
+def get_hashed_mtime(filename, length=12):
+ try:
+ filename = os.path.realpath(filename)
+ mtime = str(int(get_mtime(filename)))
+ except OSError:
+ return None
+ return get_hexdigest(mtime, length)
+
+
+def get_hashed_content(filename, length=12):
+ try:
+ filename = os.path.realpath(filename)
+ except OSError:
+ return None
+
+ # should we make sure that file is utf-8 encoded?
+ with open(filename, 'rb') as file:
+ return get_hexdigest(file.read(), length)
+
+
+def cache_get(key):
+ packed_val = cache.get(key)
+ if packed_val is None:
+ return None
+ val, refresh_time, refreshed = packed_val
+ if (time.time() > refresh_time) and not refreshed:
+ # Store the stale value while the cache
+ # revalidates for another MINT_DELAY seconds.
+ cache_set(key, val, refreshed=True,
+ timeout=settings.COMPRESS_MINT_DELAY)
+ return None
+ return val
+
+
+def cache_set(key, val, refreshed=False, timeout=None):
+ if timeout is None:
+ timeout = settings.COMPRESS_REBUILD_TIMEOUT
+ refresh_time = timeout + time.time()
+ real_timeout = timeout + settings.COMPRESS_MINT_DELAY
+ packed_val = (val, refresh_time, refreshed)
+ return cache.set(key, packed_val, real_timeout)
+
+
+cache = SimpleLazyObject(lambda: get_cache(settings.COMPRESS_CACHE_BACKEND))
--- /dev/null
+from __future__ import unicode_literals
+import os
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+
+from appconf import AppConf
+
+
+class CompressorConf(AppConf):
+ # Main switch
+ ENABLED = not settings.DEBUG
+ # Allows changing verbosity from the settings.
+ VERBOSE = False
+ # GET variable that disables compressor e.g. "nocompress"
+ DEBUG_TOGGLE = None
+ # the backend to use when parsing the JavaScript or Stylesheet files
+ PARSER = 'compressor.parser.AutoSelectParser'
+ OUTPUT_DIR = 'CACHE'
+ STORAGE = 'compressor.storage.CompressorFileStorage'
+
+ CSS_COMPRESSOR = 'compressor.css.CssCompressor'
+ JS_COMPRESSOR = 'compressor.js.JsCompressor'
+
+ URL = None
+ ROOT = None
+
+ CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter']
+ CSS_HASHING_METHOD = 'mtime'
+
+ JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
+ PRECOMPILERS = (
+ # ('text/coffeescript', 'coffee --compile --stdio'),
+ # ('text/less', 'lessc {infile} {outfile}'),
+ # ('text/x-sass', 'sass {infile} {outfile}'),
+ # ('text/stylus', 'stylus < {infile} > {outfile}'),
+ # ('text/x-scss', 'sass --scss {infile} {outfile}'),
+ )
+ 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 = ''
+ YUGLIFY_BINARY = 'yuglify'
+ YUGLIFY_CSS_ARGUMENTS = '--terminal'
+ YUGLIFY_JS_ARGUMENTS = '--terminal'
+ DATA_URI_MAX_SIZE = 1024
+
+ # the cache backend to use
+ CACHE_BACKEND = None
+ # the dotted path to the function that creates the cache key
+ CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
+ # rebuilds the cache every 30 days if nothing has changed.
+ REBUILD_TIMEOUT = 60 * 60 * 24 * 30 # 30 days
+ # the upper bound on how long any compression should take to be generated
+ # (used against dog piling, should be a lot smaller than REBUILD_TIMEOUT
+ MINT_DELAY = 30 # seconds
+ # check for file changes only after a delay
+ MTIME_DELAY = 10 # seconds
+ # enables the offline cache -- also filled by the compress command
+ OFFLINE = False
+ # invalidates the offline cache after one year
+ OFFLINE_TIMEOUT = 60 * 60 * 24 * 365 # 1 year
+ # The context to be used when compressing the files "offline"
+ OFFLINE_CONTEXT = {}
+ # The name of the manifest file (e.g. filename.ext)
+ OFFLINE_MANIFEST = 'manifest.json'
+ # The Context to be used when TemplateFilter is used
+ TEMPLATE_FILTER_CONTEXT = {}
+ # Function that returns the Jinja2 environment to use in offline compression.
+ def JINJA2_GET_ENVIRONMENT():
+ try:
+ import jinja2
+ return jinja2.Environment()
+ except ImportError:
+ return None
+
+ class Meta:
+ prefix = 'compress'
+
+ def configure_root(self, value):
+ # Uses Django's STATIC_ROOT by default
+ if value is None:
+ value = settings.STATIC_ROOT
+ if value is None:
+ raise ImproperlyConfigured('COMPRESS_ROOT defaults to ' +
+ 'STATIC_ROOT, please define either')
+ return os.path.normcase(os.path.abspath(value))
+
+ def configure_url(self, value):
+ # Uses Django's STATIC_URL by default
+ if value is None:
+ value = settings.STATIC_URL
+ if not value.endswith('/'):
+ raise ImproperlyConfigured("URL settings (e.g. COMPRESS_URL) "
+ "must have a trailing slash")
+ return value
+
+ def configure_cache_backend(self, value):
+ if value is None:
+ value = 'default'
+ return value
+
+ def configure_offline_context(self, value):
+ if not value:
+ value = {'STATIC_URL': settings.STATIC_URL}
+ return value
+
+ def configure_template_filter_context(self, value):
+ if not value:
+ value = {'STATIC_URL': settings.STATIC_URL}
+ return value
+
+ def configure_precompilers(self, value):
+ if not isinstance(value, (list, tuple)):
+ raise ImproperlyConfigured("The COMPRESS_PRECOMPILERS setting "
+ "must be a list or tuple. Check for "
+ "missing commas.")
+ return value
--- /dev/null
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.exceptions import TemplateSyntaxError
+
+from compressor.templatetags.compress import OUTPUT_FILE, CompressorMixin
+
+
+class CompressorExtension(CompressorMixin, Extension):
+
+ tags = set(['compress'])
+
+ def parse(self, parser):
+ lineno = next(parser.stream).lineno
+ kindarg = parser.parse_expression()
+ # Allow kind to be defined as jinja2 name node
+ if isinstance(kindarg, nodes.Name):
+ kindarg = nodes.Const(kindarg.name)
+ args = [kindarg]
+ if args[0].value not in self.compressors:
+ raise TemplateSyntaxError('compress kind may be one of: %s' %
+ (', '.join(self.compressors.keys())),
+ lineno)
+ if parser.stream.skip_if('comma'):
+ modearg = parser.parse_expression()
+ # Allow mode to be defined as jinja2 name node
+ if isinstance(modearg, nodes.Name):
+ modearg = nodes.Const(modearg.name)
+ args.append(modearg)
+ else:
+ args.append(nodes.Const('file'))
+
+ body = parser.parse_statements(['name:endcompress'], drop_needle=True)
+
+ # Skip the kind if used in the endblock, by using the kind in the
+ # endblock the templates are slightly more readable.
+ parser.stream.skip_if('name:' + kindarg.value)
+ return nodes.CallBlock(self.call_method('_compress_normal', args), [], [],
+ body).set_lineno(lineno)
+
+ def _compress_forced(self, kind, mode, caller):
+ return self._compress(kind, mode, caller, True)
+
+ def _compress_normal(self, kind, mode, caller):
+ return self._compress(kind, mode, caller, False)
+
+ def _compress(self, kind, mode, caller, forced):
+ mode = mode or OUTPUT_FILE
+ original_content = caller()
+ context = {
+ 'original_content': original_content
+ }
+ return self.render_compressed(context, kind, mode, forced=forced)
+
+ def get_original_content(self, context):
+ return context['original_content']
--- /dev/null
+"""
+ source: https://gist.github.com/1311010
+ Get django-sekizai, django-compessor (and django-cms) playing nicely together
+ re: https://github.com/ojii/django-sekizai/issues/4
+ using: https://github.com/django-compressor/django-compressor.git
+ and: https://github.com/ojii/django-sekizai.git@0.6 or later
+"""
+from compressor.templatetags.compress import CompressorNode
+from django.template.base import Template
+
+
+def compress(context, data, name):
+ """
+ Data is the string from the template (the list of js files in this case)
+ Name is either 'js' or 'css' (the sekizai namespace)
+ Basically passes the string through the {% compress 'js' %} template tag
+ """
+ return CompressorNode(nodelist=Template(data).nodelist, kind=name, mode='file').render(context=context)
--- /dev/null
+from compressor.base import Compressor, SOURCE_HUNK, SOURCE_FILE
+from compressor.conf import settings
+
+
+class CssCompressor(Compressor):
+
+ def __init__(self, content=None, output_prefix="css", context=None):
+ super(CssCompressor, self).__init__(content=content,
+ output_prefix=output_prefix, context=context)
+ self.filters = list(settings.COMPRESS_CSS_FILTERS)
+ self.type = output_prefix
+
+ def split_contents(self):
+ if self.split_content:
+ return self.split_content
+ self.media_nodes = []
+ for elem in self.parser.css_elems():
+ data = None
+ elem_name = self.parser.elem_name(elem)
+ elem_attribs = self.parser.elem_attribs(elem)
+ if elem_name == 'link' and elem_attribs['rel'].lower() == 'stylesheet':
+ basename = self.get_basename(elem_attribs['href'])
+ filename = self.get_filename(basename)
+ data = (SOURCE_FILE, filename, basename, elem)
+ elif elem_name == 'style':
+ data = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem)
+ if data:
+ self.split_content.append(data)
+ media = elem_attribs.get('media', None)
+ # Append to the previous node if it had the same media type
+ append_to_previous = self.media_nodes and self.media_nodes[-1][0] == media
+ # and we are not just precompiling, otherwise create a new node.
+ if append_to_previous and settings.COMPRESS_ENABLED:
+ self.media_nodes[-1][1].split_content.append(data)
+ else:
+ node = self.__class__(content=self.parser.elem_str(elem),
+ context=self.context)
+ node.split_content.append(data)
+ self.media_nodes.append((media, node))
+ return self.split_content
+
+ def output(self, *args, **kwargs):
+ if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or
+ kwargs.get('forced', False)):
+ # Populate self.split_content
+ self.split_contents()
+ if hasattr(self, 'media_nodes'):
+ ret = []
+ for media, subnode in self.media_nodes:
+ subnode.extra_context.update({'media': media})
+ ret.append(subnode.output(*args, **kwargs))
+ return ''.join(ret)
+ return super(CssCompressor, self).output(*args, **kwargs)
--- /dev/null
+class CompressorError(Exception):
+ """
+ A general error of the compressor
+ """
+ pass
+
+
+class UncompressableFileError(Exception):
+ """
+ This exception is raised when a file cannot be compressed
+ """
+ pass
+
+
+class FilterError(Exception):
+ """
+ This exception is raised when a filter fails
+ """
+ pass
+
+
+class ParserError(Exception):
+ """
+ This exception is raised when the parser fails
+ """
+ pass
+
+
+class OfflineGenerationError(Exception):
+ """
+ Offline compression generation related exceptions
+ """
+ pass
+
+
+class FilterDoesNotExist(Exception):
+ """
+ Raised when a filter class cannot be found.
+ """
+ pass
+
+
+class TemplateDoesNotExist(Exception):
+ """
+ This exception is raised when a template does not exist.
+ """
+ pass
+
+
+class TemplateSyntaxError(Exception):
+ """
+ This exception is raised when a template syntax error is encountered.
+ """
+ pass
--- /dev/null
+# flake8: noqa
+from compressor.filters.base import (FilterBase, CallbackOutputFilter,
+ CompilerFilter, FilterError)
--- /dev/null
+from __future__ import absolute_import, unicode_literals
+import io
+import logging
+import subprocess
+
+from django.core.exceptions import ImproperlyConfigured
+from django.core.files.temp import NamedTemporaryFile
+from django.utils.importlib import import_module
+from django.utils.encoding import smart_text
+from django.utils import six
+
+from compressor.conf import settings
+from compressor.exceptions import FilterError
+from compressor.utils import get_mod_func
+
+
+logger = logging.getLogger("compressor.filters")
+
+
+class FilterBase(object):
+ """
+ A base class for filters that does nothing.
+
+ Subclasses should implement `input` and/or `output` methods which must
+ return a string (unicode under python 2) or raise a NotImplementedError.
+ """
+ def __init__(self, content, filter_type=None, filename=None, verbose=0,
+ charset=None):
+ self.type = filter_type
+ self.content = content
+ self.verbose = verbose or settings.COMPRESS_VERBOSE
+ self.logger = logger
+ self.filename = filename
+ self.charset = charset
+
+ def input(self, **kwargs):
+ raise NotImplementedError
+
+ def output(self, **kwargs):
+ raise NotImplementedError
+
+
+class CallbackOutputFilter(FilterBase):
+ """
+ A filter which takes function path in `callback` attribute, imports it
+ and uses that function to filter output string::
+
+ class MyFilter(CallbackOutputFilter):
+ callback = 'path.to.my.callback'
+
+ Callback should be a function which takes a string as first argument and
+ returns a string (unicode under python 2).
+ """
+ callback = None
+ args = []
+ kwargs = {}
+ dependencies = []
+
+ def __init__(self, *args, **kwargs):
+ super(CallbackOutputFilter, self).__init__(*args, **kwargs)
+ if self.callback is None:
+ raise ImproperlyConfigured(
+ "The callback filter %s must define a 'callback' attribute." %
+ self.__class__.__name__)
+ try:
+ mod_name, func_name = get_mod_func(self.callback)
+ func = getattr(import_module(mod_name), func_name)
+ except ImportError:
+ if self.dependencies:
+ if len(self.dependencies) == 1:
+ warning = "dependency (%s) is" % self.dependencies[0]
+ else:
+ warning = ("dependencies (%s) are" %
+ ", ".join([dep for dep in self.dependencies]))
+ else:
+ warning = ""
+ raise ImproperlyConfigured(
+ "The callback %s couldn't be imported. Make sure the %s "
+ "correctly installed." % (self.callback, warning))
+ except AttributeError as e:
+ raise ImproperlyConfigured("An error occurred while importing the "
+ "callback filter %s: %s" % (self, e))
+ else:
+ self._callback_func = func
+
+ def output(self, **kwargs):
+ ret = self._callback_func(self.content, *self.args, **self.kwargs)
+ assert isinstance(ret, six.text_type)
+ return ret
+
+
+class CompilerFilter(FilterBase):
+ """
+ A filter subclass that is able to filter content via
+ external commands.
+ """
+ command = None
+ options = ()
+ default_encoding = settings.FILE_CHARSET
+
+ def __init__(self, content, command=None, *args, **kwargs):
+ super(CompilerFilter, self).__init__(content, *args, **kwargs)
+ self.cwd = None
+
+ if command:
+ self.command = command
+ if self.command is None:
+ raise FilterError("Required attribute 'command' not given")
+
+ if isinstance(self.options, dict):
+ # turn dict into a tuple
+ new_options = ()
+ for item in kwargs.items():
+ new_options += (item,)
+ self.options = new_options
+
+ # append kwargs to self.options
+ for item in kwargs.items():
+ self.options += (item,)
+
+ self.stdout = self.stdin = self.stderr = subprocess.PIPE
+ self.infile = self.outfile = None
+
+ def input(self, **kwargs):
+ encoding = self.default_encoding
+ options = dict(self.options)
+
+ if self.infile is None and "{infile}" in self.command:
+ # create temporary input file if needed
+ if self.filename is None:
+ self.infile = NamedTemporaryFile(mode='wb')
+ self.infile.write(self.content.encode(encoding))
+ self.infile.flush()
+ options["infile"] = self.infile.name
+ else:
+ # we use source file directly, which may be encoded using
+ # something different than utf8. If that's the case file will
+ # be included with charset="something" html attribute and
+ # charset will be available as filter's charset attribute
+ encoding = self.charset # or self.default_encoding
+ self.infile = open(self.filename)
+ options["infile"] = self.filename
+
+ if "{outfile}" in self.command and "outfile" not in options:
+ # create temporary output file if needed
+ ext = self.type and ".%s" % self.type or ""
+ self.outfile = NamedTemporaryFile(mode='r+', suffix=ext)
+ options["outfile"] = self.outfile.name
+
+ try:
+ command = self.command.format(**options)
+ proc = subprocess.Popen(
+ command, shell=True, cwd=self.cwd, stdout=self.stdout,
+ stdin=self.stdin, stderr=self.stderr)
+ if self.infile is None:
+ # if infile is None then send content to process' stdin
+ filtered, err = proc.communicate(
+ self.content.encode(encoding))
+ else:
+ filtered, err = proc.communicate()
+ filtered, err = filtered.decode(encoding), err.decode(encoding)
+ except (IOError, OSError) as e:
+ raise FilterError('Unable to apply %s (%r): %s' %
+ (self.__class__.__name__, self.command, e))
+ else:
+ if proc.wait() != 0:
+ # command failed, raise FilterError exception
+ if not err:
+ err = ('Unable to apply %s (%s)' %
+ (self.__class__.__name__, self.command))
+ if filtered:
+ err += '\n%s' % filtered
+ raise FilterError(err)
+
+ if self.verbose:
+ self.logger.debug(err)
+
+ outfile_path = options.get('outfile')
+ if outfile_path:
+ with io.open(outfile_path, 'r', encoding=encoding) as file:
+ filtered = file.read()
+ finally:
+ if self.infile is not None:
+ self.infile.close()
+ if self.outfile is not None:
+ self.outfile.close()
+
+ return smart_text(filtered)
--- /dev/null
+from compressor.conf import settings
+from compressor.filters import CompilerFilter
+
+
+class ClosureCompilerFilter(CompilerFilter):
+ command = "{binary} {args}"
+ options = (
+ ("binary", settings.COMPRESS_CLOSURE_COMPILER_BINARY),
+ ("args", settings.COMPRESS_CLOSURE_COMPILER_ARGUMENTS),
+ )
--- /dev/null
+import os
+import re
+import posixpath
+
+from compressor.cache import get_hashed_mtime, get_hashed_content
+from compressor.conf import settings
+from compressor.filters import FilterBase, FilterError
+from compressor.utils import staticfiles
+
+URL_PATTERN = re.compile(r'url\(([^\)]+)\)')
+SRC_PATTERN = re.compile(r'src=([\'"])(.+?)\1')
+SCHEMES = ('http://', 'https://', '/', 'data:')
+
+
+class CssAbsoluteFilter(FilterBase):
+
+ def __init__(self, *args, **kwargs):
+ super(CssAbsoluteFilter, self).__init__(*args, **kwargs)
+ self.root = settings.COMPRESS_ROOT
+ self.url = settings.COMPRESS_URL.rstrip('/')
+ self.url_path = self.url
+ self.has_scheme = False
+
+ def input(self, filename=None, basename=None, **kwargs):
+ if filename is not None:
+ filename = os.path.normcase(os.path.abspath(filename))
+ if (not (filename and filename.startswith(self.root)) and
+ not self.find(basename)):
+ return self.content
+ self.path = basename.replace(os.sep, '/')
+ self.path = self.path.lstrip('/')
+ if self.url.startswith(('http://', 'https://')):
+ self.has_scheme = True
+ parts = self.url.split('/')
+ self.url = '/'.join(parts[2:])
+ self.url_path = '/%s' % '/'.join(parts[3:])
+ self.protocol = '%s/' % '/'.join(parts[:2])
+ self.host = parts[2]
+ self.directory_name = '/'.join((self.url, os.path.dirname(self.path)))
+ return SRC_PATTERN.sub(self.src_converter,
+ URL_PATTERN.sub(self.url_converter, self.content))
+
+ def find(self, basename):
+ if settings.DEBUG and basename and staticfiles.finders:
+ return staticfiles.finders.find(basename)
+
+ def guess_filename(self, url):
+ local_path = url
+ if self.has_scheme:
+ # COMPRESS_URL had a protocol,
+ # remove it and the hostname from our path.
+ local_path = local_path.replace(self.protocol + self.host, "", 1)
+ # remove url fragment, if any
+ local_path = local_path.rsplit("#", 1)[0]
+ # remove querystring, if any
+ local_path = local_path.rsplit("?", 1)[0]
+ # Now, we just need to check if we can find
+ # the path from COMPRESS_URL in our url
+ if local_path.startswith(self.url_path):
+ local_path = local_path.replace(self.url_path, "", 1)
+ # Re-build the local full path by adding root
+ filename = os.path.join(self.root, local_path.lstrip('/'))
+ return os.path.exists(filename) and filename
+
+ def add_suffix(self, url):
+ filename = self.guess_filename(url)
+ suffix = None
+ if filename:
+ if settings.COMPRESS_CSS_HASHING_METHOD == "mtime":
+ suffix = get_hashed_mtime(filename)
+ elif settings.COMPRESS_CSS_HASHING_METHOD in ("hash", "content"):
+ suffix = get_hashed_content(filename)
+ else:
+ raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured '
+ 'with an unknown method (%s).' %
+ settings.COMPRESS_CSS_HASHING_METHOD)
+ if suffix is None:
+ return url
+ if url.startswith(SCHEMES):
+ fragment = None
+ if "#" in url:
+ url, fragment = url.rsplit("#", 1)
+ if "?" in url:
+ url = "%s&%s" % (url, suffix)
+ else:
+ url = "%s?%s" % (url, suffix)
+ if fragment is not None:
+ url = "%s#%s" % (url, fragment)
+ return url
+
+ def _converter(self, matchobj, group, template):
+ url = matchobj.group(group)
+ url = url.strip(' \'"')
+ if url.startswith('#'):
+ return "url('%s')" % url
+ elif url.startswith(SCHEMES):
+ return "url('%s')" % self.add_suffix(url)
+ full_url = posixpath.normpath('/'.join([str(self.directory_name),
+ url]))
+ if self.has_scheme:
+ full_url = "%s%s" % (self.protocol, full_url)
+ return template % self.add_suffix(full_url)
+
+ def url_converter(self, matchobj):
+ return self._converter(matchobj, 1, "url('%s')")
+
+ def src_converter(self, matchobj):
+ return self._converter(matchobj, 2, "src='%s'")
--- /dev/null
+from compressor.filters import CallbackOutputFilter
+
+
+class CSSMinFilter(CallbackOutputFilter):
+ """
+ A filter that utilizes Zachary Voase's Python port of
+ the YUI CSS compression algorithm: http://pypi.python.org/pypi/cssmin/
+ """
+ callback = "compressor.filters.cssmin.cssmin.cssmin"
+
+
+class rCSSMinFilter(CallbackOutputFilter):
+ callback = "compressor.filters.cssmin.rcssmin.cssmin"
--- /dev/null
+#!/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()
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: ascii -*-
+#
+# Copyright 2011, 2012
+# Andr\xe9 Malo or his licensors, as applicable
+#
+# 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.
+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`_\.
+
+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 50 or so (depending on the input).
+
+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/tree/
+"""
+__author__ = "Andr\xe9 Malo"
+__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
+__docformat__ = "restructuredtext en"
+__license__ = "Apache License, Version 2.0"
+__version__ = '1.0.2'
+__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 = W0612
+ # ("unused" variables)
+
+ # pylint: disable = R0911, R0912, R0914, R0915
+ # (too many anything)
+
+ 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'(?:'
+ 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((
+ r'([^\\"\047u>@\r\n\f\040\t/;:{}]+)'
+ r'|(?<=[{}(=:>+[,!])(%(space)s+)'
+ r'|^(%(space)s+)'
+ r'|(%(space)s+)(?=(([:{});=>+\],!])|$)?)'
+ r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)'
+ r'|(\{)'
+ r'|(\})'
+ r'|(%(strings)s)'
+ r'|(?<!%(nmchar)s)url\(%(spacechar)s*('
+ r'%(uri_nl_strings)s'
+ r'|%(uri)s'
+ r')%(spacechar)s*\)'
+ r'|(@[mM][eE][dD][iI][aA])(?!%(nmchar)s)'
+ r'|(%(ie7hack)s)(%(space)s*)'
+ r'|(:[fF][iI][rR][sS][tT]-[lL]'
+ r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'
+ r'(%(space)s*)(?=[{,])'
+ r'|(%(nl_strings)s)'
+ r'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}]*)'
+ ) % locals()).sub
+
+ # print main_sub.__self__.pattern
+
+ def main_subber(keep_bang_comments):
+ """ Make main subber """
+ in_macie5, in_rule, at_media = [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_media[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(group):
+ """ { handler """
+ # pylint: disable = W0613
+ if at_media[0]:
+ at_media[0] -= 1
+ else:
+ in_rule[0] = 1
+ return '{'
+
+ def fn_close(group):
+ """ } handler """
+ # pylint: disable = W0613
+ in_rule[0] = 0
+ return '}'
+
+ def fn_media(group):
+ """ @media handler """
+ at_media[0] += 1
+ return group(13)
+
+ def fn_ie7hack(group):
+ """ IE7 Hack handler """
+ if not in_rule[0] and not at_media[0]:
+ in_macie5[0] = 0
+ return group(14) + space_sub(space_subber, group(15))
+ return '>' + space_sub(space_subber, group(15))
+
+ table = (
+ 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_media, # @media
+ 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()
--- /dev/null
+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),
+ )
--- /dev/null
+from __future__ import unicode_literals
+import os
+import re
+import mimetypes
+from base64 import b64encode
+
+from compressor.conf import settings
+from compressor.filters import FilterBase
+
+
+class DataUriFilter(FilterBase):
+ """Filter for embedding media as data: URIs.
+
+ Settings:
+ COMPRESS_DATA_URI_MAX_SIZE: Only files that are smaller than this
+ value will be embedded. Unit; bytes.
+
+
+ Don't use this class directly. Use a subclass.
+ """
+ def input(self, filename=None, **kwargs):
+ if not filename or not filename.startswith(settings.COMPRESS_ROOT):
+ return self.content
+ output = self.content
+ for url_pattern in self.url_patterns:
+ output = url_pattern.sub(self.data_uri_converter, output)
+ return output
+
+ def get_file_path(self, url):
+ # strip query string of file paths
+ if "?" in url:
+ url = url.split("?")[0]
+ if "#" in url:
+ url = url.split("#")[0]
+ return os.path.join(
+ settings.COMPRESS_ROOT, url[len(settings.COMPRESS_URL):])
+
+ def data_uri_converter(self, matchobj):
+ url = matchobj.group(1).strip(' \'"')
+ if not url.startswith('data:') and not url.startswith('//'):
+ path = self.get_file_path(url)
+ if os.stat(path).st_size <= settings.COMPRESS_DATA_URI_MAX_SIZE:
+ with open(path, 'rb') as file:
+ data = b64encode(file.read()).decode('ascii')
+ return 'url("data:%s;base64,%s")' % (
+ mimetypes.guess_type(path)[0], data)
+ return 'url("%s")' % url
+
+
+class CssDataUriFilter(DataUriFilter):
+ """Filter for embedding media as data: URIs in CSS files.
+
+ See DataUriFilter.
+ """
+ url_patterns = (
+ re.compile(r'url\(([^\)]+)\)'),
+ )
--- /dev/null
+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"
+
+# This is for backwards compatibility
+JSMinFilter = rJSMinFilter
--- /dev/null
+#!/usr/bin/env python
+# -*- coding: ascii -*-
+#
+# Copyright 2011 - 2013
+# Andr\xe9 Malo or his licensors, as applicable
+#
+# 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.
+r"""
+=====================
+ Javascript Minifier
+=====================
+
+rJSmin is a javascript minifier written in python.
+
+The minifier is based on the semantics of `jsmin.c by Douglas Crockford`_\.
+
+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.
+- "+ +" and "- -" sequences are not collapsed to '++' or '--'
+- Newlines before ! operators are removed more sensibly
+- 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 1.2 and 7. 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
+"""
+__author__ = "Andr\xe9 Malo"
+__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
+__docformat__ = "restructuredtext en"
+__license__ = "Apache License, Version 2.0"
+__version__ = '1.0.7'
+__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 = R0912, R0914, W0612
+ if not python_only:
+ try:
+ import _rjsmin
+ except ImportError:
+ pass
+ else:
+ return _rjsmin.jsmin
+ try:
+ xrange
+ except NameError:
+ xrange = range # pylint: disable = W0622
+
+ space_chars = r'[\000-\011\013\014\016-\040]'
+
+ line_comment = r'(?://[^\r\n]*)'
+ space_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(first2),
+ last2 > first2 + 1 and '-' or '',
+ last2 != first2 and chr(last2) or ''
+ ) for first2, last2 in result])
+
+ return _re.sub(r'([\000-\040\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+-]')
+
+ dull = r'[^\047"/\000-\040]'
+
+ space_sub = _re.compile((
+ r'(%(dull)s+)'
+ r'|(%(strings)s%(dull)s*)'
+ r'|(?<=%(preregex1)s)'
+ r'%(space)s*(?:%(newline)s%(space)s*)*'
+ r'(%(regex)s%(dull)s*)'
+ r'|(?<=%(preregex2)s)'
+ r'%(space)s*(?:%(newline)s%(space)s)*'
+ r'(%(regex)s%(dull)s*)'
+ r'|(?<=%(id_literal_close)s)'
+ r'%(space)s*(?:(%(newline)s)%(space)s*)+'
+ r'(?=%(id_literal_open)s)'
+ r'|(?<=%(id_literal)s)(%(space)s)+(?=%(id_literal)s)'
+ r'|(?<=\+)(%(space)s)+(?=\+)'
+ r'|(?<=-)(%(space)s)+(?=-)'
+ r'|%(space)s+'
+ r'|(?:%(newline)s%(space)s*)+'
+ ) % locals()).sub
+ # print space_sub.__self__.pattern
+
+ def space_subber(match):
+ """ Substitution callback """
+ # pylint: disable = C0321, R0911
+ groups = match.groups()
+ if groups[0]: return groups[0]
+ elif groups[1]: return groups[1]
+ elif groups[2]: return groups[2]
+ elif groups[3]: return groups[3]
+ elif groups[4]: return '\n'
+ elif groups[5] or groups[6] or groups[7]: return ' '
+ else: return ''
+
+ def jsmin(script): # pylint: disable = W0621
+ 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
+
+ :Return: Minified script
+ :Rtype: ``str``
+ """
+ return space_sub(space_subber, '\n%s\n' % script).strip()
+
+ return jsmin
+
+jsmin = _make_jsmin()
+
+
+def jsmin_for_posers(script):
+ 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 regex. It's just for fun here and may
+ vanish any time. Use the `jsmin` function instead.
+
+ :Parameters:
+ `script` : ``str``
+ Script to minify
+
+ :Return: Minified script
+ :Rtype: ``str``
+ """
+ def subber(match):
+ """ Substitution callback """
+ groups = match.groups()
+ return (
+ groups[0] or
+ groups[1] or
+ groups[2] or
+ groups[3] or
+ (groups[4] and '\n') or
+ (groups[5] and ' ') or
+ (groups[6] and ' ') or
+ (groups[7] and ' ') or
+ ''
+ )
+
+ return _re.sub(
+ r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
+ r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
+ r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?<=[(,=:\[!&|?{};\r\n])(?'
+ r':[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*'
+ r'(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*'
+ r'[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:('
+ r'?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\['
+ r'\r\n]*)*/)[^\047"/\000-\040]*)|(?<=[\000-#%-,./:-@\[-^`{-~-]return'
+ r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
+ r'))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:'
+ r'/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?'
+ r':(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/'
+ r'\\\[\r\n]*)*/)[^\047"/\000-\040]*)|(?<=[^\000-!#%&(*,./:-@\[\\^`{|'
+ r'~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)'
+ r'*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040]'
+ r'|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#%-\047)*,./'
+ r':-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-\011\013\01'
+ r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^\000-#%-,./:'
+ r'-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*'
+ r'\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\013\014\016-'
+ r'\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\000-\011\013'
+ r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:(?:(?://[^'
+ r'\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^'
+ r'/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
+ ).strip()
+
+
+if __name__ == '__main__':
+ import sys as _sys
+ _sys.stdout.write(jsmin(_sys.stdin.read()))
--- /dev/null
+from __future__ import absolute_import
+from compressor.filters import CallbackOutputFilter
+
+
+class SlimItFilter(CallbackOutputFilter):
+ dependencies = ["slimit"]
+ callback = "slimit.minify"
+ kwargs = {
+ "mangle": True,
+ }
--- /dev/null
+from django.template import Template, Context
+from django.conf import settings
+
+from compressor.filters import FilterBase
+
+
+class TemplateFilter(FilterBase):
+
+ def input(self, filename=None, basename=None, **kwargs):
+ template = Template(self.content)
+ context = Context(settings.COMPRESS_TEMPLATE_FILTER_CONTEXT)
+ return template.render(context)
--- /dev/null
+from compressor.conf import settings
+from compressor.filters import CompilerFilter
+
+
+class YUglifyFilter(CompilerFilter):
+ command = "{binary} {args}"
+
+ def __init__(self, *args, **kwargs):
+ super(YUglifyFilter, self).__init__(*args, **kwargs)
+ self.command += ' --type=%s' % self.type
+
+
+class YUglifyCSSFilter(YUglifyFilter):
+ type = 'css'
+ options = (
+ ("binary", settings.COMPRESS_YUGLIFY_BINARY),
+ ("args", settings.COMPRESS_YUGLIFY_CSS_ARGUMENTS),
+ )
+
+
+class YUglifyJSFilter(YUglifyFilter):
+ type = 'js'
+ options = (
+ ("binary", settings.COMPRESS_YUGLIFY_BINARY),
+ ("args", settings.COMPRESS_YUGLIFY_JS_ARGUMENTS),
+ )
--- /dev/null
+from compressor.conf import settings
+from compressor.filters import CompilerFilter
+
+
+class YUICompressorFilter(CompilerFilter):
+ command = "{binary} {args}"
+
+ def __init__(self, *args, **kwargs):
+ super(YUICompressorFilter, self).__init__(*args, **kwargs)
+ self.command += ' --type=%s' % self.type
+ if self.verbose:
+ self.command += ' --verbose'
+
+
+class YUICSSFilter(YUICompressorFilter):
+ type = 'css'
+ options = (
+ ("binary", settings.COMPRESS_YUI_BINARY),
+ ("args", settings.COMPRESS_YUI_CSS_ARGUMENTS),
+ )
+
+
+class YUIJSFilter(YUICompressorFilter):
+ type = 'js'
+ options = (
+ ("binary", settings.COMPRESS_YUI_BINARY),
+ ("args", settings.COMPRESS_YUI_JS_ARGUMENTS),
+ )
--- /dev/null
+from compressor.utils import staticfiles
+from compressor.storage import CompressorFileStorage
+
+
+class CompressorFinder(staticfiles.finders.BaseStorageFinder):
+ """
+ A staticfiles finder that looks in COMPRESS_ROOT
+ for compressed files, to be used during development
+ with staticfiles development file server or during
+ deployment.
+ """
+ storage = CompressorFileStorage
+
+ def list(self, ignore_patterns):
+ return []
--- /dev/null
+from compressor.conf import settings
+from compressor.base import Compressor, SOURCE_HUNK, SOURCE_FILE
+
+
+class JsCompressor(Compressor):
+
+ def __init__(self, content=None, output_prefix="js", context=None):
+ super(JsCompressor, self).__init__(content, output_prefix, context)
+ self.filters = list(settings.COMPRESS_JS_FILTERS)
+ self.type = output_prefix
+
+ def split_contents(self):
+ if self.split_content:
+ return self.split_content
+ for elem in self.parser.js_elems():
+ attribs = self.parser.elem_attribs(elem)
+ if 'src' in attribs:
+ basename = self.get_basename(attribs['src'])
+ filename = self.get_filename(basename)
+ content = (SOURCE_FILE, filename, basename, elem)
+ self.split_content.append(content)
+ else:
+ content = self.parser.elem_content(elem)
+ self.split_content.append((SOURCE_HUNK, content, None, elem))
+ return self.split_content
--- /dev/null
+# flake8: noqa
+import os
+import sys
+
+from fnmatch import fnmatch
+from optparse import make_option
+
+from django.core.management.base import NoArgsCommand, CommandError
+import django.template
+from django.template import Context
+from django.utils import six
+from django.utils.datastructures import SortedDict
+from django.utils.importlib import import_module
+from django.template.loader import get_template # noqa Leave this in to preload template locations
+
+from compressor.cache import get_offline_hexdigest, write_offline_manifest
+from compressor.conf import settings
+from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
+ TemplateDoesNotExist)
+from compressor.templatetags.compress import CompressorNode
+
+if six.PY3:
+ # there is an 'io' module in python 2.6+, but io.StringIO does not
+ # accept regular strings, just unicode objects
+ from io import StringIO
+else:
+ try:
+ from cStringIO import StringIO
+ except ImportError:
+ from StringIO import StringIO
+
+
+class Command(NoArgsCommand):
+ help = "Compress content outside of the request/response cycle"
+ option_list = NoArgsCommand.option_list + (
+ make_option('--extension', '-e', action='append', dest='extensions',
+ help='The file extension(s) to examine (default: ".html", '
+ 'separate multiple extensions with commas, or use -e '
+ 'multiple times)'),
+ make_option('-f', '--force', default=False, action='store_true',
+ help="Force the generation of compressed content even if the "
+ "COMPRESS_ENABLED setting is not True.", dest='force'),
+ make_option('--follow-links', default=False, action='store_true',
+ help="Follow symlinks when traversing the COMPRESS_ROOT "
+ "(which defaults to STATIC_ROOT). Be aware that using this "
+ "can lead to infinite recursion if a link points to a parent "
+ "directory of itself.", dest='follow_links'),
+ make_option('--engine', default="django", action="store",
+ help="Specifies the templating engine. jinja2 or django",
+ dest="engine"),
+ )
+
+ requires_model_validation = False
+
+ def get_loaders(self):
+ from django.template.loader import template_source_loaders
+ if template_source_loaders is None:
+ try:
+ from django.template.loader import (
+ find_template as finder_func)
+ except ImportError:
+ from django.template.loader import (
+ find_template_source as finder_func) # noqa
+ try:
+ # Force django to calculate template_source_loaders from
+ # TEMPLATE_LOADERS settings, by asking to find a dummy template
+ source, name = finder_func('test')
+ except django.template.TemplateDoesNotExist:
+ pass
+ # Reload template_source_loaders now that it has been calculated ;
+ # it should contain the list of valid, instanciated template loaders
+ # to use.
+ from django.template.loader import template_source_loaders
+ loaders = []
+ # If template loader is CachedTemplateLoader, return the loaders
+ # that it wraps around. So if we have
+ # TEMPLATE_LOADERS = (
+ # ('django.template.loaders.cached.Loader', (
+ # 'django.template.loaders.filesystem.Loader',
+ # 'django.template.loaders.app_directories.Loader',
+ # )),
+ # )
+ # The loaders will return django.template.loaders.filesystem.Loader
+ # and django.template.loaders.app_directories.Loader
+ # The cached Loader and similar ones include a 'loaders' attribute
+ # so we look for that.
+ for loader in template_source_loaders:
+ if hasattr(loader, 'loaders'):
+ loaders.extend(loader.loaders)
+ else:
+ loaders.append(loader)
+ return loaders
+
+ def __get_parser(self, engine):
+ if engine == "jinja2":
+ from compressor.offline.jinja2 import Jinja2Parser
+ env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
+ parser = Jinja2Parser(charset=settings.FILE_CHARSET, env=env)
+ elif engine == "django":
+ from compressor.offline.django import DjangoParser
+ parser = DjangoParser(charset=settings.FILE_CHARSET)
+ else:
+ raise OfflineGenerationError("Invalid templating engine specified.")
+
+ return parser
+
+ def compress(self, log=None, **options):
+ """
+ Searches templates containing 'compress' nodes and compresses them
+ "offline" -- outside of the request/response cycle.
+
+ The result is cached with a cache-key derived from the content of the
+ compress nodes (not the content of the possibly linked files!).
+ """
+ extensions = options.get('extensions')
+ extensions = self.handle_extensions(extensions or ['html'])
+ verbosity = int(options.get("verbosity", 0))
+ if not log:
+ log = StringIO()
+ if not settings.TEMPLATE_LOADERS:
+ raise OfflineGenerationError("No template loaders defined. You "
+ "must set TEMPLATE_LOADERS in your "
+ "settings.")
+ paths = set()
+ for loader in self.get_loaders():
+ try:
+ module = import_module(loader.__module__)
+ get_template_sources = getattr(module,
+ 'get_template_sources', None)
+ if get_template_sources is None:
+ get_template_sources = loader.get_template_sources
+ paths.update(list(get_template_sources('')))
+ except (ImportError, AttributeError):
+ # Yeah, this didn't work out so well, let's move on
+ pass
+ if not paths:
+ raise OfflineGenerationError("No template paths found. None of "
+ "the configured template loaders "
+ "provided template paths. See "
+ "http://django.me/template-loaders "
+ "for more information on template "
+ "loaders.")
+ if verbosity > 1:
+ log.write("Considering paths:\n\t" + "\n\t".join(paths) + "\n")
+ templates = set()
+ for path in paths:
+ for root, dirs, files in os.walk(path,
+ followlinks=options.get('followlinks', False)):
+ templates.update(os.path.join(root, name)
+ for name in files if not name.startswith('.') and
+ any(fnmatch(name, "*%s" % glob) for glob in extensions))
+ if not templates:
+ raise OfflineGenerationError("No templates found. Make sure your "
+ "TEMPLATE_LOADERS and TEMPLATE_DIRS "
+ "settings are correct.")
+ if verbosity > 1:
+ log.write("Found templates:\n\t" + "\n\t".join(templates) + "\n")
+
+ engine = options.get("engine", "django")
+ parser = self.__get_parser(engine)
+
+ compressor_nodes = SortedDict()
+ for template_name in templates:
+ try:
+ template = parser.parse(template_name)
+ except IOError: # unreadable file -> ignore
+ if verbosity > 0:
+ log.write("Unreadable template at: %s\n" % template_name)
+ continue
+ except TemplateSyntaxError as e: # broken template -> ignore
+ if verbosity > 0:
+ log.write("Invalid template %s: %s\n" % (template_name, e))
+ continue
+ except TemplateDoesNotExist: # non existent template -> ignore
+ if verbosity > 0:
+ log.write("Non-existent template at: %s\n" % template_name)
+ continue
+ except UnicodeDecodeError:
+ if verbosity > 0:
+ log.write("UnicodeDecodeError while trying to read "
+ "template %s\n" % template_name)
+ try:
+ nodes = list(parser.walk_nodes(template))
+ except (TemplateDoesNotExist, TemplateSyntaxError) as e:
+ # Could be an error in some base template
+ if verbosity > 0:
+ log.write("Error parsing template %s: %s\n" % (template_name, e))
+ continue
+ if nodes:
+ template.template_name = template_name
+ compressor_nodes.setdefault(template, []).extend(nodes)
+
+ if not compressor_nodes:
+ raise OfflineGenerationError(
+ "No 'compress' template tags found in templates."
+ "Try running compress command with --follow-links and/or"
+ "--extension=EXTENSIONS")
+
+ if verbosity > 0:
+ log.write("Found 'compress' tags in:\n\t" +
+ "\n\t".join((t.template_name
+ for t in compressor_nodes.keys())) + "\n")
+
+ log.write("Compressing... ")
+ count = 0
+ results = []
+ offline_manifest = SortedDict()
+ init_context = parser.get_init_context(settings.COMPRESS_OFFLINE_CONTEXT)
+
+ for template, nodes in compressor_nodes.items():
+ context = Context(init_context)
+ template._log = log
+ template._log_verbosity = verbosity
+
+ if not parser.process_template(template, context):
+ continue
+
+ for node in nodes:
+ context.push()
+ parser.process_node(template, context, node)
+ rendered = parser.render_nodelist(template, context, node)
+ key = get_offline_hexdigest(rendered)
+
+ if key in offline_manifest:
+ continue
+
+ try:
+ result = parser.render_node(template, context, node)
+ except Exception as e:
+ raise CommandError("An error occured during rendering %s: "
+ "%s" % (template.template_name, e))
+ offline_manifest[key] = result
+ context.pop()
+ results.append(result)
+ count += 1
+
+ write_offline_manifest(offline_manifest)
+
+ log.write("done\nCompressed %d block(s) from %d template(s).\n" %
+ (count, len(compressor_nodes)))
+ return count, results
+
+ def handle_extensions(self, extensions=('html',)):
+ """
+ organizes multiple extensions that are separated with commas or
+ passed by using --extension/-e multiple times.
+
+ for example: running 'django-admin compress -e js,txt -e xhtml -a'
+ would result in a extension list: ['.js', '.txt', '.xhtml']
+
+ >>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
+ ['.html', '.js']
+ >>> handle_extensions(['.html, txt,.tpl'])
+ ['.html', '.tpl', '.txt']
+ """
+ ext_list = []
+ for ext in extensions:
+ ext_list.extend(ext.replace(' ', '').split(','))
+ for i, ext in enumerate(ext_list):
+ if not ext.startswith('.'):
+ ext_list[i] = '.%s' % ext_list[i]
+ return set(ext_list)
+
+ def handle_noargs(self, **options):
+ if not settings.COMPRESS_ENABLED and not options.get("force"):
+ raise CommandError(
+ "Compressor is disabled. Set the COMPRESS_ENABLED "
+ "setting or use --force to override.")
+ if not settings.COMPRESS_OFFLINE:
+ if not options.get("force"):
+ raise CommandError(
+ "Offline compression is disabled. Set "
+ "COMPRESS_OFFLINE or use the --force to override.")
+ self.compress(sys.stdout, **options)
--- /dev/null
+import fnmatch
+import os
+from optparse import make_option
+
+from django.core.management.base import NoArgsCommand, CommandError
+
+from compressor.conf import settings
+from compressor.cache import cache, get_mtime, get_mtime_cachekey
+
+
+class Command(NoArgsCommand):
+ help = "Add or remove all mtime values from the cache"
+ option_list = NoArgsCommand.option_list + (
+ make_option('-i', '--ignore', action='append', default=[],
+ dest='ignore_patterns', metavar='PATTERN',
+ help="Ignore files or directories matching this glob-style "
+ "pattern. Use multiple times to ignore more."),
+ make_option('--no-default-ignore', action='store_false',
+ dest='use_default_ignore_patterns', default=True,
+ help="Don't ignore the common private glob-style patterns 'CVS', "
+ "'.*' and '*~'."),
+ make_option('--follow-links', dest='follow_links', action='store_true',
+ help="Follow symlinks when traversing the COMPRESS_ROOT "
+ "(which defaults to STATIC_ROOT). Be aware that using this "
+ "can lead to infinite recursion if a link points to a parent "
+ "directory of itself."),
+ make_option('-c', '--clean', dest='clean', action='store_true',
+ help="Remove all items"),
+ make_option('-a', '--add', dest='add', action='store_true',
+ help="Add all items"),
+ )
+
+ def is_ignored(self, path):
+ """
+ Return True or False depending on whether the ``path`` should be
+ ignored (if it matches any pattern in ``ignore_patterns``).
+ """
+ for pattern in self.ignore_patterns:
+ if fnmatch.fnmatchcase(path, pattern):
+ return True
+ return False
+
+ def handle_noargs(self, **options):
+ ignore_patterns = options['ignore_patterns']
+ if options['use_default_ignore_patterns']:
+ ignore_patterns += ['CVS', '.*', '*~']
+ options['ignore_patterns'] = ignore_patterns
+ self.ignore_patterns = ignore_patterns
+
+ if (options['add'] and options['clean']) or (not options['add'] and not options['clean']):
+ raise CommandError('Please specify either "--add" or "--clean"')
+
+ if not settings.COMPRESS_MTIME_DELAY:
+ raise CommandError('mtime caching is currently disabled. Please '
+ 'set the COMPRESS_MTIME_DELAY setting to a number of seconds.')
+
+ files_to_add = set()
+ keys_to_delete = set()
+
+ for root, dirs, files in os.walk(settings.COMPRESS_ROOT, followlinks=options['follow_links']):
+ for dir_ in dirs:
+ if self.is_ignored(dir_):
+ dirs.remove(dir_)
+ for filename in files:
+ common = "".join(root.split(settings.COMPRESS_ROOT))
+ if common.startswith(os.sep):
+ common = common[len(os.sep):]
+ if self.is_ignored(os.path.join(common, filename)):
+ continue
+ filename = os.path.join(root, filename)
+ keys_to_delete.add(get_mtime_cachekey(filename))
+ if options['add']:
+ files_to_add.add(filename)
+
+ if keys_to_delete:
+ cache.delete_many(list(keys_to_delete))
+ print("Deleted mtimes of %d files from the cache." % len(keys_to_delete))
+
+ if files_to_add:
+ for filename in files_to_add:
+ get_mtime(filename)
+ print("Added mtimes of %d files to cache." % len(files_to_add))
--- /dev/null
+from compressor.conf import CompressorConf # noqa
--- /dev/null
+from __future__ import absolute_import
+import io
+from copy import copy
+
+from django import template
+from django.conf import settings
+from django.template import Template
+from django.template import Context
+from django.template.base import Node, VariableNode, TextNode, NodeList
+from django.template.defaulttags import IfNode
+from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext
+
+
+from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
+from compressor.templatetags.compress import CompressorNode
+
+
+def handle_extendsnode(extendsnode, block_context=None):
+ """Create a copy of Node tree of a derived template replacing
+ all blocks tags with the nodes of appropriate blocks.
+ Also handles {{ block.super }} tags.
+ """
+ if block_context is None:
+ block_context = BlockContext()
+ blocks = dict((n.name, n) for n in
+ extendsnode.nodelist.get_nodes_by_type(BlockNode))
+ block_context.add_blocks(blocks)
+
+ context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
+ compiled_parent = extendsnode.get_parent(context)
+ parent_nodelist = compiled_parent.nodelist
+ # If the parent template has an ExtendsNode it is not the root.
+ for node in parent_nodelist:
+ # The ExtendsNode has to be the first non-text node.
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ return handle_extendsnode(node, block_context)
+ break
+ # Add blocks of the root template to block context.
+ blocks = dict((n.name, n) for n in
+ parent_nodelist.get_nodes_by_type(BlockNode))
+ block_context.add_blocks(blocks)
+
+ block_stack = []
+ new_nodelist = remove_block_nodes(parent_nodelist, block_stack, block_context)
+ return new_nodelist
+
+
+def remove_block_nodes(nodelist, block_stack, block_context):
+ new_nodelist = NodeList()
+ for node in nodelist:
+ if isinstance(node, VariableNode):
+ var_name = node.filter_expression.token.strip()
+ if var_name == 'block.super':
+ if not block_stack:
+ continue
+ node = block_context.get_block(block_stack[-1].name)
+ if isinstance(node, BlockNode):
+ expanded_block = expand_blocknode(node, block_stack, block_context)
+ new_nodelist.extend(expanded_block)
+ else:
+ # IfNode has nodelist as a @property so we can not modify it
+ if isinstance(node, IfNode):
+ node = copy(node)
+ for i, (condition, sub_nodelist) in enumerate(node.conditions_nodelists):
+ sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context)
+ node.conditions_nodelists[i] = (condition, sub_nodelist)
+ else:
+ for attr in node.child_nodelists:
+ sub_nodelist = getattr(node, attr, None)
+ if sub_nodelist:
+ sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context)
+ node = copy(node)
+ setattr(node, attr, sub_nodelist)
+ new_nodelist.append(node)
+ return new_nodelist
+
+
+def expand_blocknode(node, block_stack, block_context):
+ popped_block = block = block_context.pop(node.name)
+ if block is None:
+ block = node
+ block_stack.append(block)
+ expanded_nodelist = remove_block_nodes(block.nodelist, block_stack, block_context)
+ block_stack.pop()
+ if popped_block is not None:
+ block_context.push(node.name, popped_block)
+ return expanded_nodelist
+
+
+class DjangoParser(object):
+ def __init__(self, charset):
+ self.charset = charset
+
+ def parse(self, template_name):
+ with io.open(template_name, mode='rb') as file:
+ try:
+ return Template(file.read().decode(self.charset))
+ except template.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except template.TemplateDoesNotExist as e:
+ raise TemplateDoesNotExist(str(e))
+
+ def process_template(self, template, context):
+ return True
+
+ def get_init_context(self, offline_context):
+ return offline_context
+
+ def process_node(self, template, context, node):
+ pass
+
+ def render_nodelist(self, template, context, node):
+ return node.nodelist.render(context)
+
+ def render_node(self, template, context, node):
+ return node.render(context, forced=True)
+
+ def get_nodelist(self, node):
+ if isinstance(node, ExtendsNode):
+ try:
+ return handle_extendsnode(node)
+ except template.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except template.TemplateDoesNotExist as e:
+ raise TemplateDoesNotExist(str(e))
+
+ # Check if node is an ```if``` switch with true and false branches
+ nodelist = []
+ if isinstance(node, Node):
+ for attr in node.child_nodelists:
+ nodelist += getattr(node, attr, [])
+ else:
+ nodelist = getattr(node, 'nodelist', [])
+ return nodelist
+
+ def walk_nodes(self, node):
+ for node in self.get_nodelist(node):
+ if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True):
+ yield node
+ else:
+ for node in self.walk_nodes(node):
+ yield node
--- /dev/null
+from __future__ import absolute_import
+import io
+
+import jinja2
+import jinja2.ext
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.nodes import CallBlock, Call, ExtensionAttribute
+
+from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
+
+
+def flatten_context(context):
+ if hasattr(context, 'dicts'):
+ context_dict = {}
+
+ for d in context.dicts:
+ context_dict.update(d)
+
+ return context_dict
+
+ return context
+
+
+class SpacelessExtension(Extension):
+ """
+ Functional "spaceless" extension equivalent to Django's.
+
+ See: https://github.com/django/django/blob/master/django/template/defaulttags.py
+ """
+
+ tags = set(['spaceless'])
+
+ def parse(self, parser):
+ lineno = next(parser.stream).lineno
+ body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
+
+ return nodes.CallBlock(self.call_method('_spaceless', []),
+ [], [], body).set_lineno(lineno)
+
+ def _spaceless(self, caller):
+ from django.utils.html import strip_spaces_between_tags
+
+ return strip_spaces_between_tags(caller().strip())
+
+
+def url_for(mod, filename):
+ """
+ Incomplete emulation of Flask's url_for.
+ """
+ from django.contrib.staticfiles.templatetags import staticfiles
+
+ if mod == "static":
+ return staticfiles.static(filename)
+
+ return ""
+
+
+class Jinja2Parser(object):
+ COMPRESSOR_ID = 'compressor.contrib.jinja2ext.CompressorExtension'
+
+ def __init__(self, charset, env):
+ self.charset = charset
+ self.env = env
+
+ def parse(self, template_name):
+ with io.open(template_name, mode='rb') as file:
+ try:
+ template = self.env.parse(file.read().decode(self.charset))
+ except jinja2.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except jinja2.TemplateNotFound as e:
+ raise TemplateDoesNotExist(str(e))
+
+ return template
+
+ def process_template(self, template, context):
+ return True
+
+ def get_init_context(self, offline_context):
+ # Don't need to add filters and tests to the context, as Jinja2 will
+ # automatically look for them in self.env.filters and self.env.tests.
+ # This is tested by test_complex and test_templatetag.
+
+ # Allow offline context to override the globals.
+ context = self.env.globals.copy()
+ context.update(offline_context)
+
+ return context
+
+ def process_node(self, template, context, node):
+ pass
+
+ def _render_nodes(self, template, context, nodes):
+ compiled_node = self.env.compile(jinja2.nodes.Template(nodes))
+ template = jinja2.Template.from_code(self.env, compiled_node, {})
+ flat_context = flatten_context(context)
+
+ return template.render(flat_context)
+
+ def render_nodelist(self, template, context, node):
+ return self._render_nodes(template, context, node.body)
+
+ def render_node(self, template, context, node):
+ return self._render_nodes(template, context, [node])
+
+ def get_nodelist(self, node):
+ body = getattr(node, "body", getattr(node, "nodes", []))
+
+ if isinstance(node, jinja2.nodes.If):
+ return body + node.else_
+
+ return body
+
+ def walk_nodes(self, node, block_name=None):
+ for node in self.get_nodelist(node):
+ if (isinstance(node, CallBlock) and
+ isinstance(node.call, Call) and
+ isinstance(node.call.node, ExtensionAttribute) and
+ node.call.node.identifier == self.COMPRESSOR_ID):
+ node.call.node.name = '_compress_forced'
+ yield node
+ else:
+ for node in self.walk_nodes(node, block_name=block_name):
+ yield node
--- /dev/null
+from django.utils import six
+from django.utils.functional import LazyObject
+from django.utils.importlib import import_module
+
+# support legacy parser module usage
+from compressor.parser.base import ParserBase # noqa
+from compressor.parser.lxml import LxmlParser
+from compressor.parser.default_htmlparser import DefaultHtmlParser as HtmlParser
+from compressor.parser.beautifulsoup import BeautifulSoupParser # noqa
+from compressor.parser.html5lib import Html5LibParser # noqa
+
+
+class AutoSelectParser(LazyObject):
+ options = (
+ # TODO: make lxml.html parser first again
+ (six.moves.html_parser.__name__, HtmlParser), # fast and part of the Python stdlib
+ ('lxml.html', LxmlParser), # lxml, extremely fast
+ )
+
+ def __init__(self, content):
+ self._wrapped = None
+ self._setup(content)
+
+ def __getattr__(self, name):
+ return getattr(self._wrapped, name)
+
+ def _setup(self, content):
+ for dependency, parser in self.options:
+ try:
+ import_module(dependency)
+ self._wrapped = parser(content)
+ break
+ except ImportError:
+ continue
--- /dev/null
+class ParserBase(object):
+ """
+ Base parser to be subclassed when creating an own parser.
+ """
+ def __init__(self, content):
+ self.content = content
+
+ def css_elems(self):
+ """
+ Return an iterable containing the css elements to handle
+ """
+ raise NotImplementedError
+
+ def js_elems(self):
+ """
+ Return an iterable containing the js elements to handle
+ """
+ raise NotImplementedError
+
+ def elem_attribs(self, elem):
+ """
+ Return the dictionary like attribute store of the given element
+ """
+ raise NotImplementedError
+
+ def elem_content(self, elem):
+ """
+ Return the content of the given element
+ """
+ raise NotImplementedError
+
+ def elem_name(self, elem):
+ """
+ Return the name of the given element
+ """
+ raise NotImplementedError
+
+ def elem_str(self, elem):
+ """
+ Return the string representation of the given elem
+ """
+ raise NotImplementedError
--- /dev/null
+from __future__ import absolute_import
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
+from django.utils.encoding import smart_text
+
+from compressor.exceptions import ParserError
+from compressor.parser import ParserBase
+from compressor.utils.decorators import cached_property
+
+
+class BeautifulSoupParser(ParserBase):
+
+ @cached_property
+ def soup(self):
+ try:
+ if six.PY3:
+ from bs4 import BeautifulSoup
+ else:
+ from BeautifulSoup import BeautifulSoup
+ return BeautifulSoup(self.content)
+ except ImportError as err:
+ raise ImproperlyConfigured("Error while importing BeautifulSoup: %s" % err)
+ except Exception as err:
+ raise ParserError("Error while initializing Parser: %s" % err)
+
+ def css_elems(self):
+ if six.PY3:
+ return self.soup.find_all({'link': True, 'style': True})
+ else:
+ return self.soup.findAll({'link': True, 'style': True})
+
+ def js_elems(self):
+ if six.PY3:
+ return self.soup.find_all('script')
+ else:
+ return self.soup.findAll('script')
+
+ def elem_attribs(self, elem):
+ return dict(elem.attrs)
+
+ def elem_content(self, elem):
+ return elem.string
+
+ def elem_name(self, elem):
+ return elem.name
+
+ def elem_str(self, elem):
+ return smart_text(elem)
--- /dev/null
+from django.utils import six
+from django.utils.encoding import smart_text
+
+from compressor.exceptions import ParserError
+from compressor.parser import ParserBase
+
+
+class DefaultHtmlParser(ParserBase, six.moves.html_parser.HTMLParser):
+ def __init__(self, content):
+ six.moves.html_parser.HTMLParser.__init__(self)
+ self.content = content
+ self._css_elems = []
+ self._js_elems = []
+ self._current_tag = None
+ try:
+ self.feed(self.content)
+ self.close()
+ except Exception as err:
+ lineno = err.lineno
+ line = self.content.splitlines()[lineno]
+ raise ParserError("Error while initializing HtmlParser: %s (line: %s)" % (err, repr(line)))
+
+ def handle_starttag(self, tag, attrs):
+ tag = tag.lower()
+ if tag in ('style', 'script'):
+ if tag == 'style':
+ tags = self._css_elems
+ elif tag == 'script':
+ tags = self._js_elems
+ tags.append({
+ 'tag': tag,
+ 'attrs': attrs,
+ 'attrs_dict': dict(attrs),
+ 'text': ''
+ })
+ self._current_tag = tag
+ elif tag == 'link':
+ self._css_elems.append({
+ 'tag': tag,
+ 'attrs': attrs,
+ 'attrs_dict': dict(attrs),
+ 'text': None
+ })
+
+ def handle_endtag(self, tag):
+ if self._current_tag and self._current_tag == tag.lower():
+ self._current_tag = None
+
+ def handle_data(self, data):
+ if self._current_tag == 'style':
+ self._css_elems[-1]['text'] = data
+ elif self._current_tag == 'script':
+ self._js_elems[-1]['text'] = data
+
+ def css_elems(self):
+ return self._css_elems
+
+ def js_elems(self):
+ return self._js_elems
+
+ def elem_name(self, elem):
+ return elem['tag']
+
+ def elem_attribs(self, elem):
+ return elem['attrs_dict']
+
+ def elem_content(self, elem):
+ return smart_text(elem['text'])
+
+ def elem_str(self, elem):
+ tag = {}
+ tag.update(elem)
+ tag['attrs'] = ''
+ if len(elem['attrs']):
+ tag['attrs'] = ' %s' % ' '.join(['%s="%s"' % (name, value) for name, value in elem['attrs']])
+ if elem['tag'] == 'link':
+ return '<%(tag)s%(attrs)s />' % tag
+ else:
+ return '<%(tag)s%(attrs)s>%(text)s</%(tag)s>' % tag
--- /dev/null
+from __future__ import absolute_import
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.encoding import smart_text
+
+from compressor.exceptions import ParserError
+from compressor.parser import ParserBase
+from compressor.utils.decorators import cached_property
+
+
+class Html5LibParser(ParserBase):
+
+ def __init__(self, content):
+ super(Html5LibParser, self).__init__(content)
+ import html5lib
+ self.html5lib = html5lib
+
+ def _serialize(self, elem):
+ return self.html5lib.serialize(
+ elem, tree="etree", quote_attr_values=True,
+ omit_optional_tags=False, use_trailing_solidus=True,
+ )
+
+ def _find(self, *names):
+ for elem in self.html:
+ if elem.tag in names:
+ yield elem
+
+ @cached_property
+ def html(self):
+ try:
+ return self.html5lib.parseFragment(self.content, treebuilder="etree")
+ except ImportError as err:
+ raise ImproperlyConfigured("Error while importing html5lib: %s" % err)
+ except Exception as err:
+ raise ParserError("Error while initializing Parser: %s" % err)
+
+ def css_elems(self):
+ return self._find('{http://www.w3.org/1999/xhtml}link',
+ '{http://www.w3.org/1999/xhtml}style')
+
+ def js_elems(self):
+ return self._find('{http://www.w3.org/1999/xhtml}script')
+
+ def elem_attribs(self, elem):
+ return elem.attrib
+
+ def elem_content(self, elem):
+ return smart_text(elem.text)
+
+ def elem_name(self, elem):
+ if '}' in elem.tag:
+ return elem.tag.split('}')[1]
+ return elem.tag
+
+ def elem_str(self, elem):
+ # This method serializes HTML in a way that does not pass all tests.
+ # However, this method is only called in tests anyway, so it doesn't
+ # really matter.
+ return smart_text(self._serialize(elem))
--- /dev/null
+from __future__ import absolute_import, unicode_literals
+
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
+from django.utils.encoding import smart_text
+
+from compressor.exceptions import ParserError
+from compressor.parser import ParserBase
+from compressor.utils.decorators import cached_property
+
+
+class LxmlParser(ParserBase):
+ """
+ LxmlParser will use `lxml.html` parser to parse rendered contents of
+ {% compress %} tag. Under python 2 it will also try to use beautiful
+ soup parser in case of any problems with encoding.
+ """
+ def __init__(self, content):
+ try:
+ from lxml.html import fromstring
+ from lxml.etree import tostring
+ except ImportError as err:
+ raise ImproperlyConfigured("Error while importing lxml: %s" % err)
+ except Exception as err:
+ raise ParserError("Error while initializing parser: %s" % err)
+
+ if not six.PY3:
+ # soupparser uses Beautiful Soup 3 which does not run on python 3.x
+ try:
+ from lxml.html import soupparser
+ except ImportError as err:
+ soupparser = None
+ except Exception as err:
+ raise ParserError("Error while initializing parser: %s" % err)
+ else:
+ soupparser = None
+
+ self.soupparser = soupparser
+ self.fromstring = fromstring
+ self.tostring = tostring
+ super(LxmlParser, self).__init__(content)
+
+ @cached_property
+ def tree(self):
+ """
+ Document tree.
+ """
+ content = '<root>%s</root>' % self.content
+ tree = self.fromstring(content)
+ try:
+ self.tostring(tree, encoding=six.text_type)
+ except UnicodeDecodeError:
+ if self.soupparser: # use soup parser on python 2
+ tree = self.soupparser.fromstring(content)
+ else: # raise an error on python 3
+ raise
+ return tree
+
+ def css_elems(self):
+ return self.tree.xpath('//link[re:test(@rel, "^stylesheet$", "i")]|style',
+ namespaces={"re": "http://exslt.org/regular-expressions"})
+
+ def js_elems(self):
+ return self.tree.findall('script')
+
+ def elem_attribs(self, elem):
+ return elem.attrib
+
+ def elem_content(self, elem):
+ return smart_text(elem.text)
+
+ def elem_name(self, elem):
+ return elem.tag
+
+ def elem_str(self, elem):
+ elem_as_string = smart_text(
+ self.tostring(elem, method='html', encoding=six.text_type))
+ if elem.tag == 'link':
+ # This makes testcases happy
+ return elem_as_string.replace('>', ' />')
+ return elem_as_string
--- /dev/null
+import django.dispatch
+
+
+post_compress = django.dispatch.Signal(providing_args=['type', 'mode', 'context'])
--- /dev/null
+from __future__ import unicode_literals
+import errno
+import gzip
+import os
+from datetime import datetime
+import time
+
+from django.core.files.storage import FileSystemStorage, get_storage_class
+from django.utils.functional import LazyObject, SimpleLazyObject
+
+from compressor.conf import settings
+
+
+class CompressorFileStorage(FileSystemStorage):
+ """
+ Standard file system storage for files handled by django-compressor.
+
+ The defaults for ``location`` and ``base_url`` are ``COMPRESS_ROOT`` and
+ ``COMPRESS_URL``.
+
+ """
+ def __init__(self, location=None, base_url=None, *args, **kwargs):
+ if location is None:
+ location = settings.COMPRESS_ROOT
+ if base_url is None:
+ base_url = settings.COMPRESS_URL
+ super(CompressorFileStorage, self).__init__(location, base_url,
+ *args, **kwargs)
+
+ def accessed_time(self, name):
+ return datetime.fromtimestamp(os.path.getatime(self.path(name)))
+
+ def created_time(self, name):
+ return datetime.fromtimestamp(os.path.getctime(self.path(name)))
+
+ def modified_time(self, name):
+ return datetime.fromtimestamp(os.path.getmtime(self.path(name)))
+
+ def get_available_name(self, name):
+ """
+ Deletes the given file if it exists.
+ """
+ if self.exists(name):
+ self.delete(name)
+ return name
+
+ def delete(self, name):
+ """
+ Handle deletion race condition present in Django prior to 1.4
+ https://code.djangoproject.com/ticket/16108
+ """
+ try:
+ super(CompressorFileStorage, self).delete(name)
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+
+
+compressor_file_storage = SimpleLazyObject(
+ lambda: get_storage_class('compressor.storage.CompressorFileStorage')())
+
+
+class GzipCompressorFileStorage(CompressorFileStorage):
+ """
+ The standard compressor file system storage that gzips storage files
+ additionally to the usual files.
+ """
+ def save(self, filename, content):
+ filename = super(GzipCompressorFileStorage, self).save(filename, content)
+ orig_path = self.path(filename)
+ compressed_path = '%s.gz' % orig_path
+
+ f_in = open(orig_path, 'rb')
+ f_out = open(compressed_path, 'wb')
+ try:
+ f_out = gzip.GzipFile(fileobj=f_out)
+ f_out.write(f_in.read())
+ finally:
+ f_out.close()
+ f_in.close()
+ # Ensure the file timestamps match.
+ # os.stat() returns nanosecond resolution on Linux, but os.utime()
+ # only sets microsecond resolution. Set times on both files to
+ # ensure they are equal.
+ stamp = time.time()
+ os.utime(orig_path, (stamp, stamp))
+ os.utime(compressed_path, (stamp, stamp))
+
+ return filename
+
+
+class DefaultStorage(LazyObject):
+ def _setup(self):
+ self._wrapped = get_storage_class(settings.COMPRESS_STORAGE)()
+
+default_storage = DefaultStorage()
--- /dev/null
+<link rel="stylesheet" href="{{ compressed.url }}" type="text/css"{% if compressed.media %} media="{{ compressed.media }}"{% endif %} />
\ No newline at end of file
--- /dev/null
+<style type="text/css"{% if compressed.media %} media="{{ compressed.media }}"{% endif %}>{{ compressed.content|safe }}</style>
\ No newline at end of file
--- /dev/null
+<script type="text/javascript" src="{{ compressed.url }}"></script>
\ No newline at end of file
--- /dev/null
+<script type="text/javascript">{{ compressed.content|safe }}</script>
\ No newline at end of file
--- /dev/null
+from django import template
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
+
+from compressor.cache import (cache_get, cache_set, get_offline_hexdigest,
+ get_offline_manifest, get_templatetag_cachekey)
+from compressor.conf import settings
+from compressor.exceptions import OfflineGenerationError
+from compressor.utils import get_class
+
+register = template.Library()
+
+OUTPUT_FILE = 'file'
+OUTPUT_INLINE = 'inline'
+OUTPUT_MODES = (OUTPUT_FILE, OUTPUT_INLINE)
+
+
+class CompressorMixin(object):
+
+ def get_original_content(self, context):
+ raise NotImplementedError
+
+ @property
+ def compressors(self):
+ return {
+ 'js': settings.COMPRESS_JS_COMPRESSOR,
+ 'css': settings.COMPRESS_CSS_COMPRESSOR,
+ }
+
+ def compressor_cls(self, kind, *args, **kwargs):
+ if kind not in self.compressors.keys():
+ raise template.TemplateSyntaxError(
+ "The compress tag's argument must be 'js' or 'css'.")
+ return get_class(self.compressors.get(kind),
+ exception=ImproperlyConfigured)(*args, **kwargs)
+
+ def get_compressor(self, context, kind):
+ return self.compressor_cls(kind,
+ content=self.get_original_content(context), context=context)
+
+ def debug_mode(self, context):
+ if settings.COMPRESS_DEBUG_TOGGLE:
+ # Only check for the debug parameter
+ # if a RequestContext was used
+ request = context.get('request', None)
+ if request is not None:
+ return settings.COMPRESS_DEBUG_TOGGLE in request.GET
+
+ def is_offline_compression_enabled(self, forced):
+ """
+ Check if offline compression is enabled or forced
+
+ Defaults to just checking the settings and forced argument,
+ but can be overridden to completely disable compression for
+ a subclass, for instance.
+ """
+ return (settings.COMPRESS_ENABLED and
+ settings.COMPRESS_OFFLINE) or forced
+
+ def render_offline(self, context, forced):
+ """
+ If enabled and in offline mode, and not forced check the offline cache
+ and return the result if given
+ """
+ if self.is_offline_compression_enabled(forced) and not forced:
+ key = get_offline_hexdigest(self.get_original_content(context))
+ offline_manifest = get_offline_manifest()
+ if key in offline_manifest:
+ return offline_manifest[key]
+ else:
+ raise OfflineGenerationError('You have offline compression '
+ 'enabled but key "%s" is missing from offline manifest. '
+ 'You may need to run "python manage.py compress".' % key)
+
+ def render_cached(self, compressor, kind, mode, forced=False):
+ """
+ If enabled checks the cache for the given compressor's cache key
+ and return a tuple of cache key and output
+ """
+ if settings.COMPRESS_ENABLED and not forced:
+ cache_key = get_templatetag_cachekey(compressor, mode, kind)
+ cache_content = cache_get(cache_key)
+ return cache_key, cache_content
+ return None, None
+
+ def render_compressed(self, context, kind, mode, forced=False):
+
+ # See if it has been rendered offline
+ cached_offline = self.render_offline(context, forced=forced)
+ if cached_offline:
+ return cached_offline
+
+ # Take a shortcut if we really don't have anything to do
+ if ((not settings.COMPRESS_ENABLED and
+ not settings.COMPRESS_PRECOMPILERS) and not forced):
+ return self.get_original_content(context)
+
+ context['compressed'] = {'name': getattr(self, 'name', None)}
+ compressor = self.get_compressor(context, kind)
+
+ # Prepare the actual compressor and check cache
+ cache_key, cache_content = self.render_cached(compressor, kind, mode, forced=forced)
+ if cache_content is not None:
+ return cache_content
+
+ # call compressor output method and handle exceptions
+ try:
+ rendered_output = self.render_output(compressor, mode, forced=forced)
+ if cache_key:
+ cache_set(cache_key, rendered_output)
+ assert isinstance(rendered_output, six.string_types)
+ return rendered_output
+ except Exception:
+ if settings.DEBUG or forced:
+ raise
+
+ # Or don't do anything in production
+ return self.get_original_content(context)
+
+ def render_output(self, compressor, mode, forced=False):
+ return compressor.output(mode, forced=forced)
+
+
+class CompressorNode(CompressorMixin, template.Node):
+
+ def __init__(self, nodelist, kind=None, mode=OUTPUT_FILE, name=None):
+ self.nodelist = nodelist
+ self.kind = kind
+ self.mode = mode
+ self.name = name
+
+ def get_original_content(self, context):
+ return self.nodelist.render(context)
+
+ def debug_mode(self, context):
+ if settings.COMPRESS_DEBUG_TOGGLE:
+ # Only check for the debug parameter
+ # if a RequestContext was used
+ request = context.get('request', None)
+ if request is not None:
+ return settings.COMPRESS_DEBUG_TOGGLE in request.GET
+
+ def render(self, context, forced=False):
+
+ # Check if in debug mode
+ if self.debug_mode(context):
+ return self.get_original_content(context)
+
+ return self.render_compressed(context, self.kind, self.mode, forced=forced)
+
+
+@register.tag
+def compress(parser, token):
+ """
+ Compresses linked and inline javascript or CSS into a single cached file.
+
+ Syntax::
+
+ {% compress <js/css> %}
+ <html of inline or linked JS/CSS>
+ {% endcompress %}
+
+ Examples::
+
+ {% compress css %}
+ <link rel="stylesheet" href="/static/css/one.css" type="text/css" charset="utf-8">
+ <style type="text/css">p { border:5px solid green;}</style>
+ <link rel="stylesheet" href="/static/css/two.css" type="text/css" charset="utf-8">
+ {% endcompress %}
+
+ Which would be rendered something like::
+
+ <link rel="stylesheet" href="/static/CACHE/css/f7c661b7a124.css" type="text/css" media="all" charset="utf-8">
+
+ or::
+
+ {% compress js %}
+ <script src="/static/js/one.js" type="text/javascript" charset="utf-8"></script>
+ <script type="text/javascript" charset="utf-8">obj.value = "value";</script>
+ {% endcompress %}
+
+ Which would be rendered something like::
+
+ <script type="text/javascript" src="/static/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>
+
+ Linked files must be on your COMPRESS_URL (which defaults to STATIC_URL).
+ If DEBUG is true off-site files will throw exceptions. If DEBUG is false
+ they will be silently stripped.
+ """
+
+ nodelist = parser.parse(('endcompress',))
+ parser.delete_first_token()
+
+ args = token.split_contents()
+
+ if not len(args) in (2, 3, 4):
+ raise template.TemplateSyntaxError(
+ "%r tag requires either one, two or three arguments." % args[0])
+
+ kind = args[1]
+
+ if len(args) >= 3:
+ mode = args[2]
+ if mode not in OUTPUT_MODES:
+ raise template.TemplateSyntaxError(
+ "%r's second argument must be '%s' or '%s'." %
+ (args[0], OUTPUT_FILE, OUTPUT_INLINE))
+ else:
+ mode = OUTPUT_FILE
+ if len(args) == 4:
+ name = args[3]
+ else:
+ name = None
+ return CompressorNode(nodelist, kind, mode, name)
--- /dev/null
+import os
+import django
+
+TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')
+
+COMPRESS_CACHE_BACKEND = 'locmem://'
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:',
+ }
+}
+
+INSTALLED_APPS = [
+ 'compressor',
+ 'coffin',
+ 'jingo',
+]
+
+STATIC_URL = '/static/'
+
+
+STATIC_ROOT = os.path.join(TEST_DIR, 'static')
+
+TEMPLATE_DIRS = (
+ # Specifically choose a name that will not be considered
+ # by app_directories loader, to make sure each test uses
+ # a specific template without considering the others.
+ os.path.join(TEST_DIR, 'test_templates'),
+)
+
+if django.VERSION[:2] < (1, 6):
+ TEST_RUNNER = 'discover_runner.DiscoverRunner'
+
+SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!"
+
+PASSWORD_HASHERS = (
+ 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
+)
--- /dev/null
+#!/usr/bin/env python
+from __future__ import with_statement
+import optparse
+import sys
+
+
+def main():
+ p = optparse.OptionParser()
+ p.add_option('-f', '--file', action="store",
+ type="string", dest="filename",
+ help="File to read from, defaults to stdin", default=None)
+ p.add_option('-o', '--output', action="store",
+ type="string", dest="outfile",
+ help="File to write to, defaults to stdout", default=None)
+
+ options, arguments = p.parse_args()
+
+ if options.filename:
+ f = open(options.filename)
+ content = f.read()
+ f.close()
+ else:
+ content = sys.stdin.read()
+
+ content = content.replace('background:', 'color:')
+
+ if options.outfile:
+ with open(options.outfile, 'w') as f:
+ f.write(content)
+ else:
+ print(content)
+
+
+if __name__ == '__main__':
+ main()
--- /dev/null
+.add { background-image: url("../img/add.png"); }
+.add-with-hash { background-image: url("../img/add.png#add"); }
+.python { background-image: url("../img/python.png"); }
+.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }
--- /dev/null
+.byline:before { content: " — "; }
\ No newline at end of file
--- /dev/null
+body { background:#990; }
\ No newline at end of file
--- /dev/null
+body { color:#fff; }
\ No newline at end of file
--- /dev/null
+p { background: url('../../../img/add.png'); }
+p { background: url(../../../img/add.png); }
+p { background: url( ../../../img/add.png ); }
+p { background: url( '../../../img/add.png' ); }
+p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../../../img/add.png'); }
--- /dev/null
+p { background: url( '../../images/test.png' ); }
+.byline:before { content: " — "; }
\ No newline at end of file
--- /dev/null
+p { background: url('/static/images/image.gif') }
\ No newline at end of file
--- /dev/null
+p { background: url('../../img/python.png'); }
+p { background: url(../../img/python.png); }
+p { background: url( ../../img/python.png ); }
+p { background: url( '../../img/python.png' ); }
+p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='../../img/python.png'); }
--- /dev/null
+var test_value = "Überstríng";
--- /dev/null
+var test_value = "—";
--- /dev/null
+# this is a comment.
--- /dev/null
+obj = {};
\ No newline at end of file
--- /dev/null
+from __future__ import with_statement, unicode_literals
+import os
+import re
+
+try:
+ from bs4 import BeautifulSoup
+except ImportError:
+ from BeautifulSoup import BeautifulSoup
+
+from django.utils import six
+from django.core.cache.backends import locmem
+from django.test import SimpleTestCase
+from django.test.utils import override_settings
+
+from compressor.base import SOURCE_HUNK, SOURCE_FILE
+from compressor.conf import settings
+from compressor.css import CssCompressor
+from compressor.js import JsCompressor
+from compressor.exceptions import FilterDoesNotExist
+
+
+def make_soup(markup):
+ # we use html.parser instead of lxml because it doesn't work on python 3.3
+ if six.PY3:
+ return BeautifulSoup(markup, 'html.parser')
+ else:
+ return BeautifulSoup(markup)
+
+
+def css_tag(href, **kwargs):
+ rendered_attrs = ''.join(['%s="%s" ' % (k, v) for k, v in kwargs.items()])
+ template = '<link rel="stylesheet" href="%s" type="text/css" %s/>'
+ return template % (href, rendered_attrs)
+
+
+class TestPrecompiler(object):
+ """A filter whose output is always the string 'OUTPUT' """
+ def __init__(self, content, attrs, filter_type=None, filename=None,
+ charset=None):
+ pass
+
+ def input(self, **kwargs):
+ return 'OUTPUT'
+
+
+test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
+
+
+class CompressorTestCase(SimpleTestCase):
+
+ def setUp(self):
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_PRECOMPILERS = ()
+ settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
+ self.css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
+<style type="text/css">p { border:5px solid green;}</style>
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+ self.css_node = CssCompressor(self.css)
+
+ self.js = """\
+<script src="/static/js/one.js" type="text/javascript"></script>
+<script type="text/javascript">obj.value = "value";</script>"""
+ self.js_node = JsCompressor(self.js)
+
+ def assertEqualCollapsed(self, a, b):
+ """
+ assertEqual with internal newlines collapsed to single, and
+ trailing whitespace removed.
+ """
+ collapse = lambda x: re.sub(r'\n+', '\n', x).rstrip()
+ self.assertEqual(collapse(a), collapse(b))
+
+ def assertEqualSplits(self, a, b):
+ """
+ assertEqual for splits, particularly ignoring the presence of
+ a trailing newline on the content.
+ """
+ mangle = lambda split: [(x[0], x[1], x[2], x[3].rstrip()) for x in split]
+ self.assertEqual(mangle(a), mangle(b))
+
+ def test_css_split(self):
+ out = [
+ (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'),
+ 'css/one.css', '<link rel="stylesheet" href="/static/css/one.css" type="text/css" />',
+ ),
+ (
+ SOURCE_HUNK,
+ 'p { border:5px solid green;}',
+ None,
+ '<style type="text/css">p { border:5px solid green;}</style>',
+ ),
+ (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'two.css'),
+ 'css/two.css',
+ '<link rel="stylesheet" href="/static/css/two.css" type="text/css" />',
+ ),
+ ]
+ split = self.css_node.split_contents()
+ split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
+ self.assertEqualSplits(split, out)
+
+ def test_css_hunks(self):
+ out = ['body { background:#990; }', 'p { border:5px solid green;}', 'body { color:#fff; }']
+ self.assertEqual(out, list(self.css_node.hunks()))
+
+ def test_css_output(self):
+ out = 'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }'
+ hunks = '\n'.join([h for h in self.css_node.hunks()])
+ self.assertEqual(out, hunks)
+
+ def test_css_mtimes(self):
+ is_date = re.compile(r'^\d{10}[\.\d]+$')
+ for date in self.css_node.mtimes:
+ self.assertTrue(is_date.match(str(float(date))),
+ "mtimes is returning something that doesn't look like a date: %s" % date)
+
+ def test_css_return_if_off(self):
+ settings.COMPRESS_ENABLED = False
+ self.assertEqualCollapsed(self.css, self.css_node.output())
+
+ def test_cachekey(self):
+ is_cachekey = re.compile(r'\w{12}')
+ self.assertTrue(is_cachekey.match(self.css_node.cachekey),
+ "cachekey is returning something that doesn't look like r'\w{12}'")
+
+ def test_css_return_if_on(self):
+ output = css_tag('/static/CACHE/css/e41ba2cc6982.css')
+ self.assertEqual(output, self.css_node.output().strip())
+
+ def test_js_split(self):
+ out = [
+ (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'js', 'one.js'),
+ 'js/one.js',
+ '<script src="/static/js/one.js" type="text/javascript"></script>',
+ ),
+ (
+ SOURCE_HUNK,
+ 'obj.value = "value";',
+ None,
+ '<script type="text/javascript">obj.value = "value";</script>',
+ ),
+ ]
+ split = self.js_node.split_contents()
+ split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
+ self.assertEqualSplits(split, out)
+
+ def test_js_hunks(self):
+ out = ['obj = {};', 'obj.value = "value";']
+ self.assertEqual(out, list(self.js_node.hunks()))
+
+ def test_js_output(self):
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ self.assertEqual(out, self.js_node.output())
+
+ def test_js_override_url(self):
+ self.js_node.context.update({'url': 'This is not a url, just a text'})
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ self.assertEqual(out, self.js_node.output())
+
+ def test_css_override_url(self):
+ self.css_node.context.update({'url': 'This is not a url, just a text'})
+ output = css_tag('/static/CACHE/css/e41ba2cc6982.css')
+ self.assertEqual(output, self.css_node.output().strip())
+
+ @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False)
+ def test_js_return_if_off(self):
+ self.assertEqualCollapsed(self.js, self.js_node.output())
+
+ def test_js_return_if_on(self):
+ output = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ self.assertEqual(output, self.js_node.output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='custom')
+ def test_custom_output_dir1(self):
+ output = '<script type="text/javascript" src="/static/custom/js/066cd253eada.js"></script>'
+ self.assertEqual(output, JsCompressor(self.js).output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='')
+ def test_custom_output_dir2(self):
+ output = '<script type="text/javascript" src="/static/js/066cd253eada.js"></script>'
+ self.assertEqual(output, JsCompressor(self.js).output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='/custom/nested/')
+ def test_custom_output_dir3(self):
+ output = '<script type="text/javascript" src="/static/custom/nested/js/066cd253eada.js"></script>'
+ self.assertEqual(output, JsCompressor(self.js).output())
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'compressor.tests.test_base.TestPrecompiler'),
+ ), COMPRESS_ENABLED=True)
+ def test_precompiler_class_used(self):
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor(css)
+ output = make_soup(css_node.output('inline'))
+ self.assertEqual(output.text, 'OUTPUT')
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'compressor.tests.test_base.NonexistentFilter'),
+ ), COMPRESS_ENABLED=True)
+ def test_nonexistent_precompiler_class_error(self):
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor(css)
+ self.assertRaises(FilterDoesNotExist, css_node.output, 'inline')
+
+
+class CssMediaTestCase(SimpleTestCase):
+ def setUp(self):
+ self.css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css" media="screen">
+<style type="text/css" media="print">p { border:5px solid green;}</style>
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" media="all">
+<style type="text/css">h1 { border:5px solid green;}</style>"""
+
+ def test_css_output(self):
+ css_node = CssCompressor(self.css)
+ if six.PY3:
+ links = make_soup(css_node.output()).find_all('link')
+ else:
+ links = make_soup(css_node.output()).findAll('link')
+ media = ['screen', 'print', 'all', None]
+ self.assertEqual(len(links), 4)
+ self.assertEqual(media, [l.get('media', None) for l in links])
+
+ def test_avoid_reordering_css(self):
+ 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']
+ if six.PY3:
+ links = make_soup(css_node.output()).find_all('link')
+ else:
+ links = make_soup(css_node.output()).findAll('link')
+ self.assertEqual(media, [l.get('media', None) for l in links])
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'python %s {infile} {outfile}' % os.path.join(test_dir, 'precompiler.py')),
+ ), COMPRESS_ENABLED=False)
+ def test_passthough_when_compress_disabled(self):
+ css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css" media="screen">
+<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)
+ if six.PY3:
+ output = make_soup(css_node.output()).find_all(['link', 'style'])
+ else:
+ output = make_soup(css_node.output()).findAll(['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'],
+ [l.get('media', None) for l in output])
+
+
+class VerboseTestCase(CompressorTestCase):
+
+ def setUp(self):
+ super(VerboseTestCase, self).setUp()
+ settings.COMPRESS_VERBOSE = True
+
+
+class CacheBackendTestCase(CompressorTestCase):
+
+ def test_correct_backend(self):
+ from compressor.cache import cache
+ self.assertEqual(cache.__class__, locmem.CacheClass)
--- /dev/null
+from __future__ import with_statement, unicode_literals
+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 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
+from compressor.filters.cssmin import CSSMinFilter
+from compressor.filters.css_default import CssAbsoluteFilter
+from compressor.filters.template import TemplateFilter
+from compressor.tests.test_base import test_dir
+
+
+@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;
+ }
+ """)
+ from compressor.filters.csstidy import CSSTidyFilter
+ ret = CSSTidyFilter(content).input()
+ self.assertIsInstance(ret, six.text_type)
+ self.assertEqual(
+ "font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
+
+
+class PrecompilerTestCase(TestCase):
+ def setUp(self):
+ self.filename = os.path.join(test_dir, 'static/css/one.css')
+ with io.open(self.filename, encoding=settings.FILE_CHARSET) as file:
+ self.content = file.read()
+ self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
+
+ def test_precompiler_infile_outfile(self):
+ command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(
+ content=self.content, filename=self.filename,
+ charset=settings.FILE_CHARSET, command=command)
+ self.assertEqual("body { color:#990; }", compiler.input())
+
+ def test_precompiler_infile_stdout(self):
+ command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(
+ content=self.content, filename=None, charset=None, command=command)
+ self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input())
+
+ def test_precompiler_stdin_outfile(self):
+ command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(
+ content=self.content, filename=None, charset=None, command=command)
+ self.assertEqual("body { color:#990; }", compiler.input())
+
+ def test_precompiler_stdin_stdout(self):
+ command = '%s %s' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(
+ content=self.content, filename=None, charset=None, command=command)
+ self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input())
+
+ def test_precompiler_stdin_stdout_filename(self):
+ command = '%s %s' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(
+ content=self.content, filename=self.filename,
+ charset=settings.FILE_CHARSET, command=command)
+ self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input())
+
+ def test_precompiler_output_unicode(self):
+ command = '%s %s' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
+ self.assertEqual(type(compiler.input()), six.text_type)
+
+
+class CssMinTestCase(TestCase):
+ def test_cssmin_filter(self):
+ content = """p {
+
+
+ background: rgb(51,102,153) url('../../images/image.gif');
+
+
+ }
+ """
+ output = "p{background:#369 url('../../images/image.gif')}"
+ self.assertEqual(output, CSSMinFilter(content).output())
+
+
+class CssAbsolutizingTestCase(TestCase):
+ hashing_method = 'mtime'
+ hashing_func = staticmethod(get_hashed_mtime)
+ content = ("p { background: url('../../img/python.png') }"
+ "p { filter: Alpha(src='../../img/python.png') }")
+
+ def setUp(self):
+ self.old_enabled = settings.COMPRESS_ENABLED
+ self.old_url = settings.COMPRESS_URL
+ self.old_hashing_method = settings.COMPRESS_CSS_HASHING_METHOD
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_URL = '/static/'
+ settings.COMPRESS_CSS_HASHING_METHOD = self.hashing_method
+ self.css = """
+ <link rel="stylesheet" href="/static/css/url/url1.css" type="text/css">
+ <link rel="stylesheet" href="/static/css/url/2/url2.css" type="text/css">
+ """
+ self.css_node = CssCompressor(self.css)
+
+ def tearDown(self):
+ settings.COMPRESS_ENABLED = self.old_enabled
+ settings.COMPRESS_URL = self.old_url
+ settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method
+
+ def test_css_absolute_filter(self):
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ params = {
+ 'url': settings.COMPRESS_URL,
+ 'hash': self.hashing_func(imagefilename),
+ }
+ output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
+ "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
+ filter = CssAbsoluteFilter(self.content)
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
+ filter = CssAbsoluteFilter(self.content)
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
+ "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+
+ def test_css_absolute_filter_url_fragment(self):
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ params = {
+ 'url': settings.COMPRESS_URL,
+ 'hash': self.hashing_func(imagefilename),
+ }
+ content = "p { background: url('../../img/python.png#foo') }"
+
+ output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params
+ filter = CssAbsoluteFilter(content)
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ settings.COMPRESS_URL = params['url'] = 'http://media.example.com/'
+ filter = CssAbsoluteFilter(content)
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+
+ def test_css_absolute_filter_only_url_fragment(self):
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ content = "p { background: url('#foo') }"
+ filter = CssAbsoluteFilter(content)
+ self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css'))
+ settings.COMPRESS_URL = 'http://media.example.com/'
+ filter = CssAbsoluteFilter(content)
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css'))
+
+ def test_css_absolute_filter_querystring(self):
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ params = {
+ 'url': settings.COMPRESS_URL,
+ 'hash': self.hashing_func(imagefilename),
+ }
+ content = "p { background: url('../../img/python.png?foo') }"
+
+ output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params
+ filter = CssAbsoluteFilter(content)
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ settings.COMPRESS_URL = params['url'] = 'http://media.example.com/'
+ filter = CssAbsoluteFilter(content)
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+
+ def test_css_absolute_filter_https(self):
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ params = {
+ 'url': settings.COMPRESS_URL,
+ 'hash': self.hashing_func(imagefilename),
+ }
+ output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
+ "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
+ filter = CssAbsoluteFilter(self.content)
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
+ filter = CssAbsoluteFilter(self.content)
+ filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
+ output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
+ "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+
+ def test_css_absolute_filter_relative_path(self):
+ filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'static', 'whatever/../css/url/test.css')
+ imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ params = {
+ 'url': settings.COMPRESS_URL,
+ 'hash': self.hashing_func(imagefilename),
+ }
+ output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
+ "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
+ filter = CssAbsoluteFilter(self.content)
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
+ filter = CssAbsoluteFilter(self.content)
+ output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
+ "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+
+ def test_css_hunks(self):
+ hash_dict = {
+ 'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
+ 'hash2': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')),
+ }
+ self.assertEqual(["""\
+p { background: url('/static/img/python.png?%(hash1)s'); }
+p { background: url('/static/img/python.png?%(hash1)s'); }
+p { background: url('/static/img/python.png?%(hash1)s'); }
+p { background: url('/static/img/python.png?%(hash1)s'); }
+p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/static/img/python.png?%(hash1)s'); }
+""" % hash_dict,
+ """\
+p { background: url('/static/img/add.png?%(hash2)s'); }
+p { background: url('/static/img/add.png?%(hash2)s'); }
+p { background: url('/static/img/add.png?%(hash2)s'); }
+p { background: url('/static/img/add.png?%(hash2)s'); }
+p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/static/img/add.png?%(hash2)s'); }
+""" % hash_dict], list(self.css_node.hunks()))
+
+ def test_guess_filename(self):
+ for base_url in ('/static/', 'http://static.example.com/'):
+ settings.COMPRESS_URL = base_url
+ url = '%s/img/python.png' % settings.COMPRESS_URL.rstrip('/')
+ path = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ content = "p { background: url('%s') }" % url
+ filter = CssAbsoluteFilter(content)
+ self.assertEqual(path, filter.guess_filename(url))
+
+
+class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
+ hashing_method = 'content'
+ hashing_func = staticmethod(get_hashed_content)
+
+ def setUp(self):
+ super(CssAbsolutizingTestCaseWithHash, self).setUp()
+ self.css = """
+ <link rel="stylesheet" href="/static/css/url/url1.css" type="text/css" charset="utf-8">
+ <link rel="stylesheet" href="/static/css/url/2/url2.css" type="text/css" charset="utf-8">
+ """
+ self.css_node = CssCompressor(self.css)
+
+
+class CssDataUriTestCase(TestCase):
+ def setUp(self):
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_CSS_FILTERS = [
+ 'compressor.filters.css_default.CssAbsoluteFilter',
+ 'compressor.filters.datauri.CssDataUriFilter',
+ ]
+ settings.COMPRESS_URL = '/static/'
+ settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
+ self.css = """
+ <link rel="stylesheet" href="/static/css/datauri.css" type="text/css">
+ """
+ self.css_node = CssCompressor(self.css)
+
+ def test_data_uris(self):
+ datauri_hash = get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'img/python.png'))
+ out = ['''.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }
+.add-with-hash { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }
+.python { background-image: url("/static/img/python.png?%s"); }
+.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }
+''' % datauri_hash]
+ self.assertEqual(out, list(self.css_node.hunks()))
+
+
+class TemplateTestCase(TestCase):
+ @override_settings(COMPRESS_TEMPLATE_FILTER_CONTEXT={
+ 'stuff': 'thing',
+ 'gimmick': 'bold'
+ })
+ def test_template_filter(self):
+ content = """
+ #content {background-image: url("{{ STATIC_URL|default:stuff }}/images/bg.png");}
+ #footer {font-weight: {{ gimmick }};}
+ """
+ input = """
+ #content {background-image: url("thing/images/bg.png");}
+ #footer {font-weight: bold;}
+ """
+ self.assertEqual(input, TemplateFilter(content).input())
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import with_statement, unicode_literals
+
+import sys
+
+from django.test import TestCase
+from django.utils import unittest, six
+from django.test.utils import override_settings
+
+from compressor.conf import settings
+from compressor.tests.test_base import css_tag
+
+
+@unittest.skipUnless(not six.PY3 or sys.version_info[:2] >= (3, 3),
+ 'Jinja can only run on Python < 3 and >= 3.3')
+class TestJinja2CompressorExtension(TestCase):
+ """
+ Test case for jinja2 extension.
+
+ .. note::
+ At tests we need to make some extra care about whitespace. Please note
+ that we use jinja2 specific controls (*minus* character at block's
+ beginning or end). For more information see jinja2 documentation.
+ """
+ def assertStrippedEqual(self, result, expected):
+ self.assertEqual(result.strip(), expected.strip(), "%r != %r" % (
+ result.strip(), expected.strip()))
+
+ def setUp(self):
+ import jinja2
+ self.jinja2 = jinja2
+ from compressor.contrib.jinja2ext import CompressorExtension
+ self.env = self.jinja2.Environment(extensions=[CompressorExtension])
+
+ def test_error_raised_if_no_arguments_given(self):
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
+ self.env.from_string, '{% compress %}Foobar{% endcompress %}')
+
+ def test_error_raised_if_wrong_kind_given(self):
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
+ self.env.from_string, '{% compress foo %}Foobar{% endcompress %}')
+
+ def test_error_raised_if_wrong_closing_kind_given(self):
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
+ self.env.from_string, '{% compress js %}Foobar{% endcompress css %}')
+
+ def test_error_raised_if_wrong_mode_given(self):
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
+ self.env.from_string, '{% compress css foo %}Foobar{% endcompress %}')
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_compress_is_disabled(self):
+ tag_body = '\n'.join([
+ '<link rel="stylesheet" href="css/one.css" type="text/css" charset="utf-8">',
+ '<style type="text/css">p { border:5px solid green;}</style>',
+ '<link rel="stylesheet" href="css/two.css" type="text/css" charset="utf-8">',
+ ])
+ template_string = '{% compress css %}' + tag_body + '{% endcompress %}'
+ template = self.env.from_string(template_string)
+ self.assertEqual(tag_body, template.render())
+
+ # Test with explicit kind
+ template_string = '{% compress css %}' + tag_body + '{% endcompress css %}'
+ template = self.env.from_string(template_string)
+ self.assertEqual(tag_body, template.render())
+
+ def test_empty_tag(self):
+ template = self.env.from_string("""{% compress js %}{% block js %}
+ {% endblock %}{% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ self.assertEqual('', template.render(context))
+
+ def test_empty_tag_with_kind(self):
+ template = self.env.from_string("""{% compress js %}{% block js %}
+ {% endblock %}{% endcompress js %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ self.assertEqual('', template.render(context))
+
+ def test_css_tag(self):
+ template = self.env.from_string("""{% compress css -%}
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css" charset="utf-8">
+ <style type="text/css">p { border:5px solid green;}</style>
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css" charset="utf-8">
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = css_tag("/static/CACHE/css/e41ba2cc6982.css")
+ self.assertEqual(out, template.render(context))
+
+ def test_nonascii_css_tag(self):
+ template = self.env.from_string("""{% compress css -%}
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/nonasc.css" type="text/css" charset="utf-8">
+ <style type="text/css">p { border:5px solid green;}</style>
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = css_tag("/static/CACHE/css/799f6defe43c.css")
+ self.assertEqual(out, template.render(context))
+
+ def test_js_tag(self):
+ template = self.env.from_string("""{% compress js -%}
+ <script src="{{ STATIC_URL }}js/one.js" type="text/javascript" charset="utf-8"></script>
+ <script type="text/javascript" charset="utf-8">obj.value = "value";</script>
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ self.assertEqual(out, template.render(context))
+
+ def test_nonascii_js_tag(self):
+ template = self.env.from_string("""{% compress js -%}
+ <script src="{{ STATIC_URL }}js/nonasc.js" type="text/javascript" charset="utf-8"></script>
+ <script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = '<script type="text/javascript" src="/static/CACHE/js/e214fe629b28.js"></script>'
+ self.assertEqual(out, template.render(context))
+
+ def test_nonascii_latin1_js_tag(self):
+ template = self.env.from_string("""{% compress js -%}
+ <script src="{{ STATIC_URL }}js/nonasc-latin1.js" type="text/javascript" charset="latin-1"></script>
+ <script type="text/javascript">var test_value = "\u2014";</script>
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = '<script type="text/javascript" src="/static/CACHE/js/be9e078b5ca7.js"></script>'
+ self.assertEqual(out, template.render(context))
+
+ def test_css_inline(self):
+ template = self.env.from_string("""{% compress css, inline -%}
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css" charset="utf-8">
+ <style type="text/css">p { border:5px solid green;}</style>
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = '\n'.join([
+ '<style type="text/css">body { background:#990; }',
+ 'p { border:5px solid green;}</style>',
+ ])
+ self.assertEqual(out, template.render(context))
+
+ def test_js_inline(self):
+ template = self.env.from_string("""{% compress js, inline -%}
+ <script src="{{ STATIC_URL }}js/one.js" type="text/css" type="text/javascript" charset="utf-8"></script>
+ <script type="text/javascript" charset="utf-8">obj.value = "value";</script>
+ {% endcompress %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = '<script type="text/javascript">obj={};obj.value="value";</script>'
+ self.assertEqual(out, template.render(context))
+
+ def test_nonascii_inline_css(self):
+ org_COMPRESS_ENABLED = settings.COMPRESS_ENABLED
+ settings.COMPRESS_ENABLED = False
+ template = self.env.from_string('{% compress css %}'
+ '<style type="text/css">'
+ '/* русский текст */'
+ '</style>{% endcompress %}')
+ out = '<link rel="stylesheet" href="/static/CACHE/css/b2cec0f8cb24.css" type="text/css" />'
+ settings.COMPRESS_ENABLED = org_COMPRESS_ENABLED
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ self.assertEqual(out, template.render(context))
--- /dev/null
+from __future__ import with_statement, unicode_literals
+import io
+import os
+import sys
+
+from django.core.management.base import CommandError
+from django.template import Template, Context
+from django.test import TestCase
+from django.utils import six, unittest
+
+from compressor.cache import flush_offline_manifest, get_offline_manifest
+from compressor.conf import settings
+from compressor.exceptions import OfflineGenerationError
+from compressor.management.commands.compress import Command as CompressCommand
+from compressor.storage import default_storage
+
+if six.PY3:
+ # there is an 'io' module in python 2.6+, but io.StringIO does not
+ # accept regular strings, just unicode objects
+ from io import StringIO
+else:
+ try:
+ from cStringIO import StringIO
+ except ImportError:
+ from StringIO import StringIO
+
+# The Jinja2 tests fail on Python 3.2 due to the following:
+# The line in compressor/management/commands/compress.py:
+# compressor_nodes.setdefault(template, []).extend(nodes)
+# causes the error "unhashable type: 'Template'"
+_TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2)
+
+
+class OfflineTestCaseMixin(object):
+ template_name = "test_compressor_offline.html"
+ verbosity = 0
+ # Change this for each test class
+ templates_dir = ""
+ expected_hash = ""
+ # Engines to test
+ if _TEST_JINJA2:
+ engines = ("django", "jinja2")
+ else:
+ engines = ("django",)
+
+ def setUp(self):
+ self._old_compress = settings.COMPRESS_ENABLED
+ self._old_compress_offline = settings.COMPRESS_OFFLINE
+ self._old_template_dirs = settings.TEMPLATE_DIRS
+ self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ self.log = StringIO()
+
+ # Reset template dirs, because it enables us to force compress to
+ # consider only a specific directory (helps us make true,
+ # independant unit tests).
+ # Specify both Jinja2 and Django template locations. When the wrong engine
+ # is used to parse a template, the TemplateSyntaxError will cause the
+ # template to be skipped over.
+ django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir)
+ jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir)
+ settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir)
+
+ # Enable offline compress
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_OFFLINE = True
+
+ if "django" in self.engines:
+ self.template_path = os.path.join(django_template_dir, self.template_name)
+
+ with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file:
+ self.template = Template(file.read())
+
+ self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT
+
+ if "jinja2" in self.engines:
+ # Setup Jinja2 settings.
+ settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env()
+ jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
+ self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name)
+
+ with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file:
+ self.template_jinja2 = jinja2_env.from_string(file.read())
+
+ def tearDown(self):
+ settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment
+ settings.COMPRESS_ENABLED = self._old_compress
+ settings.COMPRESS_OFFLINE = self._old_compress_offline
+ settings.TEMPLATE_DIRS = self._old_template_dirs
+ manifest_path = os.path.join('CACHE', 'manifest.json')
+ if default_storage.exists(manifest_path):
+ default_storage.delete(manifest_path)
+
+ def _render_template(self, engine):
+ if engine == "django":
+ return self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ elif engine == "jinja2":
+ return self.template_jinja2.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
+ else:
+ return None
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ self.assertEqual(1, count)
+ self.assertEqual([
+ '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
+ ], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, "".join(result) + "\n")
+
+ def test_offline(self):
+ for engine in self.engines:
+ self._test_offline(engine=engine)
+
+ def _get_jinja2_env(self):
+ import jinja2
+ import jinja2.ext
+ from compressor.offline.jinja2 import url_for, SpacelessExtension
+ from compressor.contrib.jinja2ext import CompressorExtension
+
+ # Extensions needed for the test cases only.
+ extensions = [
+ CompressorExtension,
+ SpacelessExtension,
+ jinja2.ext.with_,
+ jinja2.ext.do,
+ ]
+ loader = self._get_jinja2_loader()
+ env = jinja2.Environment(extensions=extensions, loader=loader)
+ env.globals['url_for'] = url_for
+
+ return env
+
+ def _get_jinja2_loader(self):
+ import jinja2
+
+ loader = jinja2.FileSystemLoader(settings.TEMPLATE_DIRS, encoding=settings.FILE_CHARSET)
+ return loader
+
+
+class OfflineGenerationSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_duplicate"
+
+ # We don't need to test multiples engines here.
+ engines = ("django",)
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ # Only one block compressed, the second identical one was skipped.
+ self.assertEqual(1, count)
+ # Only 1 <script> block in returned result as well.
+ self.assertEqual([
+ '<script type="text/javascript" src="/static/CACHE/js/f5e179b8eca4.js"></script>',
+ ], result)
+ rendered_template = self._render_template(engine)
+ # But rendering the template returns both (identical) scripts.
+ self.assertEqual(rendered_template, "".join(result * 2) + "\n")
+
+
+class OfflineGenerationBlockSuperTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_block_super"
+ expected_hash = "7c02d201f69d"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
+
+
+class OfflineGenerationBlockSuperMultipleTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_block_super_multiple"
+ expected_hash = "f8891c416981"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
+
+
+class OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_block_super_multiple_cached"
+ expected_hash = "2f6ef61c488e"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
+
+ def setUp(self):
+ self._old_template_loaders = settings.TEMPLATE_LOADERS
+ settings.TEMPLATE_LOADERS = (
+ ('django.template.loaders.cached.Loader', (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )),
+ )
+ super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).setUp()
+
+ def tearDown(self):
+ super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).tearDown()
+ settings.TEMPLATE_LOADERS = self._old_template_loaders
+
+
+class OfflineGenerationBlockSuperTestCaseWithExtraContent(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_block_super_extra"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ self.assertEqual(2, count)
+ self.assertEqual([
+ '<script type="text/javascript" src="/static/CACHE/js/ced14aec5856.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/7c02d201f69d.js"></script>'
+ ], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, "".join(result) + "\n")
+
+
+class OfflineGenerationConditionTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_condition"
+ expected_hash = "4e3758d50224"
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ settings.COMPRESS_OFFLINE_CONTEXT = {
+ 'condition': 'red',
+ }
+ super(OfflineGenerationConditionTestCase, self).setUp()
+
+ def tearDown(self):
+ self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
+ super(OfflineGenerationConditionTestCase, self).tearDown()
+
+
+class OfflineGenerationTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_templatetag"
+ expected_hash = "a27e1d3a619a"
+
+
+class OfflineGenerationStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_static_templatetag"
+ expected_hash = "dfa2bb387fa8"
+
+
+class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_with_context"
+ expected_hash = "5838e2fd66af"
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ settings.COMPRESS_OFFLINE_CONTEXT = {
+ 'content': 'OK!',
+ }
+ super(OfflineGenerationTestCaseWithContext, self).setUp()
+
+ def tearDown(self):
+ settings.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
+ super(OfflineGenerationTestCaseWithContext, self).tearDown()
+
+
+class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_error_handling"
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+
+ if engine == "django":
+ self.assertEqual(2, count)
+ else:
+ # Because we use env.parse in Jinja2Parser, the engine does not
+ # actually load the "extends" and "includes" templates, and so
+ # it is unable to detect that they are missing. So all the "compress"
+ # nodes are processed correctly.
+ self.assertEqual(4, count)
+ self.assertEqual(engine, "jinja2")
+ self.assertIn('<link rel="stylesheet" href="/static/CACHE/css/78bd7a762e2d.css" type="text/css" />', result)
+ self.assertIn('<link rel="stylesheet" href="/static/CACHE/css/e31030430724.css" type="text/css" />', result)
+
+ self.assertIn('<script type="text/javascript" src="/static/CACHE/js/3872c9ae3f42.js"></script>', result)
+ self.assertIn('<script type="text/javascript" src="/static/CACHE/js/cd8870829421.js"></script>', result)
+
+
+class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_error_handling'
+
+ def setUp(self):
+ self._old_compress_precompilers = settings.COMPRESS_PRECOMPILERS
+ settings.COMPRESS_PRECOMPILERS = (('text/coffeescript', 'non-existing-binary'),)
+ super(OfflineGenerationTestCaseWithError, self).setUp()
+
+ def _test_offline(self, engine):
+ """
+ Test that a CommandError is raised with DEBUG being False as well as
+ True, as otherwise errors in configuration will never show in
+ production.
+ """
+ self._old_debug = settings.DEBUG
+
+ try:
+ settings.DEBUG = True
+ self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
+
+ settings.DEBUG = False
+ self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
+
+ finally:
+ settings.DEBUG = self._old_debug
+
+ def tearDown(self):
+ settings.COMPRESS_PRECOMPILERS = self._old_compress_precompilers
+ super(OfflineGenerationTestCaseWithError, self).tearDown()
+
+
+class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "basic"
+ expected_hash = "f5e179b8eca4"
+
+ def test_rendering_without_manifest_raises_exception(self):
+ # flush cached manifest
+ flush_offline_manifest()
+ self.assertRaises(OfflineGenerationError,
+ self.template.render, Context({}))
+
+ @unittest.skipIf(not _TEST_JINJA2, "No Jinja2 testing")
+ def test_rendering_without_manifest_raises_exception_jinja2(self):
+ # flush cached manifest
+ flush_offline_manifest()
+ self.assertRaises(OfflineGenerationError,
+ self.template_jinja2.render, {})
+
+ def _test_deleting_manifest_does_not_affect_rendering(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ get_offline_manifest()
+ manifest_path = os.path.join('CACHE', 'manifest.json')
+ if default_storage.exists(manifest_path):
+ default_storage.delete(manifest_path)
+ self.assertEqual(1, count)
+ self.assertEqual([
+ '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
+ ], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, "".join(result) + "\n")
+
+ def test_deleting_manifest_does_not_affect_rendering(self):
+ for engine in self.engines:
+ self._test_deleting_manifest_does_not_affect_rendering(engine)
+
+ def test_requires_model_validation(self):
+ self.assertFalse(CompressCommand.requires_model_validation)
+
+ def test_get_loaders(self):
+ old_loaders = settings.TEMPLATE_LOADERS
+ settings.TEMPLATE_LOADERS = (
+ ('django.template.loaders.cached.Loader', (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )),
+ )
+ try:
+ from django.template.loaders.filesystem import Loader as FileSystemLoader
+ from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
+ except ImportError:
+ pass
+ else:
+ loaders = CompressCommand().get_loaders()
+ self.assertTrue(isinstance(loaders[0], FileSystemLoader))
+ self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader))
+ finally:
+ settings.TEMPLATE_LOADERS = old_loaders
+
+
+class OfflineGenerationBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase):
+ template_names = ["base.html", "base2.html", "test_compressor_offline.html"]
+ templates_dir = 'test_block_super_base_compressed'
+ expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'f8891c416981']
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
+
+ def setUp(self):
+ super(OfflineGenerationBlockSuperBaseCompressed, self).setUp()
+
+ self.template_paths = []
+ self.templates = []
+ for template_name in self.template_names:
+ template_path = os.path.join(settings.TEMPLATE_DIRS[0], template_name)
+ self.template_paths.append(template_path)
+ with io.open(template_path, encoding=settings.FILE_CHARSET) as file:
+ template = Template(file.read())
+ self.templates.append(template)
+
+ def _render_template(self, template, engine):
+ if engine == "django":
+ return template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ elif engine == "jinja2":
+ return template.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
+ else:
+ return None
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ self.assertEqual(len(self.expected_hash), count)
+ for expected_hash, template in zip(self.expected_hash, self.templates):
+ expected_output = '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (expected_hash, )
+ self.assertIn(expected_output, result)
+ rendered_template = self._render_template(template, engine)
+ self.assertEqual(rendered_template, expected_output + '\n')
+
+
+class OfflineGenerationInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_inline_non_ascii"
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ settings.COMPRESS_OFFLINE_CONTEXT = {
+ 'test_non_ascii_value': '\u2014',
+ }
+ super(OfflineGenerationInlineNonAsciiTestCase, self).setUp()
+
+ def tearDown(self):
+ self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
+ super(OfflineGenerationInlineNonAsciiTestCase, self).tearDown()
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, "".join(result) + "\n")
+
+
+class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_complex"
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ settings.COMPRESS_OFFLINE_CONTEXT = {
+ 'condition': 'OK!',
+ # Django templating does not allow definition of tuples in the
+ # templates. Make sure this is same as test_templates_jinja2/test_complex.
+ 'my_names': ("js/one.js", "js/nonasc.js"),
+ }
+ super(OfflineGenerationComplexTestCase, self).setUp()
+
+ def tearDown(self):
+ self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
+ super(OfflineGenerationComplexTestCase, self).tearDown()
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ self.assertEqual(3, count)
+ self.assertEqual([
+ '<script type="text/javascript" src="/static/CACHE/js/0e8807bebcee.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/eed1d222933e.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/00b4baffe335.js"></script>',
+ ], result)
+ rendered_template = self._render_template(engine)
+ result = (result[0], result[2])
+ self.assertEqual(rendered_template, "".join(result) + "\n")
+
+
+# Coffin does not work on Python 3.2+ due to:
+# The line at coffin/template/__init__.py:15
+# from library import *
+# causing 'ImportError: No module named library'.
+# It seems there is no evidence nor indicated support for Python 3+.
+@unittest.skipIf(sys.version_info >= (3, 2),
+ "Coffin does not support 3.2+")
+class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_coffin"
+ expected_hash = "32c8281e3346"
+ engines = ("jinja2",)
+
+ def _get_jinja2_env(self):
+ import jinja2
+ from coffin.common import env
+ from compressor.contrib.jinja2ext import CompressorExtension
+
+ # Could have used the env.add_extension method, but it's only available
+ # in Jinja2 v2.5
+ new_env = jinja2.Environment(extensions=[CompressorExtension])
+ env.extensions.update(new_env.extensions)
+
+ return env
+
+
+# Jingo does not work when using Python 3.2 due to the use of Unicode string
+# prefix (and possibly other stuff), but it actually works when using Python 3.3
+# since it tolerates the use of the Unicode string prefix. Python 3.3 support
+# is also evident in its tox.ini file.
+@unittest.skipIf(sys.version_info >= (3, 2) and sys.version_info < (3, 3),
+ "Jingo does not support 3.2")
+class OfflineGenerationJingoTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_jingo"
+ expected_hash = "61ec584468eb"
+ engines = ("jinja2",)
+
+ def _get_jinja2_env(self):
+ import jinja2
+ import jinja2.ext
+ from jingo import env
+ from compressor.contrib.jinja2ext import CompressorExtension
+ from compressor.offline.jinja2 import SpacelessExtension, url_for
+
+ # Could have used the env.add_extension method, but it's only available
+ # in Jinja2 v2.5
+ new_env = jinja2.Environment(extensions=[CompressorExtension, SpacelessExtension, jinja2.ext.with_])
+ env.extensions.update(new_env.extensions)
+ env.globals['url_for'] = url_for
+
+ return env
--- /dev/null
+from __future__ import with_statement
+import os
+
+try:
+ import lxml
+except ImportError:
+ lxml = None
+
+try:
+ import html5lib
+except ImportError:
+ html5lib = None
+
+try:
+ from BeautifulSoup import BeautifulSoup
+except ImportError:
+ BeautifulSoup = None
+
+from django.utils import unittest
+from django.test.utils import override_settings
+
+from compressor.base import SOURCE_HUNK, SOURCE_FILE
+from compressor.conf import settings
+from compressor.tests.test_base import CompressorTestCase
+
+
+class ParserTestCase(object):
+ def setUp(self):
+ self.old_parser = settings.COMPRESS_PARSER
+ settings.COMPRESS_PARSER = self.parser_cls
+ super(ParserTestCase, self).setUp()
+
+ def tearDown(self):
+ settings.COMPRESS_PARSER = self.old_parser
+
+
+@unittest.skipIf(lxml is None, 'lxml not found')
+class LxmlParserTests(ParserTestCase, CompressorTestCase):
+ parser_cls = 'compressor.parser.LxmlParser'
+
+
+@unittest.skipIf(html5lib is None, 'html5lib not found')
+class Html5LibParserTests(ParserTestCase, CompressorTestCase):
+ parser_cls = 'compressor.parser.Html5LibParser'
+ # Special test variants required since xml.etree holds attributes
+ # as a plain dictionary, e.g. key order is unpredictable.
+
+ def test_css_split(self):
+ split = self.css_node.split_contents()
+ out0 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'),
+ 'css/one.css',
+ '{http://www.w3.org/1999/xhtml}link',
+ {'rel': 'stylesheet', 'href': '/static/css/one.css',
+ 'type': 'text/css'},
+ )
+ self.assertEqual(out0, split[0][:3] + (split[0][3].tag,
+ split[0][3].attrib))
+ out1 = (
+ SOURCE_HUNK,
+ 'p { border:5px solid green;}',
+ None,
+ '<style type="text/css">p { border:5px solid green;}</style>',
+ )
+ self.assertEqual(out1, split[1][:3] +
+ (self.css_node.parser.elem_str(split[1][3]),))
+ out2 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'two.css'),
+ 'css/two.css',
+ '{http://www.w3.org/1999/xhtml}link',
+ {'rel': 'stylesheet', 'href': '/static/css/two.css',
+ 'type': 'text/css'},
+ )
+ self.assertEqual(out2, split[2][:3] + (split[2][3].tag,
+ split[2][3].attrib))
+
+ def test_js_split(self):
+ split = self.js_node.split_contents()
+ out0 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'js', 'one.js'),
+ 'js/one.js',
+ '{http://www.w3.org/1999/xhtml}script',
+ {'src': '/static/js/one.js', 'type': 'text/javascript'},
+ None,
+ )
+ self.assertEqual(out0, split[0][:3] + (split[0][3].tag,
+ split[0][3].attrib,
+ split[0][3].text))
+ out1 = (
+ SOURCE_HUNK,
+ 'obj.value = "value";',
+ None,
+ '{http://www.w3.org/1999/xhtml}script',
+ {'type': 'text/javascript'},
+ 'obj.value = "value";',
+ )
+ self.assertEqual(out1, split[1][:3] + (split[1][3].tag,
+ split[1][3].attrib,
+ split[1][3].text))
+
+ def test_css_return_if_off(self):
+ settings.COMPRESS_ENABLED = False
+ # Yes, they are semantically equal but attributes might be
+ # scrambled in unpredictable order. A more elaborate check
+ # would require parsing both arguments with a different parser
+ # and then evaluating the result, which no longer is
+ # a meaningful unit test.
+ self.assertEqual(len(self.css), len(self.css_node.output()))
+
+ @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False)
+ def test_js_return_if_off(self):
+ # As above.
+ self.assertEqual(len(self.js), len(self.js_node.output()))
+
+
+@unittest.skipIf(BeautifulSoup is None, 'BeautifulSoup not found')
+class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase):
+ parser_cls = 'compressor.parser.BeautifulSoupParser'
+
+
+class HtmlParserTests(ParserTestCase, CompressorTestCase):
+ parser_cls = 'compressor.parser.HtmlParser'
--- /dev/null
+from django.test import TestCase
+
+from mock import Mock
+
+from compressor.conf import settings
+from compressor.css import CssCompressor
+from compressor.js import JsCompressor
+from compressor.signals import post_compress
+
+
+class PostCompressSignalTestCase(TestCase):
+ def setUp(self):
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_PRECOMPILERS = ()
+ settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
+ self.css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
+<style type="text/css">p { border:5px solid green;}</style>
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+ self.css_node = CssCompressor(self.css)
+
+ self.js = """\
+<script src="/static/js/one.js" type="text/javascript"></script>
+<script type="text/javascript">obj.value = "value";</script>"""
+ self.js_node = JsCompressor(self.js)
+
+ def tearDown(self):
+ post_compress.disconnect()
+
+ def test_js_signal_sent(self):
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ self.js_node.output()
+ args, kwargs = callback.call_args
+ self.assertEqual(JsCompressor, kwargs['sender'])
+ self.assertEqual('js', kwargs['type'])
+ self.assertEqual('file', kwargs['mode'])
+ context = kwargs['context']
+ assert 'url' in context['compressed']
+
+ def test_css_signal_sent(self):
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ self.css_node.output()
+ args, kwargs = callback.call_args
+ self.assertEqual(CssCompressor, kwargs['sender'])
+ self.assertEqual('css', kwargs['type'])
+ self.assertEqual('file', kwargs['mode'])
+ context = kwargs['context']
+ assert 'url' in context['compressed']
+
+ def test_css_signal_multiple_media_attributes(self):
+ css = """\
+<link rel="stylesheet" href="/static/css/one.css" media="handheld" type="text/css" />
+<style type="text/css" media="print">p { border:5px solid green;}</style>
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+ css_node = CssCompressor(css)
+
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ css_node.output()
+ self.assertEqual(3, callback.call_count)
--- /dev/null
+from __future__ import with_statement, unicode_literals
+import errno
+import os
+
+from django.core.files.base import ContentFile
+from django.core.files.storage import get_storage_class
+from django.test import TestCase
+from django.utils.functional import LazyObject
+
+from compressor import storage
+from compressor.conf import settings
+from compressor.tests.test_base import css_tag
+from compressor.tests.test_templatetags import render
+
+
+class GzipStorage(LazyObject):
+ def _setup(self):
+ self._wrapped = get_storage_class('compressor.storage.GzipCompressorFileStorage')()
+
+
+class StorageTestCase(TestCase):
+ def setUp(self):
+ self.old_enabled = settings.COMPRESS_ENABLED
+ settings.COMPRESS_ENABLED = True
+ self.default_storage = storage.default_storage
+ storage.default_storage = GzipStorage()
+
+ def tearDown(self):
+ storage.default_storage = self.default_storage
+ settings.COMPRESS_ENABLED = self.old_enabled
+
+ def test_gzip_storage(self):
+ storage.default_storage.save('test.txt', ContentFile('yeah yeah'))
+ self.assertTrue(os.path.exists(os.path.join(settings.COMPRESS_ROOT, 'test.txt')))
+ self.assertTrue(os.path.exists(os.path.join(settings.COMPRESS_ROOT, 'test.txt.gz')))
+
+ def test_css_tag_with_storage(self):
+ template = """{% load compress %}{% compress css %}
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css">
+ <style type="text/css">p { border:5px solid white;}</style>
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
+ {% endcompress %}
+ """
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ out = css_tag("/static/CACHE/css/1d4424458f88.css")
+ self.assertEqual(out, render(template, context))
+
+ def test_race_condition_handling(self):
+ # Hold on to original os.remove
+ original_remove = os.remove
+
+ def race_remove(path):
+ "Patched os.remove to raise ENOENT (No such file or directory)"
+ original_remove(path)
+ raise OSError(errno.ENOENT, 'Fake ENOENT')
+
+ try:
+ os.remove = race_remove
+ self.default_storage.save('race.file', ContentFile('Fake ENOENT'))
+ self.default_storage.delete('race.file')
+ self.assertFalse(self.default_storage.exists('race.file'))
+ finally:
+ # Restore os.remove
+ os.remove = original_remove
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+{% load compress %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+{% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert should be included");
+ </script>
+{% endspaceless %}{% endblock %}
--- /dev/null
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+{% endspaceless %}{% endblock %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+{% load compress %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ <script type="text/javascript">
+ alert("this alert should be alone.");
+ </script>
+ {% endcompress %}
+
+ {% compress js %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert should be included");
+ </script>
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% extends "base2.html" %}
+{% load compress %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% extends "base2.html" %}
+{% load compress %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
--- /dev/null
+{% load compress static %}{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default:"yellow" }}");</script>
+ {% with names=my_names %}{% spaceless %}
+ {% for name in names %}
+ <script type="text/javascript" src="{% static name %}"></script>
+ {% endfor %}
+ {% endspaceless %}{% endwith %}
+ {% endcompress %}
+{% endif %}{% if not condition %}
+ {% compress js %}
+ <script type="text/javascript">var not_ok;</script>
+ {% endcompress %}
+{% else %}
+ {% compress js %}
+ <script type="text/javascript">var ok = "ok";</script>
+ {% endcompress %}
+{% endif %}{% endspaceless %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default:"yellow" }}");</script>
+ {% endcompress %}
+{% endif %}{% endspaceless %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% extends "buggy_template.html" %}
+{% load compress %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: orange;
+ }
+ </style>
+{% endcompress %}
--- /dev/null
+{% load compress %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: pink;
+ }
+ </style>
+{% endcompress %}
+
+
+{% fail %}
--- /dev/null
+{% extends "missing.html" %}
+{% load compress %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: purple;
+ }
+ </style>
+{% endcompress %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test, should pass in spite of errors in other templates");
+ </script>
+{% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% load compress %}
+
+{% compress js %}
+ <script type="text/coffeescript" charset="utf-8">
+ a = 1
+ </script>
+{% endcompress %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js inline %}
+ <script type="text/javascript">
+ var value = '{{ test_non_ascii_value }}';
+ </script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% load compress static %}{% spaceless %}
+
+{% compress js %}
+ <script>alert('amazing');</script>
+ <script type="text/javascript" src="{% static "js/one.js" %}"></script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("{% firstof "testtemplatetag" %}");
+ </script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("{{ content|default:"Ooops!" }}");
+ </script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ <script type="text/javascript">
+ alert("this alert should be alone.");
+ </script>
+ {% endcompress %}
+
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
--- /dev/null
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
--- /dev/null
+{%- load compress -%}
+{% spaceless %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");
+ var ok = "{% if (25*4) is divisibleby 50 %}ok{% endif %}";
+ </script>
+ {% with "js/one.js" as name -%}
+ <script type="text/javascript" src="{% static name %}"></script>
+ {%- endwith %}
+ {% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");</script>
+ {% with names=[] -%}
+ {%- do names.append("js/one.js") -%}
+ {%- do names.append("js/nonasc.js") -%}
+ {% for name in names -%}
+ <script type="text/javascript" src="{{url_for('static', filename=name)}}"></script>
+ {%- endfor %}
+ {%- endwith %}
+ {% endcompress %}
+{% endif %}
+{% if not condition -%}
+ {% compress js %}
+ <script type="text/javascript">var not_ok;</script>
+ {% endcompress %}
+{%- else -%}
+ {% compress js %}
+ <script type="text/javascript">var ok = "{% if (25*4) is divisibleby 50 %}ok{% endif %}";</script>
+ {% endcompress %}
+{%- endif %}
+{% endspaceless %}
--- /dev/null
+{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");</script>
+ {% endcompress %}
+{% endif %}{% endspaceless %}
--- /dev/null
+{% extends "buggy_template.html" %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: orange;
+ }
+ </style>
+{% endcompress %}
--- /dev/null
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: pink;
+ }
+ </style>
+{% endcompress %}
+
+
+{% fail %}
--- /dev/null
+{% extends "missing.html" %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: purple;
+ }
+ </style>
+{% endcompress %}
--- /dev/null
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test, should pass in spite of errors in other templates");
+ </script>
+{% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% compress js %}
+ <script type="text/coffeescript" charset="utf-8">
+ a = 1
+ </script>
+{% endcompress %}
--- /dev/null
+{% spaceless %}
+
+{% compress js, inline %}
+ <script type="text/javascript">
+ var value = '{{ test_non_ascii_value }}';
+ </script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% spaceless %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");
+ var ok = "{% if (25*4) is divisibleby 50 %}ok{% endif %}";
+ var text = "{{"hello\nworld"|nl2br}}";
+ </script>
+ {% with name="js/one.js" -%}
+ <script type="text/javascript" src="{{ 8|ifeq(2*4, url_for('static', name)) }}"></script>
+ {%- endwith %}
+ {% endcompress %}
+{% endspaceless %}
--- /dev/null
+{% spaceless %}
+
+{% compress js %}
+ <script>alert('amazing');</script>
+ <script type="text/javascript" src="{{ url_for('static', filename="js/one.js") }}"></script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("{{ "testtemplateTAG"|lower }}");
+ </script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("{{ content|default("Ooops!") }}");
+ </script>
+{% endcompress %}{% endspaceless %}
--- /dev/null
+from __future__ import with_statement, unicode_literals
+
+import os
+import sys
+
+from mock import Mock
+
+from django.template import Template, Context, TemplateSyntaxError
+from django.test import TestCase
+from django.test.utils import override_settings
+
+from compressor.conf import settings
+from compressor.signals import post_compress
+from compressor.tests.test_base import css_tag, test_dir
+
+
+def render(template_string, context_dict=None):
+ """
+ A shortcut for testing template output.
+ """
+ if context_dict is None:
+ context_dict = {}
+ c = Context(context_dict)
+ t = Template(template_string)
+ return t.render(c).strip()
+
+
+class TemplatetagTestCase(TestCase):
+ def setUp(self):
+ self.old_enabled = settings.COMPRESS_ENABLED
+ settings.COMPRESS_ENABLED = True
+ self.context = {'STATIC_URL': settings.COMPRESS_URL}
+
+ def tearDown(self):
+ settings.COMPRESS_ENABLED = self.old_enabled
+
+ def test_empty_tag(self):
+ template = """{% load compress %}{% compress js %}{% block js %}
+ {% endblock %}{% endcompress %}"""
+ self.assertEqual('', render(template, self.context))
+
+ def test_css_tag(self):
+ template = """{% load compress %}{% compress css %}
+<link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css">
+<style type="text/css">p { border:5px solid green;}</style>
+<link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
+{% endcompress %}"""
+ out = css_tag("/static/CACHE/css/e41ba2cc6982.css")
+ self.assertEqual(out, render(template, self.context))
+
+ def test_uppercase_rel(self):
+ template = """{% load compress %}{% compress css %}
+<link rel="StyleSheet" href="{{ STATIC_URL }}css/one.css" type="text/css">
+<style type="text/css">p { border:5px solid green;}</style>
+<link rel="StyleSheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
+{% endcompress %}"""
+ out = css_tag("/static/CACHE/css/e41ba2cc6982.css")
+ self.assertEqual(out, render(template, self.context))
+
+ def test_nonascii_css_tag(self):
+ template = """{% load compress %}{% compress css %}
+ <link rel="stylesheet" href="{{ STATIC_URL }}css/nonasc.css" type="text/css">
+ <style type="text/css">p { border:5px solid green;}</style>
+ {% endcompress %}
+ """
+ out = css_tag("/static/CACHE/css/799f6defe43c.css")
+ self.assertEqual(out, render(template, self.context))
+
+ def test_js_tag(self):
+ template = """{% load compress %}{% compress js %}
+ <script src="{{ STATIC_URL }}js/one.js" type="text/javascript"></script>
+ <script type="text/javascript">obj.value = "value";</script>
+ {% endcompress %}
+ """
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ self.assertEqual(out, render(template, self.context))
+
+ def test_nonascii_js_tag(self):
+ template = """{% load compress %}{% compress js %}
+ <script src="{{ STATIC_URL }}js/nonasc.js" type="text/javascript"></script>
+ <script type="text/javascript">var test_value = "\u2014";</script>
+ {% endcompress %}
+ """
+ out = '<script type="text/javascript" src="/static/CACHE/js/e214fe629b28.js"></script>'
+ self.assertEqual(out, render(template, self.context))
+
+ def test_nonascii_latin1_js_tag(self):
+ template = """{% load compress %}{% compress js %}
+ <script src="{{ STATIC_URL }}js/nonasc-latin1.js" type="text/javascript" charset="latin-1"></script>
+ <script type="text/javascript">var test_value = "\u2014";</script>
+ {% endcompress %}
+ """
+ out = '<script type="text/javascript" src="/static/CACHE/js/be9e078b5ca7.js"></script>'
+ self.assertEqual(out, render(template, self.context))
+
+ def test_compress_tag_with_illegal_arguments(self):
+ template = """{% load compress %}{% compress pony %}
+ <script type="pony/application">unicorn</script>
+ {% endcompress %}"""
+ self.assertRaises(TemplateSyntaxError, render, template, {})
+
+ @override_settings(COMPRESS_DEBUG_TOGGLE='togglecompress')
+ def test_debug_toggle(self):
+ template = """{% load compress %}{% compress js %}
+ <script src="{{ STATIC_URL }}js/one.js" type="text/javascript"></script>
+ <script type="text/javascript">obj.value = "value";</script>
+ {% endcompress %}
+ """
+
+ class MockDebugRequest(object):
+ GET = {settings.COMPRESS_DEBUG_TOGGLE: 'true'}
+
+ context = dict(self.context, request=MockDebugRequest())
+ out = """<script src="/static/js/one.js" type="text/javascript"></script>
+ <script type="text/javascript">obj.value = "value";</script>"""
+ self.assertEqual(out, render(template, context))
+
+ def test_named_compress_tag(self):
+ template = """{% load compress %}{% compress js inline foo %}
+ <script type="text/javascript">obj.value = "value";</script>
+ {% endcompress %}
+ """
+
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ render(template)
+ args, kwargs = callback.call_args
+ context = kwargs['context']
+ self.assertEqual('foo', context['compressed']['name'])
+
+
+class PrecompilerTemplatetagTestCase(TestCase):
+ def setUp(self):
+ self.old_enabled = settings.COMPRESS_ENABLED
+ self.old_precompilers = settings.COMPRESS_PRECOMPILERS
+
+ precompiler = os.path.join(test_dir, 'precompiler.py')
+ python = sys.executable
+
+ settings.COMPRESS_ENABLED = True
+ settings.COMPRESS_PRECOMPILERS = (
+ ('text/coffeescript', '%s %s' % (python, precompiler)),
+ ('text/less', '%s %s' % (python, precompiler)),
+ )
+ self.context = {'STATIC_URL': settings.COMPRESS_URL}
+
+ def tearDown(self):
+ settings.COMPRESS_ENABLED = self.old_enabled
+ settings.COMPRESS_PRECOMPILERS = self.old_precompilers
+
+ def test_compress_coffeescript_tag(self):
+ template = """{% load compress %}{% compress js %}
+ <script type="text/coffeescript"># this is a comment.</script>
+ {% endcompress %}"""
+ out = script(src="/static/CACHE/js/e920d58f166d.js")
+ self.assertEqual(out, render(template, self.context))
+
+ def test_compress_coffeescript_tag_and_javascript_tag(self):
+ template = """{% load compress %}{% compress js %}
+ <script type="text/coffeescript"># this is a comment.</script>
+ <script type="text/javascript"># this too is a comment.</script>
+ {% endcompress %}"""
+ out = script(src="/static/CACHE/js/ef6b32a54575.js")
+ self.assertEqual(out, render(template, self.context))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_coffeescript_and_js_tag_with_compress_enabled_equals_false(self):
+ template = """{% load compress %}{% compress js %}
+ <script type="text/coffeescript"># this is a comment.</script>
+ <script type="text/javascript"># this too is a comment.</script>
+ {% endcompress %}"""
+ out = (script('# this is a comment.\n') + '\n' +
+ script('# this too is a comment.'))
+ self.assertEqual(out, render(template, self.context))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_compress_coffeescript_tag_compress_enabled_is_false(self):
+ template = """{% load compress %}{% compress js %}
+ <script type="text/coffeescript"># this is a comment.</script>
+ {% endcompress %}"""
+ out = script("# this is a comment.\n")
+ self.assertEqual(out, render(template, self.context))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_compress_coffeescript_file_tag_compress_enabled_is_false(self):
+ template = """
+ {% load compress %}{% compress js %}
+ <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.coffee">
+ </script>
+ {% endcompress %}"""
+
+ out = script(src="/static/CACHE/js/one.95cfb869eead.js")
+ self.assertEqual(out, render(template, self.context))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_multiple_file_order_conserved(self):
+ template = """
+ {% load compress %}{% compress js %}
+ <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.coffee">
+ </script>
+ <script src="{{ STATIC_URL }}js/one.js"></script>
+ <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.js">
+ </script>
+ {% endcompress %}"""
+
+ out = '\n'.join([script(src="/static/CACHE/js/one.95cfb869eead.js"),
+ script(scripttype="", src="/static/js/one.js"),
+ script(src="/static/CACHE/js/one.81a2cd965815.js")])
+
+ self.assertEqual(out, render(template, self.context))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_css_multiple_files_disabled_compression(self):
+ assert(settings.COMPRESS_PRECOMPILERS)
+ template = """
+ {% load compress %}{% compress css %}
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/one.css"></link>
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/two.css"></link>
+ {% endcompress %}"""
+
+ out = ''.join(['<link rel="stylesheet" type="text/css" href="/static/css/one.css" />',
+ '<link rel="stylesheet" type="text/css" href="/static/css/two.css" />'])
+
+ self.assertEqual(out, render(template, self.context))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_css_multiple_files_mixed_precompile_disabled_compression(self):
+ assert(settings.COMPRESS_PRECOMPILERS)
+ template = """
+ {% load compress %}{% compress css %}
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/one.css"/>
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/two.css"/>
+ <link rel="stylesheet" type="text/less" href="{{ STATIC_URL }}css/url/test.css"/>
+ {% endcompress %}"""
+
+ out = ''.join(['<link rel="stylesheet" type="text/css" href="/static/css/one.css" />',
+ '<link rel="stylesheet" type="text/css" href="/static/css/two.css" />',
+ '<link rel="stylesheet" href="/static/CACHE/css/test.5dddc6c2fb5a.css" type="text/css" />'])
+ self.assertEqual(out, render(template, self.context))
+
+
+def script(content="", src="", scripttype="text/javascript"):
+ """
+ returns a unicode text html script element.
+
+ >>> script('#this is a comment', scripttype="text/applescript")
+ '<script type="text/applescript">#this is a comment</script>'
+ """
+ out_script = '<script '
+ if scripttype:
+ out_script += 'type="%s" ' % scripttype
+ if src:
+ out_script += 'src="%s" ' % src
+ return out_script[:-1] + '>%s</script>' % content
--- /dev/null
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+import os
+
+from django.utils import six
+
+from compressor.exceptions import FilterError
+
+
+def get_class(class_string, exception=FilterError):
+ """
+ Convert a string version of a function name to the callable object.
+ """
+ if not hasattr(class_string, '__bases__'):
+ try:
+ class_string = str(class_string)
+ mod_name, class_name = get_mod_func(class_string)
+ if class_name:
+ return getattr(__import__(mod_name, {}, {}, [str('')]), class_name)
+ except (ImportError, AttributeError):
+ raise exception('Failed to import %s' % class_string)
+
+ raise exception("Invalid class path '%s'" % class_string)
+
+
+def get_mod_func(callback):
+ """
+ Converts 'django.views.news.stories.story_detail' to
+ ('django.views.news.stories', 'story_detail')
+ """
+ try:
+ dot = callback.rindex('.')
+ except ValueError:
+ return callback, ''
+ return callback[:dot], callback[dot + 1:]
+
+
+def get_pathext(default_pathext=None):
+ """
+ Returns the path extensions from environment or a default
+ """
+ if default_pathext is None:
+ default_pathext = os.pathsep.join(['.COM', '.EXE', '.BAT', '.CMD'])
+ return os.environ.get('PATHEXT', default_pathext)
+
+
+def find_command(cmd, paths=None, pathext=None):
+ """
+ Searches the PATH for the given command and returns its path
+ """
+ if paths is None:
+ paths = os.environ.get('PATH', '').split(os.pathsep)
+ if isinstance(paths, six.string_types):
+ paths = [paths]
+ # check if there are funny path extensions for executables, e.g. Windows
+ if pathext is None:
+ pathext = get_pathext()
+ pathext = [ext for ext in pathext.lower().split(os.pathsep)]
+ # don't use extensions if the command ends with one of them
+ if os.path.splitext(cmd)[1].lower() in pathext:
+ pathext = ['']
+ # check if we find the command on PATH
+ for path in paths:
+ # try without extension first
+ cmd_path = os.path.join(path, cmd)
+ for ext in pathext:
+ # then including the extension
+ cmd_path_ext = cmd_path + ext
+ if os.path.isfile(cmd_path_ext):
+ return cmd_path_ext
+ if os.path.isfile(cmd_path):
+ return cmd_path
+ return None
--- /dev/null
+class cached_property(object):
+ """Property descriptor that caches the return value
+ of the get function.
+
+ *Examples*
+
+ .. code-block:: python
+
+ @cached_property
+ def connection(self):
+ return Connection()
+
+ @connection.setter # Prepares stored value
+ def connection(self, value):
+ if value is None:
+ raise TypeError("Connection must be a connection")
+ return value
+
+ @connection.deleter
+ def connection(self, value):
+ # Additional action to do at del(self.attr)
+ if value is not None:
+ print("Connection %r deleted" % (value, ))
+ """
+ def __init__(self, fget=None, fset=None, fdel=None, doc=None):
+ self.__get = fget
+ self.__set = fset
+ self.__del = fdel
+ self.__doc__ = doc or fget.__doc__
+ self.__name__ = fget.__name__
+ self.__module__ = fget.__module__
+
+ def __get__(self, obj, type=None):
+ if obj is None:
+ return self
+ try:
+ return obj.__dict__[self.__name__]
+ except KeyError:
+ value = obj.__dict__[self.__name__] = self.__get(obj)
+ return value
+
+ def __set__(self, obj, value):
+ if obj is None:
+ return self
+ if self.__set is not None:
+ value = self.__set(obj, value)
+ obj.__dict__[self.__name__] = value
+
+ def __delete__(self, obj):
+ if obj is None:
+ return self
+ try:
+ value = obj.__dict__.pop(self.__name__)
+ except KeyError:
+ pass
+ else:
+ if self.__del is not None:
+ self.__del(obj, value)
+
+ def setter(self, fset):
+ return self.__class__(self.__get, fset, self.__del)
+
+ def deleter(self, fdel):
+ return self.__class__(self.__get, self.__set, fdel)
--- /dev/null
+from __future__ import absolute_import, unicode_literals
+
+from django.core.exceptions import ImproperlyConfigured
+
+from compressor.conf import settings
+
+INSTALLED = ("staticfiles" in settings.INSTALLED_APPS or
+ "django.contrib.staticfiles" in settings.INSTALLED_APPS)
+
+if INSTALLED:
+ if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
+ from django.contrib.staticfiles import finders
+ else:
+ try:
+ from staticfiles import finders # noqa
+ except ImportError:
+ # Old (pre 1.0) and incompatible version of staticfiles
+ INSTALLED = False
+
+ if (INSTALLED and "compressor.finders.CompressorFinder"
+ not in settings.STATICFILES_FINDERS):
+ raise ImproperlyConfigured(
+ "When using Django Compressor together with staticfiles, "
+ "please add 'compressor.finders.CompressorFinder' to the "
+ "STATICFILES_FINDERS setting.")
+else:
+ finders = None # noqa
--- /dev/null
+# -*- 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()
--- /dev/null
+Metadata-Version: 1.1
+Name: django-compressor
+Version: 1.4
+Summary: Compresses linked and inline JavaScript or CSS into single cached files.
+Home-page: http://django-compressor.readthedocs.org/en/latest/
+Author: Jannis Leidel
+Author-email: jannis@leidel.info
+License: MIT
+Description: Django Compressor
+ =================
+
+ .. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop
+ :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop
+
+ .. image:: https://pypip.in/v/django_compressor/badge.png
+ :target: https://pypi.python.org/pypi/django_compressor
+
+ .. image:: https://pypip.in/d/django_compressor/badge.png
+ :target: https://pypi.python.org/pypi/django_compressor
+
+ .. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop
+ :alt: Build Status
+ :target: http://travis-ci.org/django-compressor/django-compressor
+
+ Django Compressor combines and compresses linked and inline Javascript
+ or CSS in a Django template into cacheable static files by using the
+ ``compress`` template tag.
+
+ HTML in between ``{% compress js/css %}`` and ``{% endcompress %}`` is
+ parsed and searched for CSS or JS. These styles and scripts are subsequently
+ processed with optional, configurable compilers and filters.
+
+ The default filter for CSS rewrites paths to static files to be absolute
+ and adds a cache busting timestamp. For Javascript the default filter
+ compresses it using ``jsmin``.
+
+ As the final result the template tag outputs a ``<script>`` or ``<link>``
+ tag pointing to the optimized file. These files are stored inside a folder
+ and given a unique name based on their content. Alternatively it can also
+ return the resulting content to the original template directly.
+
+ Since the file name is dependent on the content these files can be given
+ a far future expiration date without worrying about stale browser caches.
+
+ The concatenation and compressing process can also be jump started outside
+ of the request/response cycle by using the Django management command
+ ``manage.py compress``.
+
+ Configurability & Extendibility
+ -------------------------------
+
+ Django Compressor is highly configurable and extendible. The HTML parsing
+ is done using lxml_ or if it's not available Python's built-in HTMLParser by
+ 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`_,
+ `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
+ `data URIs`_.
+
+ If your setup requires a different compressor or other post-processing
+ tool it will be fairly easy to implement a custom filter. Simply extend
+ from one of the available base classes.
+
+ More documentation about the usage and settings of Django Compressor can be
+ found on `django-compressor.readthedocs.org`_.
+
+ The source code for Django Compressor can be found and contributed to on
+ `github.com/django-compressor/django-compressor`_. There you can also file tickets.
+
+ The in-development version of Django Compressor can be installed with
+ ``pip install http://github.com/django-compressor/django-compressor/tarball/develop``.
+
+ .. _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
+ .. _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
+
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Framework :: Django
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Topic :: Internet :: WWW/HTTP
--- /dev/null
+AUTHORS
+LICENSE
+MANIFEST.in
+Makefile
+README.rst
+setup.cfg
+setup.py
+tox.ini
+compressor/__init__.py
+compressor/base.py
+compressor/cache.py
+compressor/conf.py
+compressor/css.py
+compressor/exceptions.py
+compressor/finders.py
+compressor/js.py
+compressor/models.py
+compressor/signals.py
+compressor/storage.py
+compressor/test_settings.py
+compressor/contrib/__init__.py
+compressor/contrib/jinja2ext.py
+compressor/contrib/sekizai.py
+compressor/filters/__init__.py
+compressor/filters/base.py
+compressor/filters/closure.py
+compressor/filters/css_default.py
+compressor/filters/csstidy.py
+compressor/filters/datauri.py
+compressor/filters/template.py
+compressor/filters/yuglify.py
+compressor/filters/yui.py
+compressor/filters/cssmin/__init__.py
+compressor/filters/cssmin/cssmin.py
+compressor/filters/cssmin/rcssmin.py
+compressor/filters/jsmin/__init__.py
+compressor/filters/jsmin/rjsmin.py
+compressor/filters/jsmin/slimit.py
+compressor/management/__init__.py
+compressor/management/commands/__init__.py
+compressor/management/commands/compress.py
+compressor/management/commands/mtime_cache.py
+compressor/offline/__init__.py
+compressor/offline/django.py
+compressor/offline/jinja2.py
+compressor/parser/__init__.py
+compressor/parser/base.py
+compressor/parser/beautifulsoup.py
+compressor/parser/default_htmlparser.py
+compressor/parser/html5lib.py
+compressor/parser/lxml.py
+compressor/templates/compressor/css_file.html
+compressor/templates/compressor/css_inline.html
+compressor/templates/compressor/js_file.html
+compressor/templates/compressor/js_inline.html
+compressor/templatetags/__init__.py
+compressor/templatetags/compress.py
+compressor/tests/__init__.py
+compressor/tests/precompiler.py
+compressor/tests/test_base.py
+compressor/tests/test_filters.py
+compressor/tests/test_jinja2ext.py
+compressor/tests/test_offline.py
+compressor/tests/test_parsers.py
+compressor/tests/test_signals.py
+compressor/tests/test_storages.py
+compressor/tests/test_templatetags.py
+compressor/tests/static/css/datauri.css
+compressor/tests/static/css/nonasc.css
+compressor/tests/static/css/one.css
+compressor/tests/static/css/two.css
+compressor/tests/static/css/url/nonasc.css
+compressor/tests/static/css/url/test.css
+compressor/tests/static/css/url/url1.css
+compressor/tests/static/css/url/2/url2.css
+compressor/tests/static/img/add.png
+compressor/tests/static/img/python.png
+compressor/tests/static/js/nonasc-latin1.js
+compressor/tests/static/js/nonasc.js
+compressor/tests/static/js/one.coffee
+compressor/tests/static/js/one.js
+compressor/tests/test_templates/basic/test_compressor_offline.html
+compressor/tests/test_templates/test_block_super/base.html
+compressor/tests/test_templates/test_block_super/test_compressor_offline.html
+compressor/tests/test_templates/test_block_super_base_compressed/base.html
+compressor/tests/test_templates/test_block_super_base_compressed/base2.html
+compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html
+compressor/tests/test_templates/test_block_super_extra/base.html
+compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html
+compressor/tests/test_templates/test_block_super_multiple/base.html
+compressor/tests/test_templates/test_block_super_multiple/base2.html
+compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html
+compressor/tests/test_templates/test_block_super_multiple_cached/base.html
+compressor/tests/test_templates/test_block_super_multiple_cached/base2.html
+compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html
+compressor/tests/test_templates/test_complex/test_compressor_offline.html
+compressor/tests/test_templates/test_condition/test_compressor_offline.html
+compressor/tests/test_templates/test_duplicate/test_compressor_offline.html
+compressor/tests/test_templates/test_error_handling/buggy_extends.html
+compressor/tests/test_templates/test_error_handling/buggy_template.html
+compressor/tests/test_templates/test_error_handling/missing_extends.html
+compressor/tests/test_templates/test_error_handling/test_compressor_offline.html
+compressor/tests/test_templates/test_error_handling/with_coffeescript.html
+compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html
+compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html
+compressor/tests/test_templates/test_templatetag/test_compressor_offline.html
+compressor/tests/test_templates/test_with_context/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_block_super/base.html
+compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_block_super_extra/base.html
+compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html
+compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html
+compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html
+compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html
+compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html
+compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html
+compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html
+compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html
+compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html
+compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html
+compressor/utils/__init__.py
+compressor/utils/decorators.py
+compressor/utils/staticfiles.py
+compressor/utils/stringformat.py
+django_compressor.egg-info/PKG-INFO
+django_compressor.egg-info/SOURCES.txt
+django_compressor.egg-info/dependency_links.txt
+django_compressor.egg-info/not-zip-safe
+django_compressor.egg-info/requires.txt
+django_compressor.egg-info/top_level.txt
+docs/Makefile
+docs/behind-the-scenes.txt
+docs/changelog.txt
+docs/conf.py
+docs/contributing.txt
+docs/django-sekizai.txt
+docs/index.txt
+docs/jinja2.txt
+docs/make.bat
+docs/quickstart.txt
+docs/remote-storages.txt
+docs/scenarios.txt
+docs/settings.txt
+docs/usage.txt
+requirements/tests.txt
\ No newline at end of file
--- /dev/null
+django-appconf >= 0.4
\ No newline at end of file
--- /dev/null
+compressor
--- /dev/null
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-compressor.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-compressor.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/django-compressor"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-compressor"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
--- /dev/null
+.. _behind_the_scenes:
+
+Behind the scenes
+=================
+
+This document assumes you already have an up and running instance of
+Django Compressor, and that you understand how to use it in your templates.
+The goal is to explain what the main template tag, {% compress %}, does
+behind the scenes, to help you debug performance problems for instance.
+
+Offline cache
+-------------
+
+If offline cache is activated, the first thing {% compress %} tries to do is
+retrieve the compressed version for its nodelist from the offline manifest
+cache. It doesn't parse, doesn't check the modified times of the files, doesn't
+even know which files are concerned actually, since it doesn't look inside the
+nodelist of the template block enclosed by the ``compress`` template tag.
+The offline cache manifest is just a json file, stored on disk inside the
+directory that holds the compressed files. The format of the manifest is simply
+a key <=> value dictionary, with the hash of the nodelist being the key,
+and the HTML containing the element code for the combined file or piece of code
+being the value. Generating the offline manifest, using the ``compress``
+management command, also generates the combined files referenced in the manifest.
+
+If offline cache is activated and the nodelist hash can not be found inside the
+manifest, {% compress %} will raise an ``OfflineGenerationError``.
+
+If offline cache is de-activated, the following happens:
+
+First step: parsing and file list
+---------------------------------
+
+A compressor instance is created, which in turns instantiates the HTML parser.
+The parser is used to determine a file or code hunk list. Each file mtime is
+checked, first in cache and then on disk/storage, and this is used to
+determine an unique cache key.
+
+Second step: Checking the "main" cache
+--------------------------------------
+
+Compressor checks if it can get some info about the combined file/hunks
+corresponding to its instance, using the cache key obtained in the previous
+step. The cache content here will actually be the HTML containing the final
+element code, just like in the offline step before.
+
+Everything stops here if the cache entry exists.
+
+Third step: Generating the combined file if needed
+--------------------------------------------------
+
+The file is generated if necessary. All precompilers are called and all
+filters are executed, and a hash is determined from the contents. This in
+turns helps determine the file name, which is only saved if it didn't exist
+already. Then the HTML output is returned (and also saved in the cache).
+And that's it!
--- /dev/null
+Changelog
+=========
+
+v1.4
+----
+
+- Added Python 3 compatibility.
+
+- Added compatibility with Django 1.6.x.
+
+- Fixed compatibility with html5lib 1.0.
+
+- Added offline compression for Jinja2 with Jingo and Coffin integration.
+
+- Improved support for template inheritance in offline compression.
+
+- Made offline compression avoid compressing the same block multiple times.
+
+- Added a ``testenv`` target in the Makefile to make it easier to set up the
+ test environment.
+
+- Allowed data-uri filter to handle external/protocol-relative references.
+
+- Made ``CssCompressor`` class easier to extend.
+
+- Added support for explictly stating the block being ended.
+
+- Added rcssmin and updated rjsmin.
+
+- Removed implicit requirement on BeautifulSoup.
+
+- Made GzipCompressorFileStorage set access and modified times to the same time
+ as the corresponding base file.
+
+- Defaulted to using django's simplejson, if present.
+
+- Fixed CompilerFilter to always output Unicode strings.
+
+- Fixed windows line endings in offline compression.
+
+v1.3 (03/18/2013)
+-----------------
+
+- *Backward incompatible changes*
+
+ - Dropped support for Python 2.5. Removed ``any`` and ``walk`` compatibility
+ functions in ``compressor.utils``.
+
+ - Removed compatibility with Django 1.2 for default values of some settings:
+
+ - :attr:`~COMPRESS_ROOT` no longer uses ``MEDIA_ROOT`` if ``STATIC_ROOT`` is
+ not defined. It expects ``STATIC_ROOT`` to be defined instead.
+
+ - :attr:`~COMPRESS_URL` no longer uses ``MEDIA_URL`` if ``STATIC_URL`` is
+ not defined. It expects ``STATIC_URL`` to be defined instead.
+
+ - :attr:`~COMPRESS_CACHE_BACKEND` no longer uses ``CACHE_BACKEND`` and simply
+ defaults to ``default``.
+
+- Added precompiler class support. This enables you to write custom precompilers
+ with Python logic in them instead of just relying on executables.
+
+- Made CssAbsoluteFilter smarter: it now handles URLs with hash fragments or
+ querystring correctly. In addition, it now leaves alone fragment-only URLs.
+
+- Removed a ``fsync()`` call in ``CompilerFilter`` to improve performance.
+ We already called ``self.infile.flush()`` so that call was not necessary.
+
+- Added an extension to provide django-sekizai support.
+ See :ref:`django-sekizai Support <django-sekizai_support>` for more
+ information.
+
+- Fixed a ``DeprecationWarning`` regarding the use of ``django.utils.hashcompat``
+
+- Updated bundled ``rjsmin.py`` to fix some JavaScript compression errors.
+
+v1.2
+----
+
+- Added compatibility with Django 1.4 and dropped support for Django 1.2.X.
+
+- Added contributing docs. Be sure to check them out and start contributing!
+
+- Moved CI to Travis: http://travis-ci.org/django-compressor/django-compressor
+
+- Introduced a new ``compressed`` context dictionary that is passed to
+ the templates that are responsible for rendering the compressed snippets.
+
+ This is a **backwards-incompatible change** if you've overridden any of
+ the included templates:
+
+ - ``compressor/css_file.html``
+ - ``compressor/css_inline.html``
+ - ``compressor/js_file.html``
+ - ``compressor/js_inline.html``
+
+ The variables passed to those templates have been namespaced in a
+ dictionary, so it's easy to fix your own templates.
+
+ For example, the old ``compressor/js_file.html``::
+
+ <script type="text/javascript" src="{{ url }}"></script>
+
+ The new ``compressor/js_file.html``::
+
+ <script type="text/javascript" src="{{ compressed.url }}"></script>
+
+- Removed old templates named ``compressor/css.html`` and
+ ``compressor/js.html`` that were originally left for backwards
+ compatibility. If you've overridden them, just rename them to
+ ``compressor/css_file.html`` or ``compressor/js_file.html`` and
+ make sure you've accounted for the backwards incompatible change
+ of the template context mentioned above.
+
+- Reverted an unfortunate change to the YUI filter that prepended
+ ``'java -jar'`` to the binary name, which doesn't alway work, e.g.
+ if the YUI compressor is shipped as a script like
+ ``/usr/bin/yui-compressor``.
+
+- Changed the sender parameter of the :func:`~compressor.signals.post_compress`
+ signal to be either :class:`compressor.css.CssCompressor` or
+ :class:`compressor.js.JsCompressor` for easier customization.
+
+- Correctly handle offline compressing files that are found in ``{% if %}``
+ template blocks.
+
+- Renamed the second option for the ``COMPRESS_CSS_HASHING_METHOD`` setting
+ from ``'hash'`` to ``'content'`` to better describe what it does. The old
+ name is also supported, as well as the default being ``'mtime'``.
+
+- Fixed CssAbsoluteFilter, ``src`` attributes in includes now get transformed.
+
+- Added a new hook to allow developers to completely bypass offline
+ compression in CompressorNode subclasses: ``is_offline_compression_enabled``.
+
+- Dropped versiontools from required dependencies again.
+
+v1.1.2
+------
+
+- Fixed an installation issue related to versiontools.
+
+v1.1.1
+------
+
+- Fixed a stupid ImportError bug introduced in 1.1.
+
+- Fixed Jinja2 docs of since ``JINJA2_EXTENSIONS`` expects
+ a class, not a module.
+
+- Fixed a Windows bug with regard to file resolving with
+ staticfiles finders.
+
+- Stopped a potential memory leak when memoizing the rendered
+ output.
+
+- Fixed the integration between staticfiles (e.g. in Django <= 1.3.1)
+ and compressor which prevents the collectstatic management command
+ to work.
+
+ .. warning::
+
+ Make sure to **remove** the ``path`` method of your custom
+ :ref:`remote storage <remote_storages>` class!
+
+v1.1
+----
+
+- Made offline compression completely independent from cache (by writing a
+ manifest.json file).
+
+ You can now easily run the :ref:`compress <pre-compression>` management
+ command locally and transfer the :attr:`~django.conf.settings.COMPRESS_ROOT`
+ dir to your server.
+
+- Updated installation instructions to properly mention all dependencies,
+ even those internally used.
+
+- Fixed a bug introduced in 1.0 which would prevent the proper deactivation
+ of the compression in production.
+
+- Added a Jinja2_ :doc:`contrib extension </jinja2>`.
+
+- Made sure the rel attribute of link tags can be mixed case.
+
+- Avoid overwriting context variables needed for compressor to work.
+
+- Stopped the compress management command to require model validation.
+
+- Added missing imports and fixed a few :pep:`8` issues.
+
+.. _Jinja2: http://jinja.pocoo.org/2/
+
+v1.0.1
+------
+
+- Fixed regression in ``compressor.utils.staticfiles`` compatibility
+ module.
+
+v1.0
+----
+
+- **BACKWARDS-INCOMPATIBLE** Stopped swallowing exceptions raised by
+ rendering the template tag in production (``DEBUG = False``). This
+ has the potential to breaking lots of apps but on the other hand
+ will help find bugs.
+
+- **BACKWARDS-INCOMPATIBLE** The default function to create the cache
+ key stopped containing the server hostname. Instead the cache key
+ now only has the form ``'django_compressor.<KEY>'``.
+
+ To revert to the previous way simply set the ``COMPRESS_CACHE_KEY_FUNCTION``
+ to ``'compressor.cache.socket_cachekey'``.
+
+- **BACKWARDS-INCOMPATIBLE** Renamed ambigously named
+ ``COMPRESS_DATA_URI_MAX_SIZE`` setting to ``COMPRESS_DATA_URI_MAX_SIZE``.
+ It's the maximum size the ``compressor.filters.datauri.DataUriFilter``
+ filter will embed files as data: URIs.
+
+- Added ``COMPRESS_CSS_HASHING_METHOD`` setting with the options ``'mtime'``
+ (default) and ``'hash'`` for the ``CssAbsoluteFilter`` filter. The latter
+ uses the content of the file to calculate the cache-busting hash.
+
+- Added support for ``{{ block.super }}`` to ``compress`` management command.
+
+- Dropped Django 1.1.X support.
+
+- Fixed compiler filters on Windows.
+
+- Handle new-style cached template loaders in the compress management command.
+
+- Documented included filters.
+
+- Added `Slim It`_ filter.
+
+- Added new CallbackOutputFilter to ease the implementation of Python-based
+ callback filters that only need to pass the content to a callable.
+
+- Make use of `django-appconf`_ for settings handling and `versiontools`_
+ for versions.
+
+- Uses the current context when rendering the render templates.
+
+- Added :func:`post_compress<compressor.signals.post_compress>` signal.
+
+.. _`Slim It`: http://slimit.org/
+.. _`django-appconf`: http://django-appconf.rtfd.org/
+.. _`versiontools`: http://pypi.python.org/pypi/versiontools
+
+v0.9.2
+------
+
+- Fixed stdin handling of precompiler filter.
+
+v0.9.1
+------
+
+- Fixed encoding related issue.
+
+- Minor cleanups.
+
+v0.9
+----
+
+- Fixed the precompiler support to also use the full file path instead of a
+ temporarily created file.
+
+- Enabled test coverage.
+
+- Refactored caching and other utility code.
+
+- Switched from SHA1 to MD5 for hash generation to lower the computational impact.
+
+v0.8
+----
+
+- Replace naive jsmin.py with rJSmin (http://opensource.perlig.de/rjsmin/)
+ and fixed a few problems with JavaScript comments.
+
+- Fixed converting relative URLs in CSS files when running in debug mode.
+
+.. note::
+
+ If you relied on the ``split_contents`` method of ``Compressor`` classes,
+ please make sure a fourth item is returned in the iterable that denotes
+ the base name of the file that is compressed.
+
+v0.7.1
+------
+
+- Fixed import error when using the standalone django-staticfiles app.
+
+v0.7
+----
+
+- Created new parser, HtmlParser, based on the stdlib HTMLParser module.
+
+- Added a new default AutoSelectParser, which picks the LxmlParser if lxml
+ is available and falls back to HtmlParser.
+
+- Use unittest2 for testing goodness.
+
+- Fixed YUI JavaScript filter argument handling.
+
+- Updated bundled jsmin to use version by Dave St.Germain that was refactored for speed.
+
+v0.6.4
+------
+
+- Fixed Closure filter argument handling.
+
+v0.6.3
+------
+
+- Fixed options mangling in CompilerFilter initialization.
+
+- Fixed tox configuration.
+
+- Extended documentation and README.
+
+- In the compress command ignore hidden files when looking for templates.
+
+- Restructured utilities and added staticfiles compat layer.
+
+- Restructered parsers and added a html5lib based parser.
+
+v0.6.2
+------
+
+- Minor bugfixes that caused the compression not working reliably in
+ development mode (e.g. updated files didn't trigger a new compression).
+
+v0.6.1
+------
+
+- Fixed staticfiles support to also use its finder API to find files during
+ developement -- when the static files haven't been collected in
+ ``STATIC_ROOT``.
+
+- Fixed regression with the ``COMPRESS`` setting, pre-compilation and
+ staticfiles.
+
+v0.6
+----
+
+Major improvements and a lot of bugfixes, some of which are:
+
+- New precompilation support, which allows compilation of files and
+ hunks with easily configurable compilers before calling the actual
+ output filters. See the
+ :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` for more details.
+
+- New staticfiles support. With the introduction of the staticfiles app
+ to Django 1.3, compressor officially supports finding the files to
+ compress using the app's finder API. Have a look at the documentation
+ about :ref:`remote storages <remote_storages>` in case you want to use
+ those together with compressor.
+
+- New ``compress`` management command which allows pre-running of what the
+ compress template tag does. See the
+ :ref:`pre-compression <pre-compression>` docs for more information.
+
+- Various perfomance improvements by better caching and mtime cheking.
+
+- Deprecated ``COMPRESS_LESSC_BINARY`` setting because it's now
+ superseded by the :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS`
+ setting. Just make sure to use the correct mimetype when linking to less
+ files or adding inline code and add the following to your settings::
+
+ COMPRESS_PRECOMPILERS = (
+ ('text/less', 'lessc {infile} {outfile}'),
+ )
+
+- Added cssmin_ filter (``compressor.filters.CSSMinFilter``) based on
+ Zachary Voase's Python port of the YUI CSS compression algorithm.
+
+- Reimplemented the dog-piling prevention.
+
+- Make sure the CssAbsoluteFilter works for relative paths.
+
+- Added inline render mode. See :ref:`usage <usage>` docs.
+
+- Added ``mtime_cache`` management command to add and/or remove all mtimes
+ from the cache.
+
+- Moved docs to Read The Docs: http://django-compressor.readthedocs.org/en/latest/
+
+- Added optional ``compressor.storage.GzipCompressorFileStorage`` storage
+ backend that gzips of the saved files automatically for easier deployment.
+
+- Reimplemented a few filters on top of the new
+ ``compressor.filters.base.CompilerFilter`` to be a bit more DRY.
+
+- Added tox based test configuration, testing on Django 1.1-1.3 and Python
+ 2.5-2.7.
+
+.. _cssmin: http://pypi.python.org/pypi/cssmin/
+
--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# django-compressor documentation build configuration file, created by
+# sphinx-quickstart on Fri Jan 21 11:47:42 2011.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('..'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+# extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.txt'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Django Compressor'
+copyright = u'2014, Django Compressor authors'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+try:
+ from compressor import __version__
+ # The short X.Y version.
+ version = '.'.join(__version__.split('.')[:2])
+ # The full version, including alpha/beta/rc tags.
+ release = __version__
+except ImportError:
+ version = release = 'dev'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'murphy'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+# html_theme = 'default'
+RTD_NEW_THEME = True
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = ['_theme']
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'django-compressordoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'django-compressor.tex', u'Django Compressor Documentation',
+ u'Django Compressor authors', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'django-compressor', u'Django Compressor Documentation',
+ [u'Django Compressor authors'], 1)
+]
--- /dev/null
+Contributing
+============
+
+Like every open-source project, Django Compressor is always looking for
+motivated individuals to contribute to it's source code.
+However, to ensure the highest code quality and keep the repository nice and
+tidy, everybody has to follow a few rules (nothing major, I promise :) )
+
+Community
+---------
+
+People interested in developing for the Django Compressor should head
+over to #django-compressor on the `freenode`_ IRC network for help and to
+discuss the development.
+
+You may also be interested in following `@jezdez`_ on Twitter.
+
+In a nutshell
+-------------
+
+Here's what the contribution process looks like, in a bullet-points fashion,
+and only for the stuff we host on github:
+
+#. Django Compressor is hosted on `github`_, at https://github.com/django-compressor/django-compressor
+#. The best method to contribute back is to create a github account, then fork
+ the project. You can use this fork as if it was your own project, and should
+ push your changes to it.
+#. When you feel your code is good enough for inclusion, "send us a `pull
+ request`_", by using the nice github web interface.
+
+Contributing Code
+-----------------
+
+Getting the source code
+^^^^^^^^^^^^^^^^^^^^^^^
+
+If you're interested in developing a new feature for Compressor, it is
+recommended that you first discuss it on IRC not to do any work that will
+not get merged in anyway.
+
+- Code will be reviewed and tested by at least one core developer, preferably
+ by several. Other community members are welcome to give feedback.
+- Code *must* be tested. Your pull request should include unit-tests (that
+ cover the piece of code you're submitting, obviously)
+- Documentation should reflect your changes if relevant. There is nothing
+ worse than invalid documentation.
+- Usually, if unit tests are written, pass, and your change is relevant, then
+ it'll be merged.
+
+Since it's hosted on github, Django Compressor uses `git`_ as a version
+control system.
+
+The `github help`_ is very well written and will get you started on using git
+and github in a jiffy. It is an invaluable resource for newbies and old timers
+alike.
+
+Syntax and conventions
+^^^^^^^^^^^^^^^^^^^^^^
+
+We try to conform to `PEP8`_ as much as possible. A few highlights:
+
+- Indentation should be exactly 4 spaces. Not 2, not 6, not 8. **4**. Also,
+ tabs are evil.
+- We try (loosely) to keep the line length at 79 characters. Generally the
+ rule is "it should look good in a terminal-base editor" (eg vim), but we
+ try not be [Godwin's law] about it.
+
+Process
+^^^^^^^
+
+This is how you fix a bug or add a feature:
+
+#. `Fork`_ us on github.
+#. Checkout your fork.
+#. Hack hack hack, test test test, commit commit commit, test again.
+#. Push to your fork.
+#. Open a pull request.
+
+Tests
+^^^^^
+
+Having a wide and comprehensive library of unit-tests and integration tests is
+of exceeding importance. Contributing tests is widely regarded as a very
+prestigious contribution (you're making everybody's future work much easier by
+doing so). Good karma for you. Cookie points. Maybe even a beer if we meet in
+person :)
+
+Generally tests should be:
+
+- Unitary (as much as possible). I.E. should test as much as possible only one
+ function/method/class. That's the very definition of unit tests.
+
+- Integration tests are interesting too obviously, but require more time to
+ maintain since they have a higher probability of breaking.
+
+- Short running. No hard numbers here, but if your one test doubles the time
+ it takes for everybody to run them, it's probably an indication that you're
+ doing it wrong.
+
+In a similar way to code, pull requests will be reviewed before pulling
+(obviously), and we encourage discussion via code review (everybody learns
+something this way) or IRC discussions.
+
+Running the tests
+^^^^^^^^^^^^^^^^^
+
+To run the tests simply fork django_compressor, make the changes and open
+a pull request. The Travis_ bot will automatically run the tests of your
+branch/fork (see the `pull request announcment`_ for more info) and add a
+comment about the test results to the pull requests. Alternatively you
+can also login at Travis and enable your fork to run there, too. See the
+`Travis documentation`_ to read about how to do that.
+
+Alternatively, create a virtualenv and activate it, then install the
+requirements **in the virtualenv**::
+
+ $ virtualenv compressor_test
+ $ source compressor_test/bin/activate
+ (compressor_test) $ make testenv
+
+Then run ``make test`` to run the tests. Please note that this only tests
+django_compressor in the Python version you've created the virtualenv with
+not all the versions that are required to be supported.
+
+Contributing Documentation
+--------------------------
+
+Perhaps considered "boring" by hard-core coders, documentation is sometimes
+even more important than code! This is what brings fresh blood to a project,
+and serves as a reference for old timers. On top of this, documentation is
+the one area where less technical people can help most - you just need to
+write a semi-decent English. People need to understand you.
+
+Documentation should be:
+
+- We use `Sphinx`_/`restructuredText`_. So obviously this is the format you
+ should use :) File extensions should be ``.txt``.
+
+- Written in English. We can discuss how it would bring more people to the
+ project to have a Klingon translation or anything, but that's a problem we
+ will ask ourselves when we already have a good documentation in English.
+
+- Accessible. You should assume the reader to be moderately familiar with
+ Python and Django, but not anything else. Link to documentation of libraries
+ you use, for example, even if they are "obvious" to you. A brief
+ description of what it does is also welcome.
+
+Pulling of documentation is pretty fast and painless. Usually somebody goes
+over your text and merges it, since there are no "breaks" and that github
+parses rst files automagically it's really convenient to work with.
+
+Also, contributing to the documentation will earn you great respect from the
+core developers. You get good karma just like a test contributor, but you get
+double cookie points. Seriously. You rock.
+
+.. note::
+
+ This very document is based on the contributing docs of the
+ `django CMS`_ project. Many thanks for allowing us to steal it!
+
+.. _Fork: http://github.com/django-compressor/django-compressor
+.. _Travis: http://travis-ci.org/
+.. _`pull request announcment`: http://about.travis-ci.org/blog/announcing-pull-request-support/
+.. _`Travis documentation`: http://about.travis-ci.org/docs/
+.. _Sphinx: http://sphinx.pocoo.org/
+.. _PEP8: http://www.python.org/dev/peps/pep-0008/
+.. _github : http://www.github.com
+.. _github help : http://help.github.com
+.. _freenode : http://freenode.net/
+.. _@jezdez : https://twitter.com/jezdez
+.. _pull request : http://help.github.com/send-pull-requests/
+.. _git : http://git-scm.com/
+.. _restructuredText: http://docutils.sourceforge.net/docs/ref/rst/introduction.html
+.. _`django CMS`: http://www.django-cms.org/
--- /dev/null
+.. _django-sekizai_support:
+
+django-sekizai Support
+======================
+
+Django Compressor comes with support for _django-sekizai via an extension.
+_django-sekizai provides the ability to include template code, from within
+any block, to a parent block. It is primarily used to include js/css from
+included templates to the master template.
+
+It requires _django-sekizai to installed. Refer to the _django-sekizai _docs
+for how to use ``render_block``
+
+Usage
+-----
+
+.. code-block:: django
+
+ {% load sekizai_tags %}
+ {% render_block "<js/css>" postprocessor "compressor.contrib.sekizai.compress" %}
+
+
+.. _django-sekizai: https://github.com/ojii/django-sekizai
+.. _docs: http://django-sekizai.readthedocs.org/en/latest/
--- /dev/null
+=================
+Django Compressor
+=================
+
+Compresses linked and inline JavaScript or CSS into a single cached file.
+
+Why another static file combiner for Django?
+============================================
+
+Short version: None of them did exactly what I needed.
+
+Long version:
+
+**JS/CSS belong in the templates**
+ Every static combiner for Django I've seen makes you configure
+ your static files in your ``settings.py``. While that works, it doesn't make
+ sense. Static files are for display. And it's not even an option if your
+ settings are in completely different repositories and use different deploy
+ processes from the templates that depend on them.
+
+**Flexibility**
+ Django Compressor doesn't care if different pages use different combinations
+ of statics. It doesn't care if you use inline scripts or styles. It doesn't
+ get in the way.
+
+**Automatic regeneration and cache-foreverable generated output**
+ Statics are never stale and browsers can be told to cache the output forever.
+
+**Full test suite**
+ I has one.
+
+Contents
+========
+
+.. toctree::
+ :maxdepth: 2
+
+ quickstart
+ usage
+ scenarios
+ settings
+ remote-storages
+ behind-the-scenes
+ jinja2
+ django-sekizai
+ contributing
+ changelog
--- /dev/null
+Jinja2 In-Request Support
+=========================
+
+Django Compressor comes with support for Jinja2_ via an extension.
+
+Plain Jinja2
+------------
+
+In order to use Django Compressor's Jinja2 extension we would need to pass
+``compressor.contrib.jinja2ext.CompressorExtension`` into environment::
+
+ import jinja2
+ from compressor.contrib.jinja2ext import CompressorExtension
+
+ env = jinja2.Environment(extensions=[CompressorExtension])
+
+From now on, you can use same code you'd normally use within Django templates::
+
+ from django.conf import settings
+ template = env.from_string('\n'.join([
+ '{% compress css %}',
+ '<link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css" charset="utf-8">',
+ '{% endcompress %}',
+ ]))
+ template.render({'STATIC_URL': settings.STATIC_URL})
+
+For coffin users
+----------------
+
+Coffin_ makes it very easy to include additional Jinja2_ extensions as it
+only requires to add extension to ``JINJA2_EXTENSIONS`` at main settings
+module::
+
+ JINJA2_EXTENSIONS = [
+ 'compressor.contrib.jinja2ext.CompressorExtension',
+ ]
+
+And that's it - our extension is loaded and ready to be used.
+
+
+Jinja2 Offline Compression Support
+==================================
+You'd need to configure ``COMPRESS_JINJA2_GET_ENVIRONMENT`` so that
+Compressor can retrieve the Jinja2 environment for rendering.
+This can be a lamda or function that returns a Jinja2 environment.
+
+Usage
+-----
+Run the following compress command along with an ``-engine`` parameter. The
+parameter can be either jinja2 or django (default). For example,
+"./manage.py compress -engine jinja2".
+
+Using both Django and Jinja2 templates
+--------------------------------------
+There may be a chance that the Jinja2 parser is used to parse Django templates
+if you have a mixture of Django and Jinja2 templates in the same location(s).
+This should not be a problem since the Jinja2 parser will likely raise a
+template syntax error, causing Compressor to skip the errorneous
+template safely. (Vice versa for Django parser).
+
+A typical usage could be :
+
+- "./manage.py compress" for processing Django templates first, skipping
+ Jinja2 templates.
+- "./manage.py compress -engine jinja2" for processing Jinja2 templates,
+ skipping Django templates.
+
+However, it is still recommended that you do not mix Django and Jinja2
+templates in the same project.
+
+Limitations
+-----------
+- Does not support ``{% import %}`` and similar blocks within
+ ``{% compress %}`` blocks.
+- Does not support ``{{super()}}``.
+- All other filters, globals and language constructs such as
+ ``{% if %}``, ``{% with %}`` and ``{% for %}`` are tested and
+ should run fine.
+
+Jinja2 templates location
+-------------------------
+IMPORTANT: For Compressor to discover the templates for offline compression,
+there must be a template loader that implements the ``get_template_sources``
+method, and is in the ``TEMPLATE_LOADERS`` setting.
+
+If you're using Jinja2, you're likely to have a Jinja2 template loader in the
+``TEMPLATE_LOADERS`` setting, otherwise Django won't know how to load Jinja2
+templates. You could use Jingo_ or your own custom loader. Coffin_ works
+differently by providing a custom rendering method instead of a custom loader.
+
+Unfortunately, Jingo_ does not implement such a method in its loader;
+Coffin_ does not seem to have a template loader in the first place.
+Read on to understand how to make Compressor work nicely with Jingo_
+and Coffin_.
+
+By default, if you don't override the ``TEMPLATE_LOADERS`` setting,
+it will include the app directories loader that searches for templates under
+the ``templates`` directory in each app. If the app directories loader is in use
+and your Jinja2 templates are in the ``<app>/templates`` directories,
+Compressor will be able to find the Jinja2 templates.
+
+However, if you have Jinja2 templates in other location(s), you could include
+the filesystem loader (``django.template.loaders.filesystem.Loader``) in the
+``TEMPLATE_LOADERS`` setting and specify the custom location in the
+``TEMPLATE_DIRS`` setting.
+
+For Jingo users
+---------------
+You should configure ``TEMPLATE_LOADERS`` as such::
+
+ TEMPLATE_LOADERS = (
+ 'jingo.Loader',
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )
+
+ def COMPRESS_JINJA2_GET_ENVIRONMENT():
+ # TODO: ensure the CompressorExtension is installed with Jingo via
+ # Jingo's JINJA_CONFIG setting.
+ # Additional globals, filters, tests,
+ # and extensions used within {%compress%} blocks must be configured
+ # with Jingo.
+ from jingo import env
+
+ return env
+
+This will enable the Jingo_ loader to load Jinja2 templates and the other
+loaders to report the templates location(s).
+
+For Coffin users
+----------------
+You might want to configure ``TEMPLATE_LOADERS`` as such::
+
+ TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )
+
+ def COMPRESS_JINJA2_GET_ENVIRONMENT():
+ # TODO: ensure the CompressorExtension is installed with Coffin
+ # as described in the "In-Request Support" section above.
+ # Additional globals, filters, tests,
+ # and extensions used within {%compress%} blocks must be configured
+ # with Coffin.
+ from coffin.common import env
+
+ return env
+
+Again, if you have the Jinja2 templates in the app template directories, you're
+done here. Otherwise, specify the location in ``TEMPLATE_DIRS``.
+
+Using your custom loader
+------------------------
+You should configure ``TEMPLATE_LOADERS`` as such::
+
+ TEMPLATE_LOADERS = (
+ 'your_app.Loader',
+ ... other loaders (optional) ...
+ )
+
+You could implement the `get_template_sources` method in your loader or make
+use of the Django's builtin loaders to report the Jinja2 template location(s).
+
+Python 3 Support
+----------------
+Jingo with Jinja2 are tested and work on Python 2.6, 2.7, and 3.3.
+Coffin with Jinja2 are tested and work on Python 2.6 and 2.7 only.
+Jinja2 alone (with custom loader) are tested and work on Python 2.6, 2.7 and
+3.3 only.
+
+
+.. _Jinja2: http://jinja.pocoo.org/docs/
+.. _Coffin: http://pypi.python.org/pypi/Coffin
+.. _Jingo: https://jingo.readthedocs.org/en/latest/
+
--- /dev/null
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-compressor.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-compressor.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
--- /dev/null
+Quickstart
+==========
+
+Installation
+------------
+
+* Install Django Compressor with your favorite Python package manager::
+
+ pip install django_compressor
+
+* Add ``'compressor'`` to your ``INSTALLED_APPS`` setting::
+
+ INSTALLED_APPS = (
+ # other apps
+ "compressor",
+ )
+
+* See the list of :ref:`settings` to modify Django Compressor's
+ default behaviour and make adjustments for your website.
+
+* In case you use Django's staticfiles_ contrib app (or its standalone
+ counterpart django-staticfiles_) you have to add Django Compressor's file
+ finder to the ``STATICFILES_FINDERS`` setting, for example with
+ ``django.contrib.staticfiles``:
+
+ .. code-block:: python
+
+ STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ # other finders..
+ 'compressor.finders.CompressorFinder',
+ )
+
+* 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
+ folder.
+
+.. _staticfiles: http://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
+.. _django-staticfiles: http://pypi.python.org/pypi/django-staticfiles
+
+.. _dependencies:
+
+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>`
+ ``compressor.parser.BeautifulSoupParser`` and
+ ``compressor.parser.LxmlParser``::
+
+ pip install "BeautifulSoup<4.0"
+
+- lxml_
+
+ For the :attr:`parser <django.conf.settings.COMPRESS_PARSER>`
+ ``compressor.parser.LxmlParser``, also requires libxml2_::
+
+ STATIC_DEPS=true pip install lxml
+
+- html5lib_
+
+ For the :attr:`parser <django.conf.settings.COMPRESS_PARSER>`
+ ``compressor.parser.Html5LibParser``::
+
+ pip install html5lib
+
+- `Slim It`_
+
+ For the :ref:`Slim It filter <slimit_filter>`
+ ``compressor.filters.jsmin.SlimItFilter``::
+
+ pip install slimit
+
+.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
+.. _lxml: http://codespeak.net/lxml/
+.. _libxml2: http://xmlsoft.org/
+.. _html5lib: http://code.google.com/p/html5lib/
+.. _`Slim It`: http://slimit.org/
+.. _django-appconf: http://pypi.python.org/pypi/django-appconf/
+.. _versiontools: http://pypi.python.org/pypi/versiontools/
--- /dev/null
+.. _remote_storages:
+
+Remote storages
+---------------
+
+In some cases it's useful to use a CDN_ for serving static files such as
+those generated by Django Compressor. Due to the way Django Compressor
+processes files, it requires the files to be processed (in the
+``{% compress %}`` block) to be available in a local file system cache.
+
+Django Compressor provides hooks to automatically have compressed files
+pushed to a remote storage backend. Simply set the storage backend
+that saves the result to a remote service (see
+:attr:`~django.conf.settings.COMPRESS_STORAGE`).
+
+django-storages
+^^^^^^^^^^^^^^^
+
+So assuming your CDN is `Amazon S3`_, you can use the boto_ storage backend
+from the 3rd party app `django-storages`_. Some required settings are::
+
+ AWS_ACCESS_KEY_ID = 'XXXXXXXXXXXXXXXXXXXXX'
+ AWS_SECRET_ACCESS_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
+ AWS_STORAGE_BUCKET_NAME = 'compressor-test'
+
+Next, you need to specify the new CDN base URL and update the URLs to the
+files in your templates which you want to compress::
+
+ COMPRESS_URL = "http://compressor-test.s3.amazonaws.com/"
+
+.. note::
+
+ For staticfiles just set ``STATIC_URL = COMPRESS_URL``
+
+The storage backend to save the compressed files needs to be changed, too::
+
+ COMPRESS_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
+
+Using staticfiles
+^^^^^^^^^^^^^^^^^
+
+If you are using Django's staticfiles_ contrib app or the standalone
+app django-staticfiles_, you'll need to use a temporary filesystem cache
+for Django Compressor to know which files to compress. Since staticfiles
+provides a management command to collect static files from various
+locations which uses a storage backend, this is where both apps can be
+integrated.
+
+#. Make sure the :attr:`~django.conf.settings.COMPRESS_ROOT` and STATIC_ROOT_
+ settings are equal since both apps need to look at the same directories
+ when to do their job.
+
+#. You need to create a subclass of the remote storage backend you want
+ to use; below is an example of the boto S3 storage backend from
+ django-storages_::
+
+ from django.core.files.storage import get_storage_class
+ from storages.backends.s3boto import S3BotoStorage
+
+ class CachedS3BotoStorage(S3BotoStorage):
+ """
+ S3 storage backend that saves the files locally, too.
+ """
+ def __init__(self, *args, **kwargs):
+ super(CachedS3BotoStorage, self).__init__(*args, **kwargs)
+ self.local_storage = get_storage_class(
+ "compressor.storage.CompressorFileStorage")()
+
+ def save(self, name, content):
+ name = super(CachedS3BotoStorage, self).save(name, content)
+ self.local_storage._save(name, content)
+ return name
+
+#. Set your :attr:`~django.conf.settings.COMPRESS_STORAGE` and STATICFILES_STORAGE_
+ settings to the dotted path of your custom cached storage backend, e.g.
+ ``'mysite.storage.CachedS3BotoStorage'``.
+
+#. To have Django correctly render the URLs to your static files, set the
+ STATIC_URL_ setting to the same value as
+ :attr:`~django.conf.settings.COMPRESS_URL` (e.g.
+ ``"http://compressor-test.s3.amazonaws.com/"``).
+
+.. _CDN: http://en.wikipedia.org/wiki/Content_delivery_network
+.. _Amazon S3: https://s3.amazonaws.com/
+.. _boto: http://boto.cloudhackers.com/
+.. _django-storages: http://code.welldev.org/django-storages/
+.. _django-staticfiles: http://github.com/jezdez/django-staticfiles/
+.. _staticfiles: http://docs.djangoproject.com/en/dev/howto/static-files/
+.. _STATIC_ROOT: http://docs.djangoproject.com/en/dev/ref/settings/#static-root
+.. _STATIC_URL: http://docs.djangoproject.com/en/dev/ref/settings/#static-url
+.. _STATICFILES_STORAGE: http://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-storage
--- /dev/null
+.. _scenarios:
+
+Common Deployment Scenarios
+===========================
+
+This document presents the most typical scenarios in which Django Compressor
+can be configured, and should help you decide which method you may want to
+use for your stack.
+
+In-Request Compression
+----------------------
+
+This is the default method of compression. Where-in Django Compressor will
+go through the steps outlined in :ref:`behind_the_scenes`. You will find
+in-request compression beneficial if:
+
+* Using a single server setup, where the application and static files are on
+ the same machine.
+
+* Prefer a simple configuration. By default, there is no configuration
+ required.
+
+Caveats
+-------
+
+* If deploying to a multi-server setup and using
+ :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS`, each binary is
+ required to be installed on each application server.
+
+* Application servers may not have permissions to write to your static
+ directories. For example, if deploying to a CDN (e.g. Amazon S3)
+
+Offline Compression
+-------------------
+
+This method decouples the compression outside of the request
+(see :ref:`behind_the_Scenes`) and can prove beneficial in the speed,
+and in many scenarios, the maintainability of your deployment.
+You will find offline compression beneficial if:
+
+* Using a multi-server setup. A common scenario for this may be multiple
+ application servers and a single static file server (CDN included).
+ With offline compression, you typically run ``manage.py compress``
+ on a single utility server, meaning you only maintain
+ :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` binaries in one
+ location.
+
+* You store compressed files on a CDN.
+
+Caveats
+-------
+
+* If your templates have complex logic in how template inheritance is done
+ (e.g. ``{% extends context_variable %}``), then this becomes a problem,
+ as offline compression will not have the context, unless you set it in
+ :attr:`~django.conf.settings.COMPRESS_OFFLINE_CONTEXT`
+
+* Due to the way the manifest file is used, while deploying across a
+ multi-server setup, your application may use old templates with a new
+ manifest, possibly rendering your pages incoherent. The current suggested
+ solution for this is to change the
+ :attr:`~django.conf.settings.COMPRESS_OFFLINE_MANIFEST` path for each new
+ version of your code. This will ensure that the old code uses old
+ compressed output, and the new one appropriately as well.
+
+Every setup is unique, and your scenario may differ slightly. Choose what
+is the most sane to maintain for your situation.
--- /dev/null
+.. _settings:
+
+Settings
+========
+
+.. currentmodule:: django.conf.settings
+
+Django Compressor has a number of settings that control its behavior.
+They've been given sensible defaults.
+
+Base settings
+-------------
+
+.. attribute:: COMPRESS_ENABLED
+
+ :default: the opposite of ``DEBUG``
+
+ Boolean that decides if compression will happen. To test compression
+ when ``DEBUG`` is ``True`` ``COMPRESS_ENABLED`` must also be set to
+ ``True``.
+
+ When ``COMPRESS_ENABLED`` is ``False`` the input will be rendered without
+ any compression except for code with a mimetype matching one listed in the
+ :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` setting. These
+ matching files are still passed to the precompiler before rendering.
+
+ An example for some javascript and coffeescript.
+
+ .. code-block:: django
+
+ {% load compress %}
+
+ {% compress js %}
+ <script type="text/javascript" src="/static/js/site-base.js" />
+ <script type="text/coffeescript" charset="utf-8" src="/static/js/awesome.coffee" />
+ {% endcompress %}
+
+ With ``COMPRESS_ENABLED`` set to ``False`` this would give you something
+ like this::
+
+ <script type="text/javascript" src="/static/js/site-base.js"></script>
+ <script type="text/javascript" src="/static/CACHE/js/8dd1a2872443.js" charset="utf-8"></script>
+
+.. attribute:: COMPRESS_URL
+
+ :Default: ``STATIC_URL``
+
+ Controls the URL that linked files will be read from and compressed files
+ will be written to.
+
+.. attribute:: COMPRESS_ROOT
+
+ :Default: ``STATIC_ROOT``
+
+ Controls the absolute file path that linked static will be read from and
+ compressed static will be written to when using the default
+ :attr:`~django.conf.settings.COMPRESS_STORAGE`
+ ``compressor.storage.CompressorFileStorage``.
+
+.. attribute:: COMPRESS_OUTPUT_DIR
+
+ :Default: ``'CACHE'``
+
+ Controls the directory inside :attr:`~django.conf.settings.COMPRESS_ROOT`
+ that compressed files will be written to.
+
+Backend settings
+----------------
+
+.. attribute:: COMPRESS_CSS_FILTERS
+
+ :default: ``['compressor.filters.css_default.CssAbsoluteFilter']``
+
+ A list of filters that will be applied to CSS.
+
+ Possible options are (including their settings):
+
+ - ``compressor.filters.css_default.CssAbsoluteFilter``
+
+ A filter that normalizes the URLs used in ``url()`` CSS statements.
+
+ .. attribute:: COMPRESS_CSS_HASHING_METHOD
+
+ The method to use when calculating the hash to append to
+ processed URLs. Either ``'mtime'`` (default) or ``'content'``.
+ Use the latter in case you're using multiple server to serve your
+ static files.
+
+ - ``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.
+
+ .. attribute:: COMPRESS_DATA_URI_MAX_SIZE
+
+ Only files that are smaller than this in bytes value will be embedded.
+
+ - ``compressor.filters.yui.YUICSSFilter``
+
+ A filter that passes the CSS content to the `YUI compressor`_.
+
+ .. attribute:: COMPRESS_YUI_BINARY
+
+ The YUI compressor filesystem path. Make sure to also prepend this
+ setting with ``java -jar`` if you use that kind of distribution.
+
+ .. attribute:: COMPRESS_YUI_CSS_ARGUMENTS
+
+ The arguments passed to the compressor.
+
+ - ``compressor.filters.yuglify.YUglifyCSSFilter``
+
+ A filter that passes the CSS content to the `yUglify compressor`_.
+
+ .. attribute:: COMPRESS_YUGLIFY_BINARY
+
+ The yUglify compressor filesystem path.
+
+ .. attribute:: COMPRESS_YUGLIFY_CSS_ARGUMENTS
+
+ The arguments passed to the compressor. Defaults to --terminal.
+
+ - ``compressor.filters.cssmin.CSSMinFilter``
+
+ A filter that uses Zachary Voase's Python port of the YUI CSS compression
+ algorithm cssmin_.
+
+ .. _CSSTidy: http://csstidy.sourceforge.net/
+ .. _`data: URIs`: http://en.wikipedia.org/wiki/Data_URI_scheme
+ .. _cssmin: http://pypi.python.org/pypi/cssmin/
+
+ - ``compressor.filters.template.TemplateFilter``
+
+ A filter that renders the CSS content with Django templating system.
+
+ .. attribute:: COMPRESS_TEMPLATE_FILTER_CONTEXT
+
+ The context to render your css files with.
+
+
+.. _compress_js_filters:
+
+.. attribute:: COMPRESS_JS_FILTERS
+
+ :Default: ``['compressor.filters.jsmin.JSMinFilter']``
+
+ A list of filters that will be applied to javascript.
+
+ Possible options are:
+
+ - ``compressor.filters.jsmin.JSMinFilter``
+
+ A filter that uses the jsmin implementation rJSmin_ to compress
+ JavaScript code.
+
+ .. _slimit_filter:
+
+ - ``compressor.filters.jsmin.SlimItFilter``
+
+ A filter that uses the jsmin implementation `Slim It`_ to compress
+ JavaScript code.
+
+ - ``compressor.filters.closure.ClosureCompilerFilter``
+
+ A filter that uses `Google Closure compiler`_.
+
+ .. attribute:: COMPRESS_CLOSURE_COMPILER_BINARY
+
+ The Closure compiler filesystem path. Make sure to also prepend
+ this setting with ``java -jar`` if you use that kind of distribution.
+
+ .. attribute:: COMPRESS_CLOSURE_COMPILER_ARGUMENTS
+
+ The arguments passed to the compiler.
+
+ - ``compressor.filters.yui.YUIJSFilter``
+
+ A filter that passes the JavaScript code to the `YUI compressor`_.
+
+ .. attribute:: COMPRESS_YUI_BINARY
+
+ The YUI compressor filesystem path.
+
+ .. attribute:: COMPRESS_YUI_JS_ARGUMENTS
+
+ The arguments passed to the compressor.
+
+ - ``compressor.filters.yuglify.YUglifyJSFilter``
+
+ A filter that passes the JavaScript code to the `yUglify compressor`_.
+
+ .. attribute:: COMPRESS_YUGLIFY_BINARY
+
+ The yUglify compressor filesystem path.
+
+ .. attribute:: COMPRESS_YUGLIFY_JS_ARGUMENTS
+
+ The arguments passed to the compressor.
+
+ - ``compressor.filters.template.TemplateFilter``
+
+ A filter that renders the JavaScript code with Django templating system.
+
+ .. attribute:: COMPRESS_TEMPLATE_FILTER_CONTEXT
+
+ The context to render your JavaScript code with.
+
+ .. _rJSmin: http://opensource.perlig.de/rjsmin/
+ .. _`Google Closure compiler`: http://code.google.com/closure/compiler/
+ .. _`YUI compressor`: http://developer.yahoo.com/yui/compressor/
+ .. _`yUglify compressor`: https://github.com/yui/yuglify
+ .. _`Slim It`: http://slimit.org/
+
+.. attribute:: COMPRESS_PRECOMPILERS
+
+ :Default: ``()``
+
+ An iterable of two-tuples whose first item is the mimetype of the files or
+ hunks you want to compile with the command or filter specified as the second
+ item:
+
+ #. mimetype
+ The mimetype of the file or inline code that should be compiled.
+
+ #. command_or_filter
+ The command to call on each of the files. Modern Python string
+ formatting will be provided for the two placeholders ``{infile}`` and
+ ``{outfile}`` whose existence in the command string also triggers the
+ actual creation of those temporary files. If not given in the command
+ string, Django Compressor will use ``stdin`` and ``stdout`` respectively
+ instead.
+
+ Alternatively, you may provide the fully qualified class name of a
+ filter you wish to use as a precompiler.
+
+ Example::
+
+ COMPRESS_PRECOMPILERS = (
+ ('text/coffeescript', 'coffee --compile --stdio'),
+ ('text/less', 'lessc {infile} {outfile}'),
+ ('text/x-sass', 'sass {infile} {outfile}'),
+ ('text/x-scss', 'sass --scss {infile} {outfile}'),
+ ('text/stylus', 'stylus < {infile} > {outfile}'),
+ ('text/foobar', 'path.to.MyPrecompilerFilter'),
+ )
+
+ .. 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
+ precompiler like this::
+
+ ('text/less', 'lessc {infile}'),
+
+ With that setting (and CoffeeScript_ installed), you could add the following
+ code to your templates:
+
+ .. code-block:: django
+
+ {% load compress %}
+
+ {% compress js %}
+ <script type="text/coffeescript" charset="utf-8" src="/static/js/awesome.coffee" />
+ <script type="text/coffeescript" charset="utf-8">
+ # Functions:
+ square = (x) -> x * x
+ </script>
+ {% endcompress %}
+
+ This would give you something like this::
+
+ <script type="text/javascript" src="/static/CACHE/js/8dd1a2872443.js" charset="utf-8"></script>
+
+ The same works for less_, too:
+
+ .. code-block:: django
+
+ {% load compress %}
+
+ {% compress css %}
+ <link type="text/less" rel="stylesheet" href="/static/css/styles.less" charset="utf-8">
+ <style type="text/less">
+ @color: #4D926F;
+
+ #header {
+ color: @color;
+ }
+ </style>
+ {% endcompress %}
+
+ Which would be rendered something like::
+
+ <link rel="stylesheet" href="/static/CACHE/css/8ccf8d877f18.css" type="text/css" charset="utf-8">
+
+ .. _less: http://lesscss.org/
+ .. _CoffeeScript: http://jashkenas.github.com/coffee-script/
+
+.. attribute:: COMPRESS_STORAGE
+
+ :Default: ``'compressor.storage.CompressorFileStorage'``
+
+ The dotted path to a Django Storage backend to be used to save the
+ compressed files.
+
+ Django Compressor ships with one additional storage backend:
+
+ * ``'compressor.storage.GzipCompressorFileStorage'``
+
+ A subclass of the default storage backend, which will additionally
+ create ``*.gz`` files of each of the compressed files.
+
+.. attribute:: COMPRESS_PARSER
+
+ :Default: ``'compressor.parser.AutoSelectParser'``
+
+ The backend to use when parsing the JavaScript or Stylesheet files. The
+ ``AutoSelectParser`` picks the ``lxml`` based parser when available, and falls
+ back to ``HtmlParser`` if ``lxml`` is not available.
+
+ ``LxmlParser`` is the fastest available parser, but ``HtmlParser`` is not much
+ slower. ``AutoSelectParser`` adds a slight overhead, but in most cases it
+ won't be necessary to change the default parser.
+
+ The other two included parsers are considerably slower and should only be
+ used if absolutely necessary.
+
+ .. warning::
+
+ In some cases the ``compressor.parser.HtmlParser`` parser isn't able to
+ parse invalid HTML in JavaScript or CSS content. As a workaround you
+ should use one of the more forgiving parsers, e.g. the
+ ``BeautifulSoupParser``.
+
+ The backends included in Django Compressor:
+
+ - ``compressor.parser.AutoSelectParser``
+ - ``compressor.parser.LxmlParser``
+ - ``compressor.parser.HtmlParser``
+ - ``compressor.parser.BeautifulSoupParser``
+ - ``compressor.parser.Html5LibParser``
+
+ See :ref:`dependencies` for more info about the packages you need
+ for each parser.
+
+Caching settings
+----------------
+
+.. attribute:: COMPRESS_CACHE_BACKEND
+
+ :Default: ``CACHES["default"]`` or ``CACHE_BACKEND``
+
+ The backend to use for caching, in case you want to use a different cache
+ backend for Django Compressor.
+
+ If you have set the ``CACHES`` setting (new in Django 1.3),
+ ``COMPRESS_CACHE_BACKEND`` defaults to ``"default"``, which is the alias for
+ the default cache backend. You can set it to a different alias that you have
+ configured in your ``CACHES`` setting.
+
+ If you have not set ``CACHES`` and are using the old ``CACHE_BACKEND``
+ setting, ``COMPRESS_CACHE_BACKEND`` defaults to the ``CACHE_BACKEND`` setting.
+
+.. attribute:: COMPRESS_REBUILD_TIMEOUT
+
+ :Default: ``2592000`` (30 days in seconds)
+
+ The period of time after which the compressed files are rebuilt even if
+ no file changes are detected.
+
+.. attribute:: COMPRESS_MINT_DELAY
+
+ :Default: ``30`` (seconds)
+
+ The upper bound on how long any compression should take to run. Prevents
+ dog piling, should be a lot smaller than
+ :attr:`~django.conf.settings.COMPRESS_REBUILD_TIMEOUT`.
+
+.. attribute:: COMPRESS_MTIME_DELAY
+
+ :Default: ``10``
+
+ The amount of time (in seconds) to cache the modification timestamp of a
+ file. Should be smaller than
+ :attr:`~django.conf.settings.COMPRESS_REBUILD_TIMEOUT` and
+ :attr:`~django.conf.settings.COMPRESS_MINT_DELAY`.
+
+.. attribute:: COMPRESS_DEBUG_TOGGLE
+
+ :Default: None
+
+ The name of the GET variable that toggles the debug mode and prevents Django
+ Compressor from performing the actual compression. Only useful for debugging.
+
+ .. warning::
+
+ Don't use this option in production!
+
+ An easy convention is to only set it depending on the ``DEBUG`` setting::
+
+ if DEBUG:
+ COMPRESS_DEBUG_TOGGLE = 'whatever'
+
+ .. note::
+
+ This only works for pages that are rendered using the RequestContext_
+ 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.
+
+Offline settings
+----------------
+
+.. attribute:: COMPRESS_OFFLINE
+
+ :Default: ``False``
+
+ Boolean that decides if compression should also be done outside of the
+ request/response loop -- independent from user requests. This allows to
+ pre-compress CSS and JavaScript files and works just like the automatic
+ compression with the ``{% compress %}`` tag.
+
+.. attribute:: COMPRESS_OFFLINE_TIMEOUT
+
+ :Default: ``31536000`` (1 year in seconds)
+
+ The period of time with which the ``compress`` management command stores
+ the pre-compressed the contents of ``{% compress %}`` template tags in
+ the cache.
+
+.. attribute:: COMPRESS_OFFLINE_CONTEXT
+
+ :Default: ``{'STATIC_URL': settings.STATIC_URL}``
+
+ The context to be used by the ``compress`` management command when rendering
+ the contents of ``{% compress %}`` template tags and saving the result in the
+ offline cache.
+
+ If available, the ``STATIC_URL`` setting is also added to the context.
+
+.. attribute:: COMPRESS_OFFLINE_MANIFEST
+
+ :Default: ``manifest.json``
+
+ The name of the file to be used for saving the names of the files
+ compressed offline.
--- /dev/null
+.. _usage:
+
+Usage
+=====
+
+.. code-block:: django
+
+ {% load compress %}
+ {% compress <js/css> [<file/inline> [block_name]] %}
+ <html of inline or linked JS/CSS>
+ {% endcompress %}
+
+Examples
+--------
+
+.. code-block:: django
+
+ {% load compress %}
+
+ {% compress css %}
+ <link rel="stylesheet" href="/static/css/one.css" type="text/css" charset="utf-8">
+ <style type="text/css">p { border:5px solid green;}</style>
+ <link rel="stylesheet" href="/static/css/two.css" type="text/css" charset="utf-8">
+ {% endcompress %}
+
+Which would be rendered something like:
+
+.. code-block:: django
+
+ <link rel="stylesheet" href="/static/CACHE/css/f7c661b7a124.css" type="text/css" charset="utf-8">
+
+or:
+
+.. code-block:: django
+
+ {% load compress %}
+
+ {% compress js %}
+ <script src="/static/js/one.js" type="text/javascript" charset="utf-8"></script>
+ <script type="text/javascript" charset="utf-8">obj.value = "value";</script>
+ {% endcompress %}
+
+Which would be rendered something like:
+
+.. code-block:: django
+
+ <script type="text/javascript" src="/static/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>
+
+.. note::
+
+ Remember that django-compressor will try to :ref:`group ouputs by media <css_notes>`.
+
+Linked files **must** be accessible via
+:attr:`~django.conf.settings.COMPRESS_URL`.
+
+If the :attr:`~django.conf.settings.COMPRESS_ENABLED` setting is ``False``
+(defaults to the opposite of DEBUG) the ``compress`` template tag does nothing
+and simply returns exactly what it was given.
+
+.. note::
+
+ If you've configured any
+ :attr:`precompilers <django.conf.settings.COMPRESS_PRECOMPILERS>`
+ setting :attr:`~django.conf.settings.COMPRESS_ENABLED` to ``False`` won't
+ affect the processing of those files. Only the
+ :attr:`CSS <django.conf.settings.COMPRESS_CSS_FILTERS>` and
+ :attr:`JavaScript filters <django.conf.settings.COMPRESS_JS_FILTERS>`
+ will be disabled.
+
+If both DEBUG and :attr:`~django.conf.settings.COMPRESS_ENABLED` are set to
+``True``, incompressible files (off-site or non existent) will throw an
+exception. If DEBUG is ``False`` these files will be silently stripped.
+
+.. warning::
+
+ For production sites it is **strongly recommended** to use a real cache
+ backend such as memcached_ to speed up the checks of compressed files.
+ Make sure you set your Django cache backend appropriately (also see
+ :attr:`~django.conf.settings.COMPRESS_CACHE_BACKEND` and
+ Django's `caching documentation`_).
+
+The compress template tag supports a second argument specifying the output
+mode and defaults to saving the result in a file. Alternatively you can
+pass '``inline``' to the template tag to return the content directly to the
+rendered page, e.g.:
+
+.. code-block:: django
+
+ {% load compress %}
+
+ {% compress js inline %}
+ <script src="/static/js/one.js" type="text/javascript" charset="utf-8"></script>
+ <script type="text/javascript" charset="utf-8">obj.value = "value";</script>
+ {% endcompress %}
+
+would be rendered something like::
+
+ <script type="text/javascript" charset="utf-8">
+ obj = {};
+ obj.value = "value";
+ </script>
+
+The compress template tag also supports a third argument for naming the output
+of that particular compress tag. This is then added to the context so you can
+access it in the `post_compress signal <signals>`.
+
+.. _memcached: http://memcached.org/
+.. _caching documentation: http://docs.djangoproject.com/en/1.2/topics/cache/#memcached
+
+.. _pre-compression:
+
+Pre-compression
+---------------
+
+Django Compressor comes with an optional ``compress`` management command to
+run the compression outside of the request/response loop -- independent
+from user requests. This allows to pre-compress CSS and JavaScript files and
+works just like the automatic compression with the ``{% compress %}`` tag.
+
+To compress the files "offline" and update the offline cache you have
+to use the ``compress`` management command, ideally during deployment.
+Also make sure to enable the :attr:`django.conf.settings.COMPRESS_OFFLINE`
+setting. In case you don't use the ``compress`` management command, Django
+Compressor will automatically fallback to the automatic compression using
+the template tag.
+
+The command parses all templates that can be found with the template
+loader (as specified in the TEMPLATE_LOADERS_ setting) and looks for
+``{% compress %}`` blocks. It then will use the context as defined in
+:attr:`django.conf.settings.COMPRESS_OFFLINE_CONTEXT` to render its
+content. So if you use any variables inside the ``{% compress %}`` blocks,
+make sure to list all values you require in ``COMPRESS_OFFLINE_CONTEXT``.
+It's similar to a template context and should be used if a variable is used
+in the blocks, e.g.:
+
+.. code-block:: django
+
+ {% load compress %}
+ {% compress js %}
+ <script src="{{ path_to_files }}js/one.js" type="text/javascript" charset="utf-8"></script>
+ {% endcompress %}
+
+Since this template requires a variable (``path_to_files``) you need to
+specify this in your settings before using the ``compress`` management
+command::
+
+ COMPRESS_OFFLINE_CONTEXT = {
+ 'path_to_files': '/static/js/',
+ }
+
+If not specified, the ``COMPRESS_OFFLINE_CONTEXT`` will by default contain
+the commonly used setting to refer to saved files ``STATIC_URL``.
+
+The result of running the ``compress`` management command will be cached
+in a file called ``manifest.json`` using the :attr:`configured storage
+<django.conf.settings.COMPRESS_STORAGE>` to be able to be transfered from your developement
+computer to the server easily.
+
+.. _TEMPLATE_LOADERS: http://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
+
+.. _signals:
+
+Signals
+-------
+
+.. function:: compressor.signals.post_compress(sender, type, mode, context)
+
+Django Compressor includes a ``post_compress`` signal that enables you to
+listen for changes to your compressed CSS/JS. This is useful, for example, if
+you need the exact filenames for use in an HTML5 manifest file. The signal
+sends the following arguments:
+
+``sender``
+ Either :class:`compressor.css.CssCompressor` or
+ :class:`compressor.js.JsCompressor`.
+
+ .. versionchanged:: 1.2
+
+ The sender is now one of the supported Compressor classes for
+ easier limitation to only one of them, previously it was a string
+ named ``'django-compressor'``.
+
+``type``
+ Either "``js``" or "``css``".
+
+``mode``
+ Either "``file``" or "``inline``".
+
+``context``
+ The context dictionary used to render the output of the compress template
+ tag.
+
+ If ``mode`` is "``file``" the dictionary named ``compressed`` in the
+ context will contain a "``url``" key that maps to the relative URL for
+ the compressed asset.
+
+ If ``type`` is "``css``", the dictionary named ``compressed`` in the
+ context will additionally contain a "``media``" key with a value of
+ ``None`` if no media attribute is specified on the link/style tag and
+ equal to that attribute if one is specified.
+
+ Additionally, ``context['compressed']['name']`` will be the third
+ positional argument to the template tag, if provided.
+
+.. note::
+
+ When compressing CSS, the ``post_compress`` signal will be called once for
+ every different media attribute on the tags within the ``{% compress %}``
+ tag in question.
+
+.. _css_notes:
+
+CSS Notes
+---------
+
+All relative ``url()`` bits specified in linked CSS files are automatically
+converted to absolute URLs while being processed. Any local absolute URLs (those
+starting with a ``'/'``) are left alone.
+
+Stylesheets that are ``@import``'d are not compressed into the main file.
+They are left alone.
+
+If the media attribute is set on <style> and <link> elements, a separate
+compressed file is created and linked for each media value you specified.
+This allows the media attribute to remain on the generated link element,
+instead of wrapping your CSS with @media blocks (which can break your own
+@media queries or @font-face declarations). It also allows browsers to avoid
+downloading CSS for irrelevant media types.
+
+Recommendations
+---------------
+
+* Use only relative or full domain absolute URLs in your CSS files.
+* Avoid @import! Simply list all your CSS files in the HTML, they'll be combined anyway.
--- /dev/null
+flake8
+coverage
+html5lib
+mock
+jinja2
+lxml
+BeautifulSoup
+unittest2
+coffin
+jingo
--- /dev/null
+[wheel]
+universal = 1
+
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
--- /dev/null
+from __future__ import print_function
+import ast
+import os
+import sys
+import codecs
+from fnmatch import fnmatchcase
+from distutils.util import convert_path
+from setuptools import setup, find_packages
+
+class VersionFinder(ast.NodeVisitor):
+ def __init__(self):
+ self.version = None
+
+ def visit_Assign(self, node):
+ if node.targets[0].id == '__version__':
+ self.version = node.value.s
+
+
+def read(*parts):
+ filename = os.path.join(os.path.dirname(__file__), *parts)
+ with codecs.open(filename, encoding='utf-8') as fp:
+ return fp.read()
+
+
+def find_version(*parts):
+ finder = VersionFinder()
+ finder.visit(ast.parse(read(*parts)))
+ return finder.version
+
+
+# Provided as an attribute, so you can append to these instead
+# of replicating them:
+standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak')
+standard_exclude_directories = ('.*', 'CVS', '_darcs', './build',
+ './dist', 'EGG-INFO', '*.egg-info')
+
+
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+# Note: you may want to copy this into your setup.py file verbatim, as
+# you can't import this from another package, when you don't know if
+# that package is installed yet.
+def find_package_data(where='.', package='',
+ exclude=standard_exclude,
+ exclude_directories=standard_exclude_directories,
+ only_in_packages=True,
+ show_ignored=False):
+ """
+ Return a dictionary suitable for use in ``package_data``
+ in a distutils ``setup.py`` file.
+
+ The dictionary looks like::
+
+ {'package': [files]}
+
+ Where ``files`` is a list of all the files in that package that
+ don't match anything in ``exclude``.
+
+ If ``only_in_packages`` is true, then top-level directories that
+ are not packages won't be included (but directories under packages
+ will).
+
+ Directories matching any pattern in ``exclude_directories`` will
+ be ignored; by default directories with leading ``.``, ``CVS``,
+ and ``_darcs`` will be ignored.
+
+ If ``show_ignored`` is true, then all the files that aren't
+ included in package data are shown on stderr (for debugging
+ purposes).
+
+ Note patterns use wildcards, or can be exact paths (including
+ leading ``./``), and all searching is case-insensitive.
+ """
+
+ out = {}
+ stack = [(convert_path(where), '', package, only_in_packages)]
+ while stack:
+ where, prefix, package, only_in_packages = stack.pop(0)
+ for name in os.listdir(where):
+ fn = os.path.join(where, name)
+ if os.path.isdir(fn):
+ bad_name = False
+ for pattern in exclude_directories:
+ if (fnmatchcase(name, pattern) or fn.lower() == pattern.lower()):
+ bad_name = True
+ if show_ignored:
+ print("Directory %s ignored by pattern %s" %
+ (fn, pattern), file=sys.stderr)
+ break
+ if bad_name:
+ continue
+ if (os.path.isfile(os.path.join(fn, '__init__.py')) and not prefix):
+ if not package:
+ new_package = name
+ else:
+ new_package = package + '.' + name
+ stack.append((fn, '', new_package, False))
+ else:
+ stack.append((fn, prefix + name + '/', package, only_in_packages))
+ elif package or not only_in_packages:
+ # is a file
+ bad_name = False
+ for pattern in exclude:
+ if (fnmatchcase(name, pattern) or fn.lower() == pattern.lower()):
+ bad_name = True
+ if show_ignored:
+ print("File %s ignored by pattern %s" %
+ (fn, pattern), file=sys.stderr)
+ break
+ if bad_name:
+ continue
+ out.setdefault(package, []).append(prefix + name)
+ return out
+
+setup(
+ name="django_compressor",
+ version=find_version("compressor", "__init__.py"),
+ url='http://django-compressor.readthedocs.org/en/latest/',
+ license='MIT',
+ description="Compresses linked and inline JavaScript or CSS into single cached files.",
+ long_description=read('README.rst'),
+ author='Jannis Leidel',
+ author_email='jannis@leidel.info',
+ packages=find_packages(),
+ package_data=find_package_data(),
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.2',
+ 'Programming Language :: Python :: 3.3',
+ 'Topic :: Internet :: WWW/HTTP',
+ ],
+ zip_safe=False,
+ install_requires=[
+ 'django-appconf >= 0.4',
+ ],
+)
--- /dev/null
+[deps]
+two =
+ flake8
+ coverage
+ html5lib
+ mock
+ jinja2
+ lxml
+ BeautifulSoup
+ unittest2
+ jingo
+ coffin
+three =
+ flake8
+ coverage
+ html5lib
+ mock
+ jinja2
+ lxml
+ BeautifulSoup4
+ jingo
+ coffin
+three_two =
+ flake8
+ coverage
+ html5lib
+ mock
+ jinja2==2.6
+ lxml
+ BeautifulSoup4
+ jingo
+ coffin
+
+[tox]
+envlist =
+ py33-1.6.X,
+ py32-1.6.X,
+ py27-1.6.X,
+ py26-1.6.X,
+ py33-1.5.X,
+ py32-1.5.X,
+ py27-1.5.X,
+ py26-1.5.X,
+ py27-1.4.X,
+ py26-1.4.X
+
+[testenv]
+setenv =
+ CPPFLAGS=-O0
+usedevelop = true
+whitelist_externals = /usr/bin/make
+downloadcache = {toxworkdir}/_download/
+commands =
+ django-admin.py --version
+ make test
+
+[testenv:py33-1.6.X]
+basepython = python3.3
+deps =
+ Django>=1.6,<1.7
+ {[deps]three}
+
+[testenv:py32-1.6.X]
+basepython = python3.2
+deps =
+ Django>=1.6,<1.7
+ {[deps]three_two}
+
+[testenv:py27-1.6.X]
+basepython = python2.7
+deps =
+ Django>=1.6,<1.7
+ {[deps]two}
+
+[testenv:py26-1.6.X]
+basepython = python2.6
+deps =
+ Django>=1.6,<1.7
+ {[deps]two}
+
+[testenv:py33-1.5.X]
+basepython = python3.3
+deps =
+ Django>=1.5,<1.6
+ django-discover-runner
+ {[deps]three}
+
+[testenv:py32-1.5.X]
+basepython = python3.2
+deps =
+ Django>=1.5,<1.6
+ django-discover-runner
+ {[deps]three_two}
+
+[testenv:py27-1.5.X]
+basepython = python2.7
+deps =
+ Django>=1.5,<1.6
+ django-discover-runner
+ {[deps]two}
+
+[testenv:py26-1.5.X]
+basepython = python2.6
+deps =
+ Django>=1.5,<1.6
+ django-discover-runner
+ {[deps]two}
+
+[testenv:py27-1.4.X]
+basepython = python2.7
+deps =
+ Django>=1.4,<1.5
+ django-discover-runner
+ {[deps]two}
+
+[testenv:py26-1.4.X]
+basepython = python2.6
+deps =
+ Django>=1.4,<1.5
+ django-discover-runner
+ {[deps]two}
--- /dev/null
+#!/bin/bash -x
+
+RES=0
+
+case $1 in
+python-compressor|python-django-compressor)
+ echo "Testing $1"
+ python -c "import compressor"
+ RES=$?
+;;
+*)
+ echo "test not defined, skipping..."
+;;
+esac
+
+exit $RES