Files
deb-python-django-compressor/compressor/__init__.py

218 lines
7.5 KiB
Python

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.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()
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 [os.path.getmtime(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)
class CssCompressor(Compressor):
def __init__(self, content, output_prefix="css"):
self.extension = ".css"
self.template_name = "compressor/css.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.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