195 lines
6.5 KiB
Python
195 lines
6.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, ouput_prefix="compressed"):
|
|
self.content = content
|
|
self.type = None
|
|
self.ouput_prefix = ouput_prefix
|
|
self.split_content = []
|
|
self.soup = BeautifulSoup(self.content)
|
|
|
|
def content_hash(self):
|
|
"""docstring for content_hash"""
|
|
pass
|
|
|
|
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 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)
|
|
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)
|
|
self._hunks.append(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)
|
|
self._hunks.append(input)
|
|
fd.close()
|
|
return self._hunks
|
|
|
|
def concat(self):
|
|
# if any of the hunks are unicode, all of them will be coerced
|
|
# this breaks any hunks with non-ASCII data in them
|
|
return "\n".join([str(hunk) for hunk in self.hunks])
|
|
|
|
def filter(self, content, method, **kwargs):
|
|
content = content
|
|
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()
|
|
filter_method = getattr(self, 'filter_method', None)
|
|
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 "/".join((settings.OUTPUT_DIR.strip('/'), self.ouput_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, ouput_prefix="css"):
|
|
self.extension = ".css"
|
|
self.template_name = "compressor/css.html"
|
|
self.filters = [
|
|
'compressor.filters.css_default.CssAbsoluteFilter',
|
|
'compressor.filters.css_default.CssMediaFilter',
|
|
]
|
|
self.filters.extend(settings.COMPRESS_CSS_FILTERS)
|
|
self.type = 'css'
|
|
super(CssCompressor, self).__init__(content, ouput_prefix)
|
|
|
|
def split_contents(self):
|
|
if self.split_content:
|
|
return self.split_content
|
|
split = self.soup.findAll({'link' : True, 'style' : True})
|
|
for elem in split:
|
|
if elem.name == 'link' and elem['rel'] == 'stylesheet':
|
|
# TODO: Make sure this doesn't break when debug is off. I was thinking it would just skip over but it 500's :(
|
|
try:
|
|
self.split_content.append(('file', self.get_filename(elem['href']), elem))
|
|
except UncompressableFileError:
|
|
if django_settings.DEBUG:
|
|
raise
|
|
if elem.name == 'style':
|
|
self.split_content.append(('hunk', elem.string, elem))
|
|
return self.split_content
|
|
|
|
|
|
class JsCompressor(Compressor):
|
|
|
|
def __init__(self, content, ouput_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, ouput_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
|