diff --git a/README.rst b/README.rst
index 2b8decb..7288fbc 100644
--- a/README.rst
+++ b/README.rst
@@ -148,6 +148,19 @@ A list of filters that will be applied to javascript.
The dotted path to a Django Storage backend to be used to save the
compressed files.
+``COMPRESS_PARSER``
+--------------------
+
+:Default: ``'compressor.parser.BeautifulSoupParser'``
+
+The backend to use when parsing the JavaScript or Stylesheet files.
+The backends included in ``compressor``:
+
+ - ``compressor.parser.BeautifulSoupParser``
+ - ``compressor.parser.LxmlParser``
+
+See `Dependencies`_ for more info about the packages you need for each parser.
+
``COMPRESS_REBUILD_TIMEOUT``
----------------------------
@@ -175,8 +188,21 @@ modification timestamp of a file. Disabled by default. Should be smaller
than ``COMPRESS_REBUILD_TIMEOUT`` and ``COMPRESS_MINT_DELAY``.
-Dependecies
-***********
+Dependencies
+************
-* BeautifulSoup
+* BeautifulSoup_ (for the default ``compressor.parser.BeautifulSoupParser``)
+::
+
+ pip install BeautifulSoup
+
+* lxml_ (for the optional ``compressor.parser.LxmlParser``, requires libxml2_)
+
+::
+
+ STATIC_DEPS=true pip install lxml
+
+.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
+.. _lxml: http://codespeak.net/lxml/
+.. _libxml2: http://xmlsoft.org/
diff --git a/compressor/__init__.py b/compressor/__init__.py
index 592cec7..6a0bd7e 100644
--- a/compressor/__init__.py
+++ b/compressor/__init__.py
@@ -1,235 +1,5 @@
-import os
-from BeautifulSoup import BeautifulSoup
-
-from django import template
-from django.conf import settings as django_settings
-from django.template.loader import render_to_string
-
-from django.core.cache import cache
-from django.core.files.base import ContentFile
-from django.core.files.storage import get_storage_class
-
-from compressor.conf import settings
-from compressor import filters
-
-
-register = template.Library()
-
-
-class UncompressableFileError(Exception):
- pass
-
-
-def get_hexdigest(plaintext):
- try:
- import hashlib
- return hashlib.sha1(plaintext).hexdigest()
- except ImportError:
- import sha
- return sha.new(plaintext).hexdigest()
-
-
-def get_mtime(filename):
- if settings.MTIME_DELAY:
- key = "django_compressor.mtime.%s" % filename
- mtime = cache.get(key)
- if mtime is None:
- mtime = os.path.getmtime(filename)
- cache.set(key, mtime, settings.MTIME_DELAY)
- return mtime
- return os.path.getmtime(filename)
-
-class Compressor(object):
-
- def __init__(self, content, output_prefix="compressed"):
- self.content = content
- self.type = None
- self.output_prefix = output_prefix
- self.split_content = []
-
- def split_contents(self):
- raise NotImplementedError('split_contents must be defined in a subclass')
-
- def get_filename(self, url):
- if not url.startswith(self.storage.base_url):
- raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, self.storage.base_url))
- basename = url.replace(self.storage.base_url, "", 1)
- if not self.storage.exists(basename):
- raise UncompressableFileError('"%s" does not exist' % self.storage.path(basename))
- return self.storage.path(basename)
-
- @property
- def soup(self):
- return BeautifulSoup(self.content)
-
- @property
- def mtimes(self):
- return [get_mtime(h[1]) for h in self.split_contents() if h[0] == 'file']
-
- @property
- def cachekey(self):
- cachebits = [self.content]
- cachebits.extend([str(m) for m in self.mtimes])
- cachestr = "".join(cachebits).encode(django_settings.DEFAULT_CHARSET)
- return "django_compressor.%s" % get_hexdigest(cachestr)[:12]
-
- @property
- def storage(self):
- return get_storage_class(settings.STORAGE)()
-
- @property
- def hunks(self):
- if getattr(self, '_hunks', ''):
- return self._hunks
- self._hunks = []
- for kind, v, elem in self.split_contents():
- if kind == 'hunk':
- input = v
- if self.filters:
- input = self.filter(input, 'input', elem=elem)
- # Let's cast BeautifulSoup element to unicode here since
- # it will try to encode using ascii internally later
- self._hunks.append(unicode(input))
- if kind == 'file':
- # TODO: wrap this in a try/except for IoErrors(?)
- fd = open(v, 'rb')
- input = fd.read()
- if self.filters:
- input = self.filter(input, 'input', filename=v, elem=elem)
- charset = elem.get('charset', django_settings.DEFAULT_CHARSET)
- self._hunks.append(unicode(input, charset))
- fd.close()
- return self._hunks
-
- def concat(self):
- # Design decision needed: either everything should be unicode up to
- # here or we encode strings as soon as we acquire them. Currently
- # concat() expects all hunks to be unicode and does the encoding
- return "\n".join([hunk.encode(django_settings.DEFAULT_CHARSET) for hunk in self.hunks])
-
- def filter(self, content, method, **kwargs):
- for f in self.filters:
- filter = getattr(filters.get_class(f)(content, filter_type=self.type), method)
- try:
- if callable(filter):
- content = filter(**kwargs)
- except NotImplementedError:
- pass
- return content
-
- @property
- def combined(self):
- if getattr(self, '_output', ''):
- return self._output
- output = self.concat()
- if self.filters:
- output = self.filter(output, 'output')
- self._output = output
- return self._output
-
- @property
- def hash(self):
- return get_hexdigest(self.combined)[:12]
-
- @property
- def new_filepath(self):
- filename = "".join([self.hash, self.extension])
- return os.path.join(
- settings.OUTPUT_DIR.strip(os.sep), self.output_prefix, filename)
-
- def save_file(self):
- if self.storage.exists(self.new_filepath):
- return False
- self.storage.save(self.new_filepath, ContentFile(self.combined))
- return True
-
- def output(self):
- if not settings.COMPRESS:
- return self.content
- self.save_file()
- context = getattr(self, 'extra_context', {})
- context['url'] = self.storage.url(self.new_filepath)
- return render_to_string(self.template_name, context)
-
- def output_inline(self):
- context = {'content': settings.COMPRESS and self.combined or self.concat()}
- if hasattr(self, 'extra_context'):
- context.update(self.extra_context)
- return render_to_string(self.template_name_inline, context)
-
-class CssCompressor(Compressor):
-
- def __init__(self, content, output_prefix="css"):
- self.extension = ".css"
- self.template_name = "compressor/css.html"
- self.template_name_inline = "compressor/css_inline.html"
- self.filters = ['compressor.filters.css_default.CssAbsoluteFilter']
- self.filters.extend(settings.COMPRESS_CSS_FILTERS)
- self.type = 'css'
- super(CssCompressor, self).__init__(content, output_prefix)
-
- def split_contents(self):
- if self.split_content:
- return self.split_content
- split = self.soup.findAll({'link' : True, 'style' : True})
- self.media_nodes = []
- for elem in split:
- data = None
- if elem.name == 'link' and elem['rel'] == 'stylesheet':
- try:
- data = ('file', self.get_filename(elem['href']), elem)
- except UncompressableFileError:
- if django_settings.DEBUG:
- raise
- elif elem.name == 'style':
- data = ('hunk', elem.string, elem)
- if data:
- self.split_content.append(data)
- media = elem.get('media', None)
- # Append to the previous node if it had the same media type,
- # otherwise create a new node.
- if self.media_nodes and self.media_nodes[-1][0] == media:
- self.media_nodes[-1][1].split_content.append(data)
- else:
- node = CssCompressor(content='')
- node.split_content.append(data)
- self.media_nodes.append((media, node))
- return self.split_content
-
- def output(self):
- self.split_contents()
- if not hasattr(self, 'media_nodes'):
- return super(CssCompressor, self).output()
- if not settings.COMPRESS:
- return self.content
- ret = []
- for media, subnode in self.media_nodes:
- subnode.extra_context = {'media': media}
- ret.append(subnode.output())
- return ''.join(ret)
-
-
-class JsCompressor(Compressor):
-
- def __init__(self, content, output_prefix="js"):
- self.extension = ".js"
- self.template_name = "compressor/js.html"
- self.template_name_inline = "compressor/js_inline.html"
- self.filters = settings.COMPRESS_JS_FILTERS
- self.type = 'js'
- super(JsCompressor, self).__init__(content, output_prefix)
-
- def split_contents(self):
- if self.split_content:
- return self.split_content
- split = self.soup.findAll('script')
- for elem in split:
- if elem.has_key('src'):
- try:
- self.split_content.append(('file', self.get_filename(elem['src']), elem))
- except UncompressableFileError:
- if django_settings.DEBUG:
- raise
- else:
- self.split_content.append(('hunk', elem.string, elem))
- return self.split_content
+from compressor.base import Compressor
+from compressor.js import JsCompressor
+from compressor.css import CssCompressor
+from compressor.utils import get_hexdigest, get_mtime
+from compressor.exceptions import UncompressableFileError
diff --git a/compressor/base.py b/compressor/base.py
new file mode 100644
index 0000000..a0fe870
--- /dev/null
+++ b/compressor/base.py
@@ -0,0 +1,139 @@
+import os
+
+
+from django.conf import settings as django_settings
+from django.template.loader import render_to_string
+from django.core.files.base import ContentFile
+from django.core.files.storage import get_storage_class
+
+from compressor.conf import settings
+from compressor import filters
+from compressor.exceptions import UncompressableFileError
+from compressor.utils import get_hexdigest, get_mtime, get_class
+
+class Compressor(object):
+
+ def __init__(self, content, output_prefix="compressed"):
+ self.content = content
+ self.type = None
+ self.output_prefix = output_prefix
+ self.split_content = []
+ self._parser = None
+
+ def split_contents(self):
+ raise NotImplementedError('split_contents must be defined in a subclass')
+
+ def get_filename(self, url):
+ if not url.startswith(self.storage.base_url):
+ raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, self.storage.base_url))
+ basename = url.replace(self.storage.base_url, "", 1)
+ if not self.storage.exists(basename):
+ raise UncompressableFileError('"%s" does not exist' % self.storage.path(basename))
+ return self.storage.path(basename)
+
+ def _get_parser(self):
+ if self._parser:
+ return self._parser
+ parser_cls = get_class(settings.PARSER)
+ self._parser = parser_cls(self.content)
+ return self._parser
+
+ def _set_parser(self, parser):
+ self._parser = parser
+ parser = property(_get_parser, _set_parser)
+
+ @property
+ def mtimes(self):
+ return [get_mtime(h[1]) for h in self.split_contents() if h[0] == 'file']
+
+ @property
+ def cachekey(self):
+ cachebits = [self.content]
+ cachebits.extend([str(m) for m in self.mtimes])
+ cachestr = "".join(cachebits).encode(django_settings.DEFAULT_CHARSET)
+ return "django_compressor.%s" % get_hexdigest(cachestr)[:12]
+
+ @property
+ def storage(self):
+ return get_storage_class(settings.STORAGE)()
+
+ @property
+ def hunks(self):
+ if getattr(self, '_hunks', ''):
+ return self._hunks
+ self._hunks = []
+ for kind, v, elem in self.split_contents():
+ attribs = self.parser.elem_attribs(elem)
+ if kind == 'hunk':
+ input = v
+ if self.filters:
+ input = self.filter(input, 'input', elem=elem)
+ # Let's cast BeautifulSoup element to unicode here since
+ # it will try to encode using ascii internally later
+ self._hunks.append(unicode(input))
+ if kind == 'file':
+ # TODO: wrap this in a try/except for IoErrors(?)
+ fd = open(v, 'rb')
+ input = fd.read()
+ if self.filters:
+ input = self.filter(input, 'input', filename=v, elem=elem)
+ charset = attribs.get('charset', django_settings.DEFAULT_CHARSET)
+ self._hunks.append(unicode(input, charset))
+ fd.close()
+ return self._hunks
+
+ def concat(self):
+ # Design decision needed: either everything should be unicode up to
+ # here or we encode strings as soon as we acquire them. Currently
+ # concat() expects all hunks to be unicode and does the encoding
+ return "\n".join([hunk.encode(django_settings.DEFAULT_CHARSET) for hunk in self.hunks])
+
+ def filter(self, content, method, **kwargs):
+ for f in self.filters:
+ filter = getattr(filters.get_class(f)(content, filter_type=self.type), method)
+ try:
+ if callable(filter):
+ content = filter(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ @property
+ def combined(self):
+ if getattr(self, '_output', ''):
+ return self._output
+ output = self.concat()
+ if self.filters:
+ output = self.filter(output, 'output')
+ self._output = output
+ return self._output
+
+ @property
+ def hash(self):
+ return get_hexdigest(self.combined)[:12]
+
+ @property
+ def new_filepath(self):
+ filename = "".join([self.hash, self.extension])
+ return os.path.join(
+ settings.OUTPUT_DIR.strip(os.sep), self.output_prefix, filename)
+
+ def save_file(self):
+ if self.storage.exists(self.new_filepath):
+ return False
+ self.storage.save(self.new_filepath, ContentFile(self.combined))
+ return True
+
+ def output(self):
+ if not settings.COMPRESS:
+ return self.content
+ self.save_file()
+ context = getattr(self, 'extra_context', {})
+ context['url'] = self.storage.url(self.new_filepath)
+ return render_to_string(self.template_name, context)
+
+ def output_inline(self):
+ context = {'content': settings.COMPRESS and self.combined or self.concat()}
+ if hasattr(self, 'extra_context'):
+ context.update(self.extra_context)
+ return render_to_string(self.template_name_inline, context)
diff --git a/compressor/conf/settings.py b/compressor/conf/settings.py
index d65d4f6..9f72c32 100644
--- a/compressor/conf/settings.py
+++ b/compressor/conf/settings.py
@@ -30,3 +30,6 @@ MINT_DELAY = getattr(settings, 'COMPRESS_MINT_DELAY', 30) # 30 seconds
# check for file changes only after a delay (in seconds, disabled by default)
MTIME_DELAY = getattr(settings, 'COMPRESS_MTIME_DELAY', None)
+
+# the backend to use when parsing the JavaScript or Stylesheet files
+PARSER = getattr(settings, 'COMPRESS_PARSER', 'compressor.parser.BeautifulSoupParser')
diff --git a/compressor/css.py b/compressor/css.py
new file mode 100644
index 0000000..1f57140
--- /dev/null
+++ b/compressor/css.py
@@ -0,0 +1,57 @@
+from django.conf import settings as django_settings
+
+from compressor.conf import settings
+from compressor.base import Compressor, UncompressableFileError
+
+class CssCompressor(Compressor):
+
+ def __init__(self, content, output_prefix="css"):
+ self.extension = ".css"
+ self.template_name = "compressor/css.html"
+ self.template_name_inline = "compressor/css_inline.html"
+ self.filters = ['compressor.filters.css_default.CssAbsoluteFilter']
+ self.filters.extend(settings.COMPRESS_CSS_FILTERS)
+ self.type = 'css'
+ super(CssCompressor, self).__init__(content, 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'] == 'stylesheet':
+ try:
+ content = self.parser.elem_content(elem)
+ data = ('file', self.get_filename(elem_attribs['href']), elem)
+ except UncompressableFileError:
+ if django_settings.DEBUG:
+ raise
+ elif elem_name == 'style':
+ data = ('hunk', self.parser.elem_content(elem), 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,
+ # otherwise create a new node.
+ if self.media_nodes and self.media_nodes[-1][0] == media:
+ self.media_nodes[-1][1].split_content.append(data)
+ else:
+ node = CssCompressor(content='')
+ node.split_content.append(data)
+ self.media_nodes.append((media, node))
+ return self.split_content
+
+ def output(self):
+ self.split_contents()
+ if not hasattr(self, 'media_nodes'):
+ return super(CssCompressor, self).output()
+ if not settings.COMPRESS:
+ return self.content
+ ret = []
+ for media, subnode in self.media_nodes:
+ subnode.extra_context = {'media': media}
+ ret.append(subnode.output())
+ return ''.join(ret)
diff --git a/compressor/exceptions.py b/compressor/exceptions.py
new file mode 100644
index 0000000..a79c9ad
--- /dev/null
+++ b/compressor/exceptions.py
@@ -0,0 +1,17 @@
+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
diff --git a/compressor/filters/__init__.py b/compressor/filters/__init__.py
index dbf971b..f81eb90 100644
--- a/compressor/filters/__init__.py
+++ b/compressor/filters/__init__.py
@@ -1,3 +1,6 @@
+from compressor.exceptions import FilterError
+from compressor.utils import get_class, get_mod_func
+
class FilterBase(object):
def __init__(self, content, filter_type=None, verbose=0):
self.type = filter_type
@@ -8,38 +11,3 @@ class FilterBase(object):
raise NotImplementedError
def output(self, **kwargs):
raise NotImplementedError
-
-class FilterError(Exception):
- """
- This exception is raised when a filter fails
- """
- pass
-
-def get_class(class_string):
- """
- Convert a string version of a function name to the callable object.
- """
-
- if not hasattr(class_string, '__bases__'):
-
- try:
- class_string = class_string.encode('ascii')
- mod_name, class_name = get_mod_func(class_string)
- if class_name != '':
- cls = getattr(__import__(mod_name, {}, {}, ['']), class_name)
- except (ImportError, AttributeError):
- raise FilterError('Failed to import filter %s' % class_string)
-
- return cls
-
-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:]
diff --git a/compressor/filters/css_default.py b/compressor/filters/css_default.py
index fca6e87..53a886f 100644
--- a/compressor/filters/css_default.py
+++ b/compressor/filters/css_default.py
@@ -4,7 +4,7 @@ import posixpath
from compressor.filters import FilterBase, FilterError
from compressor.conf import settings
-from compressor import get_hexdigest, get_mtime
+from compressor.utils import get_hexdigest, get_mtime
class CssAbsoluteFilter(FilterBase):
def input(self, filename=None, **kwargs):
diff --git a/compressor/js.py b/compressor/js.py
new file mode 100644
index 0000000..c185a62
--- /dev/null
+++ b/compressor/js.py
@@ -0,0 +1,30 @@
+from django.conf import settings as django_settings
+
+from compressor.conf import settings
+from compressor.base import Compressor, UncompressableFileError
+
+class JsCompressor(Compressor):
+
+ def __init__(self, content, output_prefix="js"):
+ self.extension = ".js"
+ self.template_name = "compressor/js.html"
+ self.template_name_inline = "compressor/js_inline.html"
+ self.filters = settings.COMPRESS_JS_FILTERS
+ self.type = 'js'
+ super(JsCompressor, self).__init__(content, 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:
+ try:
+ self.split_content.append(('file', self.get_filename(attribs['src']), elem))
+ except UncompressableFileError:
+ if django_settings.DEBUG:
+ raise
+ else:
+ content = self.parser.elem_content(elem)
+ self.split_content.append(('hunk', content, elem))
+ return self.split_content
diff --git a/compressor/parser.py b/compressor/parser.py
new file mode 100644
index 0000000..01cf5ca
--- /dev/null
+++ b/compressor/parser.py
@@ -0,0 +1,116 @@
+from django.conf import settings as django_settings
+from django.utils.encoding import smart_unicode
+
+from compressor.conf import settings
+from compressor.exceptions import ParserError
+from compressor.utils import get_class
+
+class ParserBase(object):
+
+ 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
+
+class BeautifulSoupParser(ParserBase):
+ _soup = None
+
+ @property
+ def soup(self):
+ try:
+ from BeautifulSoup import BeautifulSoup
+ except ImportError, e:
+ raise ParserError("Error while initializing Parser: %s" % e)
+ if self._soup is None:
+ self._soup = BeautifulSoup(self.content)
+ return self._soup
+
+ def css_elems(self):
+ return self.soup.findAll({'link' : True, 'style' : True})
+
+ def js_elems(self):
+ 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_unicode(elem)
+
+class LxmlParser(ParserBase):
+ _tree = None
+
+ @property
+ def tree(self):
+ try:
+ from lxml import html
+ from lxml.etree import tostring
+ except ImportError, e:
+ raise ParserError("Error while initializing Parser: %s" % e)
+ if self._tree is None:
+ content = '%s' % self.content
+ self._tree = html.fromstring(content)
+ try:
+ ignore = tostring(self._tree, encoding=unicode)
+ except UnicodeDecodeError:
+ self._tree = html.soupparser.fromstring(content)
+ return self._tree
+
+ def css_elems(self):
+ return self.tree.xpath('link[@rel="stylesheet"]|style')
+
+ def js_elems(self):
+ return self.tree.findall('script')
+
+ def elem_attribs(self, elem):
+ return elem.attrib
+
+ def elem_content(self, elem):
+ return smart_unicode(elem.text)
+
+ def elem_name(self, elem):
+ return elem.tag
+
+ def elem_str(self, elem):
+ from lxml import etree
+ return smart_unicode(etree.tostring(elem, method='html', encoding=unicode))
diff --git a/compressor/utils.py b/compressor/utils.py
new file mode 100644
index 0000000..1341d7f
--- /dev/null
+++ b/compressor/utils.py
@@ -0,0 +1,54 @@
+import os
+from django.core.cache import cache
+from compressor.conf import settings
+from compressor.exceptions import FilterError
+
+def get_hexdigest(plaintext):
+ try:
+ import hashlib
+ return hashlib.sha1(plaintext).hexdigest()
+ except ImportError:
+ import sha
+ return sha.new(plaintext).hexdigest()
+
+
+def get_mtime(filename):
+ if settings.MTIME_DELAY:
+ key = "django_compressor.mtime.%s" % filename
+ mtime = cache.get(key)
+ if mtime is None:
+ mtime = os.path.getmtime(filename)
+ cache.set(key, mtime, settings.MTIME_DELAY)
+ return mtime
+ return os.path.getmtime(filename)
+
+
+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 = class_string.encode('ascii')
+ mod_name, class_name = get_mod_func(class_string)
+ if class_name != '':
+ cls = getattr(__import__(mod_name, {}, {}, ['']), class_name)
+ except (ImportError, AttributeError):
+ raise exception('Failed to import filter %s' % class_string)
+
+ return cls
+
+
+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:]
diff --git a/tests/core/tests.py b/tests/core/tests.py
index ce7d84a..aa36225 100644
--- a/tests/core/tests.py
+++ b/tests/core/tests.py
@@ -31,12 +31,12 @@ class CompressorTestCase(TestCase):
def test_css_split(self):
out = [
- ('file', os.path.join(settings.MEDIA_ROOT, u'css/one.css'), ''),
- ('hunk', u'p { border:5px solid green;}', ''),
- ('file', os.path.join(settings.MEDIA_ROOT, u'css/two.css'), ''),
+ ('file', os.path.join(settings.MEDIA_ROOT, u'css/one.css'), u''),
+ ('hunk', u'p { border:5px solid green;}', u''),
+ ('file', os.path.join(settings.MEDIA_ROOT, u'css/two.css'), u''),
]
split = self.cssNode.split_contents()
- split = [(x[0], x[1], str(x[2])) for x in split]
+ split = [(x[0], x[1], self.cssNode.parser.elem_str(x[2])) for x in split]
self.assertEqual(out, split)
def test_css_hunks(self):
@@ -72,7 +72,7 @@ class CompressorTestCase(TestCase):
('hunk', u'obj.value = "value";', '')
]
split = self.jsNode.split_contents()
- split = [(x[0], x[1], str(x[2])) for x in split]
+ split = [(x[0], x[1], self.jsNode.parser.elem_str(x[2])) for x in split]
self.assertEqual(out, split)
def test_js_hunks(self):
@@ -108,6 +108,26 @@ class CompressorTestCase(TestCase):
self.assertEqual(output, JsCompressor(self.js).output())
settings.OUTPUT_DIR = old_output_dir
+class LxmlCompressorTestCase(CompressorTestCase):
+
+ def test_css_split(self):
+ out = [
+ ('file', os.path.join(settings.MEDIA_ROOT, u'css/one.css'), u''),
+ ('hunk', u'p { border:5px solid green;}', u''),
+ ('file', os.path.join(settings.MEDIA_ROOT, u'css/two.css'), u''),
+ ]
+ split = self.cssNode.split_contents()
+ split = [(x[0], x[1], self.cssNode.parser.elem_str(x[2])) for x in split]
+ self.assertEqual(out, split)
+
+ def setUp(self):
+ self.old_parser = settings.PARSER
+ settings.PARSER = 'compressor.parser.LxmlParser'
+ super(LxmlCompressorTestCase, self).setUp()
+
+ def tearDown(self):
+ settings.PARSER = self.old_parser
+
class CssAbsolutizingTestCase(TestCase):
def setUp(self):
settings.COMPRESS = True