Refactored the compressor code a little (split in separate modules). Also abstracted the file parsing and added a BeautifulSoupParser and a LxmlParser, while the former is the default.
This commit is contained in:
32
README.rst
32
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/
|
||||
|
@@ -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
|
||||
|
139
compressor/base.py
Normal file
139
compressor/base.py
Normal file
@@ -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)
|
@@ -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')
|
||||
|
57
compressor/css.py
Normal file
57
compressor/css.py
Normal file
@@ -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)
|
17
compressor/exceptions.py
Normal file
17
compressor/exceptions.py
Normal file
@@ -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
|
@@ -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:]
|
||||
|
@@ -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):
|
||||
|
30
compressor/js.py
Normal file
30
compressor/js.py
Normal file
@@ -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
|
116
compressor/parser.py
Normal file
116
compressor/parser.py
Normal file
@@ -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 = '<root>%s</root>' % 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))
|
54
compressor/utils.py
Normal file
54
compressor/utils.py
Normal file
@@ -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:]
|
@@ -31,12 +31,12 @@ class CompressorTestCase(TestCase):
|
||||
|
||||
def test_css_split(self):
|
||||
out = [
|
||||
('file', os.path.join(settings.MEDIA_ROOT, u'css/one.css'), '<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8" />'),
|
||||
('hunk', u'p { border:5px solid green;}', '<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
('file', os.path.join(settings.MEDIA_ROOT, u'css/two.css'), '<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" />'),
|
||||
('file', os.path.join(settings.MEDIA_ROOT, u'css/one.css'), u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8" />'),
|
||||
('hunk', u'p { border:5px solid green;}', u'<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
('file', os.path.join(settings.MEDIA_ROOT, u'css/two.css'), u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" />'),
|
||||
]
|
||||
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";', '<script type="text/javascript" charset="utf-8">obj.value = "value";</script>')
|
||||
]
|
||||
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'<link rel="stylesheet" href="/media/css/one.css" type="text/css" charset="utf-8">'),
|
||||
('hunk', u'p { border:5px solid green;}', u'<style type="text/css">p { border:5px solid green;}</style>'),
|
||||
('file', os.path.join(settings.MEDIA_ROOT, u'css/two.css'), u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8">'),
|
||||
]
|
||||
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
|
||||
|
Reference in New Issue
Block a user