commit 53e3d58d02df6c5ff6c0cfe39085c07c23ab7b73 Author: xian Date: Fri Apr 24 16:02:20 2009 -0500 Initial commit (includes bits from django-compress) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2d16b63 --- /dev/null +++ b/LICENSE @@ -0,0 +1,77 @@ +django_compressor +--------------- +Copyright (c) 2009 Christian Metts + +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 + +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. + + +jsmin.py (License-information from the file) +-------------------------------------------- +This code is original from jsmin by Douglas Crockford, it was translated to +Python by Baruch Even. The original code had the following copyright and +license. + +/* jsmin.c + 2007-05-22 + +Copyright (c) 2002 Douglas Crockford (www.crockford.com) + +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 shall be used for Good, not Evil. + +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. +*/ diff --git a/compressor/__init__.py b/compressor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compressor/conf/__init__.py b/compressor/conf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compressor/conf/settings.py b/compressor/conf/settings.py new file mode 100644 index 0000000..a106cb4 --- /dev/null +++ b/compressor/conf/settings.py @@ -0,0 +1,17 @@ +from django.core.exceptions import ImproperlyConfigured +from django.conf import settings + + +MEDIA_URL = getattr(settings, 'COMPRESS_URL', settings.MEDIA_URL) +MEDIA_ROOT = getattr(settings, 'COMPRESS_ROOT', settings.MEDIA_ROOT) +PREFIX = getattr(settings, 'COMPRESS_PREFIX', 'compressed') + +COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG) +COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', []) +COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compressor.filters.jsmin.JSMinFilter']) + +if COMPRESS_CSS_FILTERS is None: + COMPRESS_CSS_FILTERS = [] + +if COMPRESS_JS_FILTERS is None: + COMPRESS_JS_FILTERS = [] diff --git a/compressor/filters/__init__.py b/compressor/filters/__init__.py new file mode 100644 index 0000000..9b98531 --- /dev/null +++ b/compressor/filters/__init__.py @@ -0,0 +1,14 @@ +class FilterBase: + def __init__(self, verbose): + self.verbose = verbose + + def filter_css(self, css): + raise NotImplementedError + def filter_js(self, js): + raise NotImplementedError + +class FilterError(Exception): + """ + This exception is raised when a filter fails + """ + pass \ No newline at end of file diff --git a/compressor/filters/csstidy/__init__.py b/compressor/filters/csstidy/__init__.py new file mode 100644 index 0000000..508d6bc --- /dev/null +++ b/compressor/filters/csstidy/__init__.py @@ -0,0 +1,33 @@ +import os +import warnings +import tempfile + +from django.conf import settings + +from compressor.filters import FilterBase + +BINARY = getattr(settings, 'CSSTIDY_BINARY', 'csstidy') +ARGUMENTS = getattr(settings, 'CSSTIDY_ARGUMENTS', '--template=highest') + +warnings.simplefilter('ignore', RuntimeWarning) + +class CSSTidyFilter(FilterBase): + def filter_css(self, css): + tmp_file = tempfile.NamedTemporaryFile(mode='w+b') + tmp_file.write(css) + tmp_file.flush() + + output_file = tempfile.NamedTemporaryFile(mode='w+b') + + command = '%s %s %s %s' % (BINARY, tmp_file.name, ARGUMENTS, output_file.name) + + command_output = os.popen(command).read() + + filtered_css = output_file.read() + output_file.close() + tmp_file.close() + + if self.verbose: + print command_output + + return filtered_css diff --git a/compressor/filters/jsmin/__init__.py b/compressor/filters/jsmin/__init__.py new file mode 100644 index 0000000..93ca6bf --- /dev/null +++ b/compressor/filters/jsmin/__init__.py @@ -0,0 +1,6 @@ +from compress.filters.jsmin.jsmin import jsmin +from compressor.filters import FilterBase + +class JSMinFilter(FilterBase): + def filter_js(self, js): + return jsmin(js) \ No newline at end of file diff --git a/compressor/filters/jsmin/jsmin.py b/compressor/filters/jsmin/jsmin.py new file mode 100644 index 0000000..4f9d384 --- /dev/null +++ b/compressor/filters/jsmin/jsmin.py @@ -0,0 +1,218 @@ +#!/usr/bin/python + +# This code is original from jsmin by Douglas Crockford, it was translated to +# Python by Baruch Even. The original code had the following copyright and +# license. +# +# /* jsmin.c +# 2007-05-22 +# +# Copyright (c) 2002 Douglas Crockford (www.crockford.com) +# +# 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 shall be used for Good, not Evil. +# +# 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. +# */ + +from StringIO import StringIO + +def jsmin(js): + ins = StringIO(js) + outs = StringIO() + JavascriptMinify().minify(ins, outs) + str = outs.getvalue() + if len(str) > 0 and str[0] == '\n': + str = str[1:] + return str + +def isAlphanum(c): + """return true if the character is a letter, digit, underscore, + dollar sign, or non-ASCII character. + """ + return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or + (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126)); + +class UnterminatedComment(Exception): + pass + +class UnterminatedStringLiteral(Exception): + pass + +class UnterminatedRegularExpression(Exception): + pass + +class JavascriptMinify(object): + + def _outA(self): + self.outstream.write(self.theA) + def _outB(self): + self.outstream.write(self.theB) + + def _get(self): + """return the next character from stdin. Watch out for lookahead. If + the character is a control character, translate it to a space or + linefeed. + """ + c = self.theLookahead + self.theLookahead = None + if c == None: + c = self.instream.read(1) + if c >= ' ' or c == '\n': + return c + if c == '': # EOF + return '\000' + if c == '\r': + return '\n' + return ' ' + + def _peek(self): + self.theLookahead = self._get() + return self.theLookahead + + def _next(self): + """get the next character, excluding comments. peek() is used to see + if a '/' is followed by a '/' or '*'. + """ + c = self._get() + if c == '/': + p = self._peek() + if p == '/': + c = self._get() + while c > '\n': + c = self._get() + return c + if p == '*': + c = self._get() + while 1: + c = self._get() + if c == '*': + if self._peek() == '/': + self._get() + return ' ' + if c == '\000': + raise UnterminatedComment() + + return c + + def _action(self, action): + """do something! What you do is determined by the argument: + 1 Output A. Copy B to A. Get the next B. + 2 Copy B to A. Get the next B. (Delete A). + 3 Get the next B. (Delete B). + action treats a string as a single character. Wow! + action recognizes a regular expression if it is preceded by ( or , or =. + """ + if action <= 1: + self._outA() + + if action <= 2: + self.theA = self.theB + if self.theA == "'" or self.theA == '"': + while 1: + self._outA() + self.theA = self._get() + if self.theA == self.theB: + break + if self.theA <= '\n': + raise UnterminatedStringLiteral() + if self.theA == '\\': + self._outA() + self.theA = self._get() + + + if action <= 3: + self.theB = self._next() + if self.theB == '/' and (self.theA == '(' or self.theA == ',' or + self.theA == '=' or self.theA == ':' or + self.theA == '[' or self.theA == '?' or + self.theA == '!' or self.theA == '&' or + self.theA == '|' or self.theA == ';' or + self.theA == '{' or self.theA == '}' or + self.theA == '\n'): + self._outA() + self._outB() + while 1: + self.theA = self._get() + if self.theA == '/': + break + elif self.theA == '\\': + self._outA() + self.theA = self._get() + elif self.theA <= '\n': + raise UnterminatedRegularExpression() + self._outA() + self.theB = self._next() + + + def _jsmin(self): + """Copy the input to the output, deleting the characters which are + insignificant to JavaScript. Comments will be removed. Tabs will be + replaced with spaces. Carriage returns will be replaced with linefeeds. + Most spaces and linefeeds will be removed. + """ + self.theA = '\n' + self._action(3) + + while self.theA != '\000': + if self.theA == ' ': + if isAlphanum(self.theB): + self._action(1) + else: + self._action(2) + elif self.theA == '\n': + if self.theB in ['{', '[', '(', '+', '-']: + self._action(1) + elif self.theB == ' ': + self._action(3) + else: + if isAlphanum(self.theB): + self._action(1) + else: + self._action(2) + else: + if self.theB == ' ': + if isAlphanum(self.theA): + self._action(1) + else: + self._action(3) + elif self.theB == '\n': + if self.theA in ['}', ']', ')', '+', '-', '"', '\'']: + self._action(1) + else: + if isAlphanum(self.theA): + self._action(1) + else: + self._action(3) + else: + self._action(1) + + def minify(self, instream, outstream): + self.instream = instream + self.outstream = outstream + self.theA = '\n' + self.theB = None + self.theLookahead = None + + self._jsmin() + self.instream.close() + +if __name__ == '__main__': + import sys + jsm = JavascriptMinify() + jsm.minify(sys.stdin, sys.stdout) \ No newline at end of file diff --git a/compressor/filters/yui/__init__.py b/compressor/filters/yui/__init__.py new file mode 100644 index 0000000..9e2a946 --- /dev/null +++ b/compressor/filters/yui/__init__.py @@ -0,0 +1,44 @@ +import subprocess + +from django.conf import settings + +from compressor.filters import FilterBase, FilterError + +BINARY = getattr(settings, 'COMPRESS_YUI_BINARY', 'java -jar yuicompressor.jar') +CSS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_CSS_ARGUMENTS', '') +JS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_JS_ARGUMENTS', '') + +class YUICompressorFilter(FilterBase): + + def filter_common(self, content, type_, arguments): + command = '%s --type=%s %s' % (BINARY, type_, arguments) + + if self.verbose: + command += ' --verbose' + + p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + p.stdin.write(content) + p.stdin.close() + + filtered_css = p.stdout.read() + p.stdout.close() + + err = p.stderr.read() + p.stderr.close() + + if p.wait() != 0: + if not err: + err = 'Unable to apply YUI Compressor filter' + + raise FilterError(err) + + if self.verbose: + print err + + return filtered_css + + def filter_js(self, js): + return self.filter_common(js, 'js', JS_ARGUMENTS) + + def filter_css(self, css): + return self.filter_common(css, 'css', CSS_ARGUMENTS) \ No newline at end of file diff --git a/compressor/templates/compressor/css.html b/compressor/templates/compressor/css.html new file mode 100644 index 0000000..3751a4a --- /dev/null +++ b/compressor/templates/compressor/css.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/compressor/templates/compressor/js.html b/compressor/templates/compressor/js.html new file mode 100644 index 0000000..8419c20 --- /dev/null +++ b/compressor/templates/compressor/js.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/compressor/templatetags/__init__.py b/compressor/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/compressor/templatetags/compress.py b/compressor/templatetags/compress.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/models.py b/tests/core/models.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/manage.py b/tests/manage.py new file mode 100755 index 0000000..5a3afa9 --- /dev/null +++ b/tests/manage.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import sys + +# Give tests/manage.py access to django-compress +from os.path import dirname, abspath +sys.path += [dirname(dirname(abspath(__file__)))] + +try: + import settings # Assumed to be in the same directory. +except ImportError: + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) + sys.exit(1) + +if __name__ == "__main__": + execute_manager(settings) diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..22fcc55 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,20 @@ +import sys +from os.path import dirname, abspath, join +TEST_DIR = [dirname(abspath(__file__))] + +ROOT_URLCONF = 'urls' + +DATABASE_ENGINE = 'sqlite3' +DATABASE_NAME = 'django_inlines_tests.db' + +INSTALLED_APPS = [ + 'core', + 'django-compress', +] +TEMPLATE_LOADERS = ( + 'django.template.loaders.app_directories.load_template_source', +) + +TEMPLATE_DIRS = ( + join (TEST_DIR, 'templates'), +) \ No newline at end of file diff --git a/tests/urls.py b/tests/urls.py new file mode 100644 index 0000000..e69de29