]> review.fuel-infra Code Review - packages/trusty/python-django-compressor.git/commitdiff
Build python-compressor for Ubuntu 14.04 21/4021/1
authorSergey Kolekonov <skolekonov@mirantis.com>
Thu, 26 Feb 2015 10:17:46 +0000 (13:17 +0300)
committerSergey Kolekonov <skolekonov@mirantis.com>
Thu, 26 Feb 2015 10:23:22 +0000 (13:23 +0300)
According to Juno requirements [1], python-django-compressor have to be updated
to 1.4 version, Trusty contains old package

[1] https://github.com/openstack/requirements/blob/stable/juno/global-requirements.txt#L19

Sources have been copied from packages/precise/python-django-compressor 6.1 branch
Commit e0a2d84cb9eb5ff2ec8306ae30787fd1259058d3

Change-Id: Id83f5b47ccea8629bcac949fcb7baac670f0efe7

166 files changed:
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/docs [new file with mode: 0644]
debian/pydist-overrides [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/source/format [new file with mode: 0644]
debian/watch [new file with mode: 0644]
django-compressor/AUTHORS [new file with mode: 0644]
django-compressor/LICENSE [new file with mode: 0644]
django-compressor/MANIFEST.in [new file with mode: 0644]
django-compressor/Makefile [new file with mode: 0644]
django-compressor/PKG-INFO [new file with mode: 0644]
django-compressor/README.rst [new file with mode: 0644]
django-compressor/compressor/__init__.py [new file with mode: 0644]
django-compressor/compressor/base.py [new file with mode: 0644]
django-compressor/compressor/cache.py [new file with mode: 0644]
django-compressor/compressor/conf.py [new file with mode: 0644]
django-compressor/compressor/contrib/__init__.py [new file with mode: 0644]
django-compressor/compressor/contrib/jinja2ext.py [new file with mode: 0644]
django-compressor/compressor/contrib/sekizai.py [new file with mode: 0644]
django-compressor/compressor/css.py [new file with mode: 0644]
django-compressor/compressor/exceptions.py [new file with mode: 0644]
django-compressor/compressor/filters/__init__.py [new file with mode: 0644]
django-compressor/compressor/filters/base.py [new file with mode: 0644]
django-compressor/compressor/filters/closure.py [new file with mode: 0644]
django-compressor/compressor/filters/css_default.py [new file with mode: 0644]
django-compressor/compressor/filters/cssmin/__init__.py [new file with mode: 0644]
django-compressor/compressor/filters/cssmin/cssmin.py [new file with mode: 0644]
django-compressor/compressor/filters/cssmin/rcssmin.py [new file with mode: 0644]
django-compressor/compressor/filters/csstidy.py [new file with mode: 0644]
django-compressor/compressor/filters/datauri.py [new file with mode: 0644]
django-compressor/compressor/filters/jsmin/__init__.py [new file with mode: 0644]
django-compressor/compressor/filters/jsmin/rjsmin.py [new file with mode: 0755]
django-compressor/compressor/filters/jsmin/slimit.py [new file with mode: 0644]
django-compressor/compressor/filters/template.py [new file with mode: 0644]
django-compressor/compressor/filters/yuglify.py [new file with mode: 0644]
django-compressor/compressor/filters/yui.py [new file with mode: 0644]
django-compressor/compressor/finders.py [new file with mode: 0644]
django-compressor/compressor/js.py [new file with mode: 0644]
django-compressor/compressor/management/__init__.py [new file with mode: 0644]
django-compressor/compressor/management/commands/__init__.py [new file with mode: 0644]
django-compressor/compressor/management/commands/compress.py [new file with mode: 0644]
django-compressor/compressor/management/commands/mtime_cache.py [new file with mode: 0644]
django-compressor/compressor/models.py [new file with mode: 0644]
django-compressor/compressor/offline/__init__.py [new file with mode: 0644]
django-compressor/compressor/offline/django.py [new file with mode: 0644]
django-compressor/compressor/offline/jinja2.py [new file with mode: 0644]
django-compressor/compressor/parser/__init__.py [new file with mode: 0644]
django-compressor/compressor/parser/base.py [new file with mode: 0644]
django-compressor/compressor/parser/beautifulsoup.py [new file with mode: 0644]
django-compressor/compressor/parser/default_htmlparser.py [new file with mode: 0644]
django-compressor/compressor/parser/html5lib.py [new file with mode: 0644]
django-compressor/compressor/parser/lxml.py [new file with mode: 0644]
django-compressor/compressor/signals.py [new file with mode: 0644]
django-compressor/compressor/storage.py [new file with mode: 0644]
django-compressor/compressor/templates/compressor/css_file.html [new file with mode: 0644]
django-compressor/compressor/templates/compressor/css_inline.html [new file with mode: 0644]
django-compressor/compressor/templates/compressor/js_file.html [new file with mode: 0644]
django-compressor/compressor/templates/compressor/js_inline.html [new file with mode: 0644]
django-compressor/compressor/templatetags/__init__.py [new file with mode: 0644]
django-compressor/compressor/templatetags/compress.py [new file with mode: 0644]
django-compressor/compressor/test_settings.py [new file with mode: 0644]
django-compressor/compressor/tests/__init__.py [new file with mode: 0644]
django-compressor/compressor/tests/precompiler.py [new file with mode: 0644]
django-compressor/compressor/tests/static/css/datauri.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/nonasc.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/one.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/two.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/url/2/url2.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/url/nonasc.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/url/test.css [new file with mode: 0644]
django-compressor/compressor/tests/static/css/url/url1.css [new file with mode: 0644]
django-compressor/compressor/tests/static/img/add.png [new file with mode: 0644]
django-compressor/compressor/tests/static/img/python.png [new file with mode: 0644]
django-compressor/compressor/tests/static/js/nonasc-latin1.js [new file with mode: 0644]
django-compressor/compressor/tests/static/js/nonasc.js [new file with mode: 0644]
django-compressor/compressor/tests/static/js/one.coffee [new file with mode: 0644]
django-compressor/compressor/tests/static/js/one.js [new file with mode: 0644]
django-compressor/compressor/tests/test_base.py [new file with mode: 0644]
django-compressor/compressor/tests/test_filters.py [new file with mode: 0644]
django-compressor/compressor/tests/test_jinja2ext.py [new file with mode: 0644]
django-compressor/compressor/tests/test_offline.py [new file with mode: 0644]
django-compressor/compressor/tests/test_parsers.py [new file with mode: 0644]
django-compressor/compressor/tests/test_signals.py [new file with mode: 0644]
django-compressor/compressor/tests/test_storages.py [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html [new file with mode: 0644]
django-compressor/compressor/tests/test_templatetags.py [new file with mode: 0644]
django-compressor/compressor/utils/__init__.py [new file with mode: 0644]
django-compressor/compressor/utils/decorators.py [new file with mode: 0644]
django-compressor/compressor/utils/staticfiles.py [new file with mode: 0644]
django-compressor/compressor/utils/stringformat.py [new file with mode: 0644]
django-compressor/django_compressor.egg-info/PKG-INFO [new file with mode: 0644]
django-compressor/django_compressor.egg-info/SOURCES.txt [new file with mode: 0644]
django-compressor/django_compressor.egg-info/dependency_links.txt [new file with mode: 0644]
django-compressor/django_compressor.egg-info/not-zip-safe [new file with mode: 0644]
django-compressor/django_compressor.egg-info/requires.txt [new file with mode: 0644]
django-compressor/django_compressor.egg-info/top_level.txt [new file with mode: 0644]
django-compressor/docs/Makefile [new file with mode: 0644]
django-compressor/docs/behind-the-scenes.txt [new file with mode: 0644]
django-compressor/docs/changelog.txt [new file with mode: 0644]
django-compressor/docs/conf.py [new file with mode: 0644]
django-compressor/docs/contributing.txt [new file with mode: 0644]
django-compressor/docs/django-sekizai.txt [new file with mode: 0644]
django-compressor/docs/index.txt [new file with mode: 0644]
django-compressor/docs/jinja2.txt [new file with mode: 0644]
django-compressor/docs/make.bat [new file with mode: 0644]
django-compressor/docs/quickstart.txt [new file with mode: 0644]
django-compressor/docs/remote-storages.txt [new file with mode: 0644]
django-compressor/docs/scenarios.txt [new file with mode: 0644]
django-compressor/docs/settings.txt [new file with mode: 0644]
django-compressor/docs/usage.txt [new file with mode: 0644]
django-compressor/requirements/tests.txt [new file with mode: 0644]
django-compressor/setup.cfg [new file with mode: 0644]
django-compressor/setup.py [new file with mode: 0644]
django-compressor/tox.ini [new file with mode: 0644]

diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..655fbb6
--- /dev/null
@@ -0,0 +1,55 @@
+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
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..ec63514
--- /dev/null
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..b621576
--- /dev/null
@@ -0,0 +1,32 @@
+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.6.6-3~),
+ 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.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..c573e42
--- /dev/null
@@ -0,0 +1,108 @@
+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
diff --git a/debian/docs b/debian/docs
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/debian/pydist-overrides b/debian/pydist-overrides
new file mode 100644 (file)
index 0000000..3a0edc6
--- /dev/null
@@ -0,0 +1 @@
+django_appconf python-appconf
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..b4db7a3
--- /dev/null
@@ -0,0 +1,32 @@
+#!/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/*
diff --git a/debian/source/format b/debian/source/format
new file mode 100644 (file)
index 0000000..163aaf8
--- /dev/null
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/watch b/debian/watch
new file mode 100644 (file)
index 0000000..a0ca5aa
--- /dev/null
@@ -0,0 +1,2 @@
+version=3
+http://pypi.python.org/packages/source/d/django-compressor/django-compressor-(.*)\.tar.gz
diff --git a/django-compressor/AUTHORS b/django-compressor/AUTHORS
new file mode 100644 (file)
index 0000000..de59146
--- /dev/null
@@ -0,0 +1,94 @@
+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
diff --git a/django-compressor/LICENSE b/django-compressor/LICENSE
new file mode 100644 (file)
index 0000000..d9432d5
--- /dev/null
@@ -0,0 +1,128 @@
+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.
diff --git a/django-compressor/MANIFEST.in b/django-compressor/MANIFEST.in
new file mode 100644 (file)
index 0000000..a470974
--- /dev/null
@@ -0,0 +1,10 @@
+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
diff --git a/django-compressor/Makefile b/django-compressor/Makefile
new file mode 100644 (file)
index 0000000..0c4c65f
--- /dev/null
@@ -0,0 +1,11 @@
+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
diff --git a/django-compressor/PKG-INFO b/django-compressor/PKG-INFO
new file mode 100644 (file)
index 0000000..6b41def
--- /dev/null
@@ -0,0 +1,104 @@
+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
diff --git a/django-compressor/README.rst b/django-compressor/README.rst
new file mode 100644 (file)
index 0000000..93afc64
--- /dev/null
@@ -0,0 +1,81 @@
+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
+
diff --git a/django-compressor/compressor/__init__.py b/django-compressor/compressor/__init__.py
new file mode 100644 (file)
index 0000000..ae247e6
--- /dev/null
@@ -0,0 +1,2 @@
+# following PEP 386
+__version__ = "1.4"
diff --git a/django-compressor/compressor/base.py b/django-compressor/compressor/base.py
new file mode 100644 (file)
index 0000000..de9c9ce
--- /dev/null
@@ -0,0 +1,339 @@
+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)
diff --git a/django-compressor/compressor/cache.py b/django-compressor/compressor/cache.py
new file mode 100644 (file)
index 0000000..4847939
--- /dev/null
@@ -0,0 +1,151 @@
+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))
diff --git a/django-compressor/compressor/conf.py b/django-compressor/compressor/conf.py
new file mode 100644 (file)
index 0000000..e9763d9
--- /dev/null
@@ -0,0 +1,120 @@
+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
diff --git a/django-compressor/compressor/contrib/__init__.py b/django-compressor/compressor/contrib/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/django-compressor/compressor/contrib/jinja2ext.py b/django-compressor/compressor/contrib/jinja2ext.py
new file mode 100644 (file)
index 0000000..7215d4d
--- /dev/null
@@ -0,0 +1,55 @@
+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']
diff --git a/django-compressor/compressor/contrib/sekizai.py b/django-compressor/compressor/contrib/sekizai.py
new file mode 100644 (file)
index 0000000..87966c5
--- /dev/null
@@ -0,0 +1,18 @@
+"""
+ 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)
diff --git a/django-compressor/compressor/css.py b/django-compressor/compressor/css.py
new file mode 100644 (file)
index 0000000..e10697b
--- /dev/null
@@ -0,0 +1,53 @@
+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)
diff --git a/django-compressor/compressor/exceptions.py b/django-compressor/compressor/exceptions.py
new file mode 100644 (file)
index 0000000..c2d7c60
--- /dev/null
@@ -0,0 +1,54 @@
+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
diff --git a/django-compressor/compressor/filters/__init__.py b/django-compressor/compressor/filters/__init__.py
new file mode 100644 (file)
index 0000000..cd317fa
--- /dev/null
@@ -0,0 +1,3 @@
+# flake8: noqa
+from compressor.filters.base import (FilterBase, CallbackOutputFilter,
+                                     CompilerFilter, FilterError)
diff --git a/django-compressor/compressor/filters/base.py b/django-compressor/compressor/filters/base.py
new file mode 100644 (file)
index 0000000..284afcb
--- /dev/null
@@ -0,0 +1,188 @@
+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)
diff --git a/django-compressor/compressor/filters/closure.py b/django-compressor/compressor/filters/closure.py
new file mode 100644 (file)
index 0000000..d229bcb
--- /dev/null
@@ -0,0 +1,10 @@
+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),
+    )
diff --git a/django-compressor/compressor/filters/css_default.py b/django-compressor/compressor/filters/css_default.py
new file mode 100644 (file)
index 0000000..1727beb
--- /dev/null
@@ -0,0 +1,108 @@
+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'")
diff --git a/django-compressor/compressor/filters/cssmin/__init__.py b/django-compressor/compressor/filters/cssmin/__init__.py
new file mode 100644 (file)
index 0000000..073303d
--- /dev/null
@@ -0,0 +1,13 @@
+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"
diff --git a/django-compressor/compressor/filters/cssmin/cssmin.py b/django-compressor/compressor/filters/cssmin/cssmin.py
new file mode 100644 (file)
index 0000000..e8a02b0
--- /dev/null
@@ -0,0 +1,245 @@
+#!/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()
diff --git a/django-compressor/compressor/filters/cssmin/rcssmin.py b/django-compressor/compressor/filters/cssmin/rcssmin.py
new file mode 100644 (file)
index 0000000..ff8e273
--- /dev/null
@@ -0,0 +1,360 @@
+#!/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()
diff --git a/django-compressor/compressor/filters/csstidy.py b/django-compressor/compressor/filters/csstidy.py
new file mode 100644 (file)
index 0000000..4b7e4c7
--- /dev/null
@@ -0,0 +1,10 @@
+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),
+    )
diff --git a/django-compressor/compressor/filters/datauri.py b/django-compressor/compressor/filters/datauri.py
new file mode 100644 (file)
index 0000000..ee67eeb
--- /dev/null
@@ -0,0 +1,57 @@
+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\(([^\)]+)\)'),
+    )
diff --git a/django-compressor/compressor/filters/jsmin/__init__.py b/django-compressor/compressor/filters/jsmin/__init__.py
new file mode 100644 (file)
index 0000000..48d8007
--- /dev/null
@@ -0,0 +1,10 @@
+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
diff --git a/django-compressor/compressor/filters/jsmin/rjsmin.py b/django-compressor/compressor/filters/jsmin/rjsmin.py
new file mode 100755 (executable)
index 0000000..6eedf2f
--- /dev/null
@@ -0,0 +1,300 @@
+#!/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()))
diff --git a/django-compressor/compressor/filters/jsmin/slimit.py b/django-compressor/compressor/filters/jsmin/slimit.py
new file mode 100644 (file)
index 0000000..9ffc7f4
--- /dev/null
@@ -0,0 +1,10 @@
+from __future__ import absolute_import
+from compressor.filters import CallbackOutputFilter
+
+
+class SlimItFilter(CallbackOutputFilter):
+    dependencies = ["slimit"]
+    callback = "slimit.minify"
+    kwargs = {
+        "mangle": True,
+    }
diff --git a/django-compressor/compressor/filters/template.py b/django-compressor/compressor/filters/template.py
new file mode 100644 (file)
index 0000000..8bf7365
--- /dev/null
@@ -0,0 +1,12 @@
+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)
diff --git a/django-compressor/compressor/filters/yuglify.py b/django-compressor/compressor/filters/yuglify.py
new file mode 100644 (file)
index 0000000..07066cc
--- /dev/null
@@ -0,0 +1,26 @@
+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),
+    )
diff --git a/django-compressor/compressor/filters/yui.py b/django-compressor/compressor/filters/yui.py
new file mode 100644 (file)
index 0000000..60fd1f7
--- /dev/null
@@ -0,0 +1,28 @@
+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),
+    )
diff --git a/django-compressor/compressor/finders.py b/django-compressor/compressor/finders.py
new file mode 100644 (file)
index 0000000..7de1fa2
--- /dev/null
@@ -0,0 +1,15 @@
+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 []
diff --git a/django-compressor/compressor/js.py b/django-compressor/compressor/js.py
new file mode 100644 (file)
index 0000000..b087804
--- /dev/null
@@ -0,0 +1,25 @@
+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
diff --git a/django-compressor/compressor/management/__init__.py b/django-compressor/compressor/management/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/django-compressor/compressor/management/commands/__init__.py b/django-compressor/compressor/management/commands/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/django-compressor/compressor/management/commands/compress.py b/django-compressor/compressor/management/commands/compress.py
new file mode 100644 (file)
index 0000000..6be215e
--- /dev/null
@@ -0,0 +1,274 @@
+# 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)
diff --git a/django-compressor/compressor/management/commands/mtime_cache.py b/django-compressor/compressor/management/commands/mtime_cache.py
new file mode 100644 (file)
index 0000000..e96f004
--- /dev/null
@@ -0,0 +1,82 @@
+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))
diff --git a/django-compressor/compressor/models.py b/django-compressor/compressor/models.py
new file mode 100644 (file)
index 0000000..ff4f420
--- /dev/null
@@ -0,0 +1 @@
+from compressor.conf import CompressorConf  # noqa
diff --git a/django-compressor/compressor/offline/__init__.py b/django-compressor/compressor/offline/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/django-compressor/compressor/offline/django.py b/django-compressor/compressor/offline/django.py
new file mode 100644 (file)
index 0000000..6541471
--- /dev/null
@@ -0,0 +1,143 @@
+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
diff --git a/django-compressor/compressor/offline/jinja2.py b/django-compressor/compressor/offline/jinja2.py
new file mode 100644 (file)
index 0000000..feee818
--- /dev/null
@@ -0,0 +1,125 @@
+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
diff --git a/django-compressor/compressor/parser/__init__.py b/django-compressor/compressor/parser/__init__.py
new file mode 100644 (file)
index 0000000..a3fe78f
--- /dev/null
@@ -0,0 +1,34 @@
+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
diff --git a/django-compressor/compressor/parser/base.py b/django-compressor/compressor/parser/base.py
new file mode 100644 (file)
index 0000000..8bf4dd2
--- /dev/null
@@ -0,0 +1,42 @@
+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
diff --git a/django-compressor/compressor/parser/beautifulsoup.py b/django-compressor/compressor/parser/beautifulsoup.py
new file mode 100644 (file)
index 0000000..d143df4
--- /dev/null
@@ -0,0 +1,48 @@
+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)
diff --git a/django-compressor/compressor/parser/default_htmlparser.py b/django-compressor/compressor/parser/default_htmlparser.py
new file mode 100644 (file)
index 0000000..80272cb
--- /dev/null
@@ -0,0 +1,79 @@
+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
diff --git a/django-compressor/compressor/parser/html5lib.py b/django-compressor/compressor/parser/html5lib.py
new file mode 100644 (file)
index 0000000..b1d0948
--- /dev/null
@@ -0,0 +1,59 @@
+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))
diff --git a/django-compressor/compressor/parser/lxml.py b/django-compressor/compressor/parser/lxml.py
new file mode 100644 (file)
index 0000000..64a8fcb
--- /dev/null
@@ -0,0 +1,81 @@
+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
diff --git a/django-compressor/compressor/signals.py b/django-compressor/compressor/signals.py
new file mode 100644 (file)
index 0000000..b6632c7
--- /dev/null
@@ -0,0 +1,4 @@
+import django.dispatch
+
+
+post_compress = django.dispatch.Signal(providing_args=['type', 'mode', 'context'])
diff --git a/django-compressor/compressor/storage.py b/django-compressor/compressor/storage.py
new file mode 100644 (file)
index 0000000..16419a8
--- /dev/null
@@ -0,0 +1,96 @@
+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()
diff --git a/django-compressor/compressor/templates/compressor/css_file.html b/django-compressor/compressor/templates/compressor/css_file.html
new file mode 100644 (file)
index 0000000..2b3a86f
--- /dev/null
@@ -0,0 +1 @@
+<link rel="stylesheet" href="{{ compressed.url }}" type="text/css"{% if compressed.media %} media="{{ compressed.media }}"{% endif %} />
\ No newline at end of file
diff --git a/django-compressor/compressor/templates/compressor/css_inline.html b/django-compressor/compressor/templates/compressor/css_inline.html
new file mode 100644 (file)
index 0000000..86c3d8f
--- /dev/null
@@ -0,0 +1 @@
+<style type="text/css"{% if compressed.media %} media="{{ compressed.media }}"{% endif %}>{{ compressed.content|safe }}</style>
\ No newline at end of file
diff --git a/django-compressor/compressor/templates/compressor/js_file.html b/django-compressor/compressor/templates/compressor/js_file.html
new file mode 100644 (file)
index 0000000..09d6a9b
--- /dev/null
@@ -0,0 +1 @@
+<script type="text/javascript" src="{{ compressed.url }}"></script>
\ No newline at end of file
diff --git a/django-compressor/compressor/templates/compressor/js_inline.html b/django-compressor/compressor/templates/compressor/js_inline.html
new file mode 100644 (file)
index 0000000..403bec5
--- /dev/null
@@ -0,0 +1 @@
+<script type="text/javascript">{{ compressed.content|safe }}</script>
\ No newline at end of file
diff --git a/django-compressor/compressor/templatetags/__init__.py b/django-compressor/compressor/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/django-compressor/compressor/templatetags/compress.py b/django-compressor/compressor/templatetags/compress.py
new file mode 100644 (file)
index 0000000..a45f454
--- /dev/null
@@ -0,0 +1,214 @@
+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)
diff --git a/django-compressor/compressor/test_settings.py b/django-compressor/compressor/test_settings.py
new file mode 100644 (file)
index 0000000..a5abf92
--- /dev/null
@@ -0,0 +1,40 @@
+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',
+)
diff --git a/django-compressor/compressor/tests/__init__.py b/django-compressor/compressor/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/django-compressor/compressor/tests/precompiler.py b/django-compressor/compressor/tests/precompiler.py
new file mode 100644 (file)
index 0000000..059a322
--- /dev/null
@@ -0,0 +1,35 @@
+#!/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()
diff --git a/django-compressor/compressor/tests/static/css/datauri.css b/django-compressor/compressor/tests/static/css/datauri.css
new file mode 100644 (file)
index 0000000..756276b
--- /dev/null
@@ -0,0 +1,4 @@
+.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=="); }
diff --git a/django-compressor/compressor/tests/static/css/nonasc.css b/django-compressor/compressor/tests/static/css/nonasc.css
new file mode 100644 (file)
index 0000000..43159ab
--- /dev/null
@@ -0,0 +1 @@
+.byline:before { content: " —  "; }
\ No newline at end of file
diff --git a/django-compressor/compressor/tests/static/css/one.css b/django-compressor/compressor/tests/static/css/one.css
new file mode 100644 (file)
index 0000000..769b83f
--- /dev/null
@@ -0,0 +1 @@
+body { background:#990; }
\ No newline at end of file
diff --git a/django-compressor/compressor/tests/static/css/two.css b/django-compressor/compressor/tests/static/css/two.css
new file mode 100644 (file)
index 0000000..b73f594
--- /dev/null
@@ -0,0 +1 @@
+body { color:#fff; }
\ No newline at end of file
diff --git a/django-compressor/compressor/tests/static/css/url/2/url2.css b/django-compressor/compressor/tests/static/css/url/2/url2.css
new file mode 100644 (file)
index 0000000..45686ca
--- /dev/null
@@ -0,0 +1,5 @@
+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'); }
diff --git a/django-compressor/compressor/tests/static/css/url/nonasc.css b/django-compressor/compressor/tests/static/css/url/nonasc.css
new file mode 100644 (file)
index 0000000..2afa456
--- /dev/null
@@ -0,0 +1,2 @@
+p { background: url( '../../images/test.png' ); }
+.byline:before { content: " —  "; }
\ No newline at end of file
diff --git a/django-compressor/compressor/tests/static/css/url/test.css b/django-compressor/compressor/tests/static/css/url/test.css
new file mode 100644 (file)
index 0000000..0d4a22b
--- /dev/null
@@ -0,0 +1 @@
+p { background: url('/static/images/image.gif') }
\ No newline at end of file
diff --git a/django-compressor/compressor/tests/static/css/url/url1.css b/django-compressor/compressor/tests/static/css/url/url1.css
new file mode 100644 (file)
index 0000000..609c111
--- /dev/null
@@ -0,0 +1,5 @@
+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'); }
diff --git a/django-compressor/compressor/tests/static/img/add.png b/django-compressor/compressor/tests/static/img/add.png
new file mode 100644 (file)
index 0000000..6332fef
Binary files /dev/null and b/django-compressor/compressor/tests/static/img/add.png differ
diff --git a/django-compressor/compressor/tests/static/img/python.png b/django-compressor/compressor/tests/static/img/python.png
new file mode 100644 (file)
index 0000000..738f6ed
Binary files /dev/null and b/django-compressor/compressor/tests/static/img/python.png differ
diff --git a/django-compressor/compressor/tests/static/js/nonasc-latin1.js b/django-compressor/compressor/tests/static/js/nonasc-latin1.js
new file mode 100644 (file)
index 0000000..109aa20
--- /dev/null
@@ -0,0 +1 @@
+var test_value = "Überstríng";
diff --git a/django-compressor/compressor/tests/static/js/nonasc.js b/django-compressor/compressor/tests/static/js/nonasc.js
new file mode 100644 (file)
index 0000000..838a628
--- /dev/null
@@ -0,0 +1 @@
+var test_value = "—";
diff --git a/django-compressor/compressor/tests/static/js/one.coffee b/django-compressor/compressor/tests/static/js/one.coffee
new file mode 100644 (file)
index 0000000..57bf896
--- /dev/null
@@ -0,0 +1 @@
+# this is a comment.
diff --git a/django-compressor/compressor/tests/static/js/one.js b/django-compressor/compressor/tests/static/js/one.js
new file mode 100644 (file)
index 0000000..b7d2a00
--- /dev/null
@@ -0,0 +1 @@
+obj = {};
\ No newline at end of file
diff --git a/django-compressor/compressor/tests/test_base.py b/django-compressor/compressor/tests/test_base.py
new file mode 100644 (file)
index 0000000..46b1d91
--- /dev/null
@@ -0,0 +1,270 @@
+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)
diff --git a/django-compressor/compressor/tests/test_filters.py b/django-compressor/compressor/tests/test_filters.py
new file mode 100644 (file)
index 0000000..b656a65
--- /dev/null
@@ -0,0 +1,303 @@
+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())
diff --git a/django-compressor/compressor/tests/test_jinja2ext.py b/django-compressor/compressor/tests/test_jinja2ext.py
new file mode 100644 (file)
index 0000000..5adc8ee
--- /dev/null
@@ -0,0 +1,156 @@
+# -*- 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))
diff --git a/django-compressor/compressor/tests/test_offline.py b/django-compressor/compressor/tests/test_offline.py
new file mode 100644 (file)
index 0000000..327b901
--- /dev/null
@@ -0,0 +1,499 @@
+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
diff --git a/django-compressor/compressor/tests/test_parsers.py b/django-compressor/compressor/tests/test_parsers.py
new file mode 100644 (file)
index 0000000..d9b4dd6
--- /dev/null
@@ -0,0 +1,125 @@
+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'
diff --git a/django-compressor/compressor/tests/test_signals.py b/django-compressor/compressor/tests/test_signals.py
new file mode 100644 (file)
index 0000000..13d5eed
--- /dev/null
@@ -0,0 +1,68 @@
+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)
diff --git a/django-compressor/compressor/tests/test_storages.py b/django-compressor/compressor/tests/test_storages.py
new file mode 100644 (file)
index 0000000..91a36f2
--- /dev/null
@@ -0,0 +1,64 @@
+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
diff --git a/django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/basic/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..7deba32
--- /dev/null
@@ -0,0 +1,8 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("Basic test");
+    </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super/base.html b/django-compressor/compressor/tests/test_templates/test_block_super/base.html
new file mode 100644 (file)
index 0000000..e9ca3ad
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..ee9270a
--- /dev/null
@@ -0,0 +1,13 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base.html
new file mode 100644 (file)
index 0000000..481ff40
--- /dev/null
@@ -0,0 +1,10 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+{% block js %}
+    <script type="text/javascript">
+        alert("test using multiple inheritance and block.super");
+    </script>
+{% endblock %}
+{% endcompress %}
+{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/base2.html
new file mode 100644 (file)
index 0000000..abd074d
--- /dev/null
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+    {{ block.super }}
+    <script type="text/javascript">
+        alert("this alert should be included");
+    </script>
+{% endspaceless %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..01382ec
--- /dev/null
@@ -0,0 +1,8 @@
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+    {{ block.super }}
+    <script type="text/javascript">
+        alert("this alert shouldn't be alone!");
+    </script>
+{% endspaceless %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_extra/base.html
new file mode 100644 (file)
index 0000000..e9ca3ad
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_extra/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..2293065
--- /dev/null
@@ -0,0 +1,19 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base.html
new file mode 100644 (file)
index 0000000..c9ee6cc
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/base2.html
new file mode 100644 (file)
index 0000000..c781fb5
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+    {{ block.super }}
+    <script type="text/javascript">
+        alert("this alert should be included");
+    </script>
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..a05a7b7
--- /dev/null
@@ -0,0 +1,11 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base.html
new file mode 100644 (file)
index 0000000..c9ee6cc
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/base2.html
new file mode 100644 (file)
index 0000000..b0b2fef
--- /dev/null
@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_block_super_multiple_cached/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..a05a7b7
--- /dev/null
@@ -0,0 +1,11 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_complex/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..6eea06e
--- /dev/null
@@ -0,0 +1,20 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_condition/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..4b0223c
--- /dev/null
@@ -0,0 +1,7 @@
+{% load compress %}{% spaceless %}
+
+{% if condition %}
+    {% compress js%}
+        <script type="text/javascript">alert("{{ condition|default:"yellow" }}");</script>
+    {% endcompress %}
+{% endif %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..6050c8b
--- /dev/null
@@ -0,0 +1,13 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html b/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_extends.html
new file mode 100644 (file)
index 0000000..dede1ce
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends "buggy_template.html" %}
+{% load compress %}
+
+{% compress css %}
+    <style type="text/css">
+        body {
+            background: orange;
+        }
+    </style>
+{% endcompress %}
diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html b/django-compressor/compressor/tests/test_templates/test_error_handling/buggy_template.html
new file mode 100644 (file)
index 0000000..1a99dab
--- /dev/null
@@ -0,0 +1,12 @@
+{% load compress %}
+
+{% compress css %}
+    <style type="text/css">
+        body {
+            background: pink;
+        }
+    </style>
+{% endcompress %}
+
+
+{% fail %}
diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html b/django-compressor/compressor/tests/test_templates/test_error_handling/missing_extends.html
new file mode 100644 (file)
index 0000000..588ba8a
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends "missing.html" %}
+{% load compress %}
+
+{% compress css %}
+    <style type="text/css">
+        body {
+            background: purple;
+        }
+    </style>
+{% endcompress %}
diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_error_handling/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..a0b3c79
--- /dev/null
@@ -0,0 +1,8 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("Basic test, should pass in spite of errors in other templates");
+    </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html b/django-compressor/compressor/tests/test_templates/test_error_handling/with_coffeescript.html
new file mode 100644 (file)
index 0000000..6e56c8a
--- /dev/null
@@ -0,0 +1,7 @@
+{% load compress %}
+
+{% compress js %}
+    <script type="text/coffeescript" charset="utf-8">
+      a = 1
+    </script>
+{% endcompress %}
diff --git a/django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_inline_non_ascii/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..a4d2bc5
--- /dev/null
@@ -0,0 +1,7 @@
+{% load compress %}{% spaceless %}
+
+{% compress js inline %}
+    <script type="text/javascript">
+        var value = '{{ test_non_ascii_value }}';
+    </script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_static_templatetag/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..8e17d32
--- /dev/null
@@ -0,0 +1,6 @@
+{% load compress static %}{% spaceless %}
+
+{% compress js %}
+    <script>alert('amazing');</script>
+    <script type="text/javascript" src="{% static "js/one.js" %}"></script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_templatetag/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..868f188
--- /dev/null
@@ -0,0 +1,7 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("{% firstof "testtemplatetag" %}");
+    </script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates/test_with_context/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..4970747
--- /dev/null
@@ -0,0 +1,7 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("{{ content|default:"Ooops!" }}");
+    </script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..6e89ed2
--- /dev/null
@@ -0,0 +1,8 @@
+{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("Basic test");
+    </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/base.html
new file mode 100644 (file)
index 0000000..e9ca3ad
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..e1fabd8
--- /dev/null
@@ -0,0 +1,12 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html
new file mode 100644 (file)
index 0000000..e9ca3ad
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..328ccb9
--- /dev/null
@@ -0,0 +1,18 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html
new file mode 100644 (file)
index 0000000..c9ee6cc
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html
new file mode 100644 (file)
index 0000000..b0b2fef
--- /dev/null
@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..accd76d
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+       {% compress js %}
+           {{ super() }}
+           <script type="text/javascript">
+               alert("this alert shouldn't be alone!");
+           </script>
+       {% endcompress %}
+{% endspaceless %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html
new file mode 100644 (file)
index 0000000..c9ee6cc
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html
new file mode 100644 (file)
index 0000000..b0b2fef
--- /dev/null
@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..accd76d
--- /dev/null
@@ -0,0 +1,10 @@
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+       {% compress js %}
+           {{ super() }}
+           <script type="text/javascript">
+               alert("this alert shouldn't be alone!");
+           </script>
+       {% endcompress %}
+{% endspaceless %}{% endblock %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..511ddd0
--- /dev/null
@@ -0,0 +1,11 @@
+{%- 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..4707182
--- /dev/null
@@ -0,0 +1,24 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..bd1adb8
--- /dev/null
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% if condition %}
+    {% compress js%}
+        <script type="text/javascript">alert("{{ condition|default("yellow") }}");</script>
+    {% endcompress %}
+{% endif %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html
new file mode 100644 (file)
index 0000000..72513f7
--- /dev/null
@@ -0,0 +1,9 @@
+{% extends "buggy_template.html" %}
+
+{% compress css %}
+    <style type="text/css">
+        body {
+            background: orange;
+        }
+    </style>
+{% endcompress %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html
new file mode 100644 (file)
index 0000000..a01b899
--- /dev/null
@@ -0,0 +1,10 @@
+{% compress css %}
+    <style type="text/css">
+        body {
+            background: pink;
+        }
+    </style>
+{% endcompress %}
+
+
+{% fail %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html
new file mode 100644 (file)
index 0000000..dc76034
--- /dev/null
@@ -0,0 +1,9 @@
+{% extends "missing.html" %}
+
+{% compress css %}
+    <style type="text/css">
+        body {
+            background: purple;
+        }
+    </style>
+{% endcompress %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..3ecffa5
--- /dev/null
@@ -0,0 +1,8 @@
+{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("Basic test, should pass in spite of errors in other templates");
+    </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html b/django-compressor/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html
new file mode 100644 (file)
index 0000000..8a53e44
--- /dev/null
@@ -0,0 +1,5 @@
+{% compress js %}
+    <script type="text/coffeescript" charset="utf-8">
+      a = 1
+    </script>
+{% endcompress %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..c03b191
--- /dev/null
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% compress js, inline %}
+    <script type="text/javascript">
+        var value = '{{ test_non_ascii_value }}';
+    </script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..d79c797
--- /dev/null
@@ -0,0 +1,11 @@
+{% 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 %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..ed7238c
--- /dev/null
@@ -0,0 +1,6 @@
+{% spaceless %}
+
+{% compress js %}
+    <script>alert('amazing');</script>
+    <script type="text/javascript" src="{{ url_for('static', filename="js/one.js") }}"></script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..31c5d17
--- /dev/null
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("{{ "testtemplateTAG"|lower }}");
+    </script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html b/django-compressor/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..2289a5f
--- /dev/null
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% compress js %}
+    <script type="text/javascript">
+        alert("{{ content|default("Ooops!") }}");
+    </script>
+{% endcompress %}{% endspaceless %}
diff --git a/django-compressor/compressor/tests/test_templatetags.py b/django-compressor/compressor/tests/test_templatetags.py
new file mode 100644 (file)
index 0000000..db0d1b7
--- /dev/null
@@ -0,0 +1,256 @@
+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
diff --git a/django-compressor/compressor/utils/__init__.py b/django-compressor/compressor/utils/__init__.py
new file mode 100644 (file)
index 0000000..1c3479b
--- /dev/null
@@ -0,0 +1,73 @@
+# -*- 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
diff --git a/django-compressor/compressor/utils/decorators.py b/django-compressor/compressor/utils/decorators.py
new file mode 100644 (file)
index 0000000..f96c929
--- /dev/null
@@ -0,0 +1,64 @@
+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)
diff --git a/django-compressor/compressor/utils/staticfiles.py b/django-compressor/compressor/utils/staticfiles.py
new file mode 100644 (file)
index 0000000..28026f2
--- /dev/null
@@ -0,0 +1,27 @@
+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
diff --git a/django-compressor/compressor/utils/stringformat.py b/django-compressor/compressor/utils/stringformat.py
new file mode 100644 (file)
index 0000000..9311e78
--- /dev/null
@@ -0,0 +1,260 @@
+# -*- 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()
diff --git a/django-compressor/django_compressor.egg-info/PKG-INFO b/django-compressor/django_compressor.egg-info/PKG-INFO
new file mode 100644 (file)
index 0000000..19e5fea
--- /dev/null
@@ -0,0 +1,104 @@
+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
diff --git a/django-compressor/django_compressor.egg-info/SOURCES.txt b/django-compressor/django_compressor.egg-info/SOURCES.txt
new file mode 100644 (file)
index 0000000..445ff9c
--- /dev/null
@@ -0,0 +1,156 @@
+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
diff --git a/django-compressor/django_compressor.egg-info/dependency_links.txt b/django-compressor/django_compressor.egg-info/dependency_links.txt
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/django-compressor/django_compressor.egg-info/not-zip-safe b/django-compressor/django_compressor.egg-info/not-zip-safe
new file mode 100644 (file)
index 0000000..8b13789
--- /dev/null
@@ -0,0 +1 @@
+
diff --git a/django-compressor/django_compressor.egg-info/requires.txt b/django-compressor/django_compressor.egg-info/requires.txt
new file mode 100644 (file)
index 0000000..0868509
--- /dev/null
@@ -0,0 +1 @@
+django-appconf >= 0.4
\ No newline at end of file
diff --git a/django-compressor/django_compressor.egg-info/top_level.txt b/django-compressor/django_compressor.egg-info/top_level.txt
new file mode 100644 (file)
index 0000000..7b710e8
--- /dev/null
@@ -0,0 +1 @@
+compressor
diff --git a/django-compressor/docs/Makefile b/django-compressor/docs/Makefile
new file mode 100644 (file)
index 0000000..e4de9f8
--- /dev/null
@@ -0,0 +1,130 @@
+# 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."
diff --git a/django-compressor/docs/behind-the-scenes.txt b/django-compressor/docs/behind-the-scenes.txt
new file mode 100644 (file)
index 0000000..0cd2a3c
--- /dev/null
@@ -0,0 +1,56 @@
+.. _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!
diff --git a/django-compressor/docs/changelog.txt b/django-compressor/docs/changelog.txt
new file mode 100644 (file)
index 0000000..3828197
--- /dev/null
@@ -0,0 +1,398 @@
+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/
+
diff --git a/django-compressor/docs/conf.py b/django-compressor/docs/conf.py
new file mode 100644 (file)
index 0000000..34552c3
--- /dev/null
@@ -0,0 +1,222 @@
+# -*- 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)
+]
diff --git a/django-compressor/docs/contributing.txt b/django-compressor/docs/contributing.txt
new file mode 100644 (file)
index 0000000..225a1ae
--- /dev/null
@@ -0,0 +1,174 @@
+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/
diff --git a/django-compressor/docs/django-sekizai.txt b/django-compressor/docs/django-sekizai.txt
new file mode 100644 (file)
index 0000000..6fd80c9
--- /dev/null
@@ -0,0 +1,24 @@
+.. _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/
diff --git a/django-compressor/docs/index.txt b/django-compressor/docs/index.txt
new file mode 100644 (file)
index 0000000..f1adfa4
--- /dev/null
@@ -0,0 +1,47 @@
+=================
+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
diff --git a/django-compressor/docs/jinja2.txt b/django-compressor/docs/jinja2.txt
new file mode 100644 (file)
index 0000000..134b0b8
--- /dev/null
@@ -0,0 +1,175 @@
+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/
+
diff --git a/django-compressor/docs/make.bat b/django-compressor/docs/make.bat
new file mode 100644 (file)
index 0000000..aa5c189
--- /dev/null
@@ -0,0 +1,170 @@
+@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
diff --git a/django-compressor/docs/quickstart.txt b/django-compressor/docs/quickstart.txt
new file mode 100644 (file)
index 0000000..4acfab2
--- /dev/null
@@ -0,0 +1,100 @@
+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/
diff --git a/django-compressor/docs/remote-storages.txt b/django-compressor/docs/remote-storages.txt
new file mode 100644 (file)
index 0000000..91e7c2e
--- /dev/null
@@ -0,0 +1,91 @@
+.. _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
diff --git a/django-compressor/docs/scenarios.txt b/django-compressor/docs/scenarios.txt
new file mode 100644 (file)
index 0000000..5f86fd7
--- /dev/null
@@ -0,0 +1,67 @@
+.. _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.
diff --git a/django-compressor/docs/settings.txt b/django-compressor/docs/settings.txt
new file mode 100644 (file)
index 0000000..0d3fd72
--- /dev/null
@@ -0,0 +1,465 @@
+.. _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.
diff --git a/django-compressor/docs/usage.txt b/django-compressor/docs/usage.txt
new file mode 100644 (file)
index 0000000..3e18a8f
--- /dev/null
@@ -0,0 +1,234 @@
+.. _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.
diff --git a/django-compressor/requirements/tests.txt b/django-compressor/requirements/tests.txt
new file mode 100644 (file)
index 0000000..775874f
--- /dev/null
@@ -0,0 +1,10 @@
+flake8
+coverage
+html5lib
+mock
+jinja2
+lxml
+BeautifulSoup
+unittest2
+coffin
+jingo
diff --git a/django-compressor/setup.cfg b/django-compressor/setup.cfg
new file mode 100644 (file)
index 0000000..6c71b61
--- /dev/null
@@ -0,0 +1,8 @@
+[wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/django-compressor/setup.py b/django-compressor/setup.py
new file mode 100644 (file)
index 0000000..81f5dde
--- /dev/null
@@ -0,0 +1,145 @@
+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',
+    ],
+)
diff --git a/django-compressor/tox.ini b/django-compressor/tox.ini
new file mode 100644 (file)
index 0000000..1aa5e81
--- /dev/null
@@ -0,0 +1,121 @@
+[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}