Merge remote branch 'muhuk/master'

Conflicts:
	compressor/__init__.py
	tests/core/tests.py
This commit is contained in:
Jannis Leidel
2010-02-23 13:39:55 +01:00
parent 1f9bd58344
commit 55acb2da06
9 changed files with 69 additions and 38 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
build build
*CACHE* *CACHE*
dist dist
MANIFEST MANIFEST
*.pyc
*.egg-info

View File

@@ -19,7 +19,7 @@ Examples::
Which would be rendered something like:: Which would be rendered something like::
<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" media="all" charset="utf-8"> <link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" charset="utf-8">
or:: or::
@@ -50,8 +50,12 @@ starting with a '/') are left alone.
Stylesheets that are @import'd are not compressed into the main file. They are Stylesheets that are @import'd are not compressed into the main file. They are
left alone. left alone.
Set the media attribute as normal on your <style> and <link> elements and If the media attribute is set on <style> and <link> elements, a separate
the combined CSS will be wrapped in @media blocks as necessary. compressed file is created and linked for each media value you specified.
This allows the media attribute to remain on the generated link element,
instead of wrapping your CSS with @media blocks (which can break your own
@media queries or @font-face declarations). It also allows browsers to avoid
downloading CSS for irrelevant media types.
**Recommendations:** **Recommendations:**

View File

@@ -1,9 +1,11 @@
import os import os
from collections import defaultdict
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from django import template from django import template
from django.conf import settings as django_settings from django.conf import settings as django_settings
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.functional import curry
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import get_storage_class from django.core.files.storage import get_storage_class
@@ -56,7 +58,7 @@ class Compressor(object):
def cachekey(self): def cachekey(self):
cachebits = [self.content] cachebits = [self.content]
cachebits.extend([str(m) for m in self.mtimes]) cachebits.extend([str(m) for m in self.mtimes])
cachestr = "".join(cachebits) cachestr = "".join(cachebits).encode(django_settings.DEFAULT_CHARSET)
return "django_compressor.%s" % get_hexdigest(cachestr)[:12] return "django_compressor.%s" % get_hexdigest(cachestr)[:12]
@property @property
@@ -73,21 +75,24 @@ class Compressor(object):
input = v input = v
if self.filters: if self.filters:
input = self.filter(input, 'input', elem=elem) input = self.filter(input, 'input', elem=elem)
self._hunks.append(input) # 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': if kind == 'file':
# TODO: wrap this in a try/except for IoErrors(?) # TODO: wrap this in a try/except for IoErrors(?)
fd = open(v, 'rb') fd = open(v, 'rb')
input = fd.read() input = fd.read()
if self.filters: if self.filters:
input = self.filter(input, 'input', filename=v, elem=elem) input = self.filter(input, 'input', filename=v, elem=elem)
self._hunks.append(input) self._hunks.append(unicode(input, elem.get('charset', django_settings.DEFAULT_CHARSET)))
fd.close() fd.close()
return self._hunks return self._hunks
def concat(self): def concat(self):
# if any of the hunks are unicode, all of them will be coerced # Design decision needed: either everything should be unicode up to
# this breaks any hunks with non-ASCII data in them # here or we encode strings as soon as we acquire them. Currently
return "\n".join([str(hunk) for hunk in self.hunks]) # 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): def filter(self, content, method, **kwargs):
content = content content = content
@@ -139,10 +144,7 @@ class CssCompressor(Compressor):
def __init__(self, content, output_prefix="css"): def __init__(self, content, output_prefix="css"):
self.extension = ".css" self.extension = ".css"
self.template_name = "compressor/css.html" self.template_name = "compressor/css.html"
self.filters = [ self.filters = ['compressor.filters.css_default.CssAbsoluteFilter']
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.css_default.CssMediaFilter',
]
self.filters.extend(settings.COMPRESS_CSS_FILTERS) self.filters.extend(settings.COMPRESS_CSS_FILTERS)
self.type = 'css' self.type = 'css'
super(CssCompressor, self).__init__(content, output_prefix) super(CssCompressor, self).__init__(content, output_prefix)
@@ -151,17 +153,34 @@ class CssCompressor(Compressor):
if self.split_content: if self.split_content:
return self.split_content return self.split_content
split = self.soup.findAll({'link' : True, 'style' : True}) split = self.soup.findAll({'link' : True, 'style' : True})
self.by_media = defaultdict(curry(CssCompressor, content=''))
for elem in split: for elem in split:
data = None
if elem.name == 'link' and elem['rel'] == 'stylesheet': if elem.name == 'link' and elem['rel'] == 'stylesheet':
try: try:
self.split_content.append(('file', self.get_filename(elem['href']), elem)) data = ('file', self.get_filename(elem['href']), elem)
except UncompressableFileError: except UncompressableFileError:
if django_settings.DEBUG: if django_settings.DEBUG:
raise raise
if elem.name == 'style': elif elem.name == 'style':
self.split_content.append(('hunk', elem.string, elem)) data = ('hunk', elem.string, elem)
if data:
self.split_content.append(data)
self.by_media[elem.get('media', None)].split_content.append(data)
return self.split_content return self.split_content
def output(self):
self.split_contents()
if not hasattr(self, 'by_media'):
return super(CssCompressor, self).output()
if not settings.COMPRESS:
return self.content
ret = []
for media, subnode in self.by_media.items():
subnode.extra_context = {'media': media}
ret.append(subnode.output())
return ''.join(ret)
class JsCompressor(Compressor): class JsCompressor(Compressor):

View File

@@ -8,7 +8,7 @@ OUTPUT_DIR = getattr(settings, 'COMPRESS_OUTPUT_DIR', 'CACHE')
STORAGE = getattr(settings, 'COMPRESS_STORAGE', 'compressor.storage.CompressorFileStorage') STORAGE = getattr(settings, 'COMPRESS_STORAGE', 'compressor.storage.CompressorFileStorage')
COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG) COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG)
COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', []) COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compressor.filters.css_default.CssAbsoluteFilter'])
COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compressor.filters.jsmin.JSMinFilter']) COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compressor.filters.jsmin.JSMinFilter'])
if COMPRESS_CSS_FILTERS is None: if COMPRESS_CSS_FILTERS is None:

View File

@@ -35,12 +35,3 @@ class CssAbsoluteFilter(FilterBase):
if self.has_http: if self.has_http:
full_url = "%s%s" % (self.protocol,full_url) full_url = "%s%s" % (self.protocol,full_url)
return "url('%s')" % full_url return "url('%s')" % full_url
class CssMediaFilter(FilterBase):
def input(self, elem=None, **kwargs):
try:
self.media = elem['media']
except (TypeError, KeyError):
return self.content
return "@media %s {%s}" % (str(self.media), self.content)

View File

@@ -14,7 +14,7 @@ warnings.simplefilter('ignore', RuntimeWarning)
class CSSTidyFilter(FilterBase): class CSSTidyFilter(FilterBase):
def output(self, **kwargs): def output(self, **kwargs):
tmp_file = tempfile.NamedTemporaryFile(mode='w+b') tmp_file = tempfile.NamedTemporaryFile(mode='w+b')
tmp_file.write(css) tmp_file.write(self.content)
tmp_file.flush() tmp_file.flush()
output_file = tempfile.NamedTemporaryFile(mode='w+b') output_file = tempfile.NamedTemporaryFile(mode='w+b')

View File

@@ -1 +1 @@
<link rel="stylesheet" href="{{ url }}" type="text/css" media="all" charset="utf-8"> <link rel="stylesheet" href="{{ url }}" type="text/css" {% if media %}media="{{ media }}" {% endif %}charset="utf-8" />

View File

@@ -64,8 +64,8 @@ class CompressorTestCase(TestCase):
self.assertEqual('f7c661b7a124', self.cssNode.hash) self.assertEqual('f7c661b7a124', self.cssNode.hash)
def test_css_return_if_on(self): def test_css_return_if_on(self):
output = u'<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" media="all" charset="utf-8">' output = u'<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" charset="utf-8" />'
self.assertEqual(output, self.cssNode.output()) self.assertEqual(output, self.cssNode.output().strip())
def test_js_split(self): def test_js_split(self):
@@ -144,12 +144,15 @@ class CssMediaTestCase(TestCase):
<link rel="stylesheet" href="/media/css/one.css" type="text/css" media="screen" charset="utf-8"> <link rel="stylesheet" href="/media/css/one.css" type="text/css" media="screen" charset="utf-8">
<style type="text/css" media="print">p { border:5px solid green;}</style> <style type="text/css" media="print">p { border:5px solid green;}</style>
<link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" media="all"> <link rel="stylesheet" href="/media/css/two.css" type="text/css" charset="utf-8" media="all">
<style type="text/css">h1 { border:5px solid green;}</style>
""" """
self.cssNode = CssCompressor(self.css) self.cssNode = CssCompressor(self.css)
def test_css_output(self): def test_css_output(self):
out = u'@media screen {body { background:#990; }}\n@media print {p { border:5px solid green;}}\n@media all {body { color:#fff; }}' links = BeautifulSoup(self.cssNode.output()).findAll('link')
self.assertEqual(out, self.cssNode.combined) media = set([u'screen', u'print', u'all', None])
self.assertEqual(len(links), 4)
self.assertEqual(media, set([l.get('media', None) for l in links]))
def render(template_string, context_dict=None): def render(template_string, context_dict=None):
"""A shortcut for testing template output.""" """A shortcut for testing template output."""
@@ -163,7 +166,7 @@ def render(template_string, context_dict=None):
class TemplatetagTestCase(TestCase): class TemplatetagTestCase(TestCase):
def setUp(self): def setUp(self):
settings.COMPRESS = True settings.COMPRESS = True
def test_css_tag(self): def test_css_tag(self):
template = u"""{% load compress %}{% compress css %} template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8"> <link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css" charset="utf-8">
@@ -172,17 +175,17 @@ class TemplatetagTestCase(TestCase):
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.MEDIA_URL } context = { 'MEDIA_URL': settings.MEDIA_URL }
out = u'<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" media="all" charset="utf-8">' out = u'<link rel="stylesheet" href="/media/CACHE/css/f7c661b7a124.css" type="text/css" charset="utf-8" />'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
def test_nonascii_css_tag(self): def test_nonascii_css_tag(self):
template = u"""{% load compress %}{% compress css %} template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/nonasc.css" type="text/css" media="print" charset="utf-8"> <link rel="stylesheet" href="{{ MEDIA_URL }}css/nonasc.css" type="text/css" charset="utf-8">
<style type="text/css">p { border:5px solid green;}</style> <style type="text/css">p { border:5px solid green;}</style>
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.MEDIA_URL } context = { 'MEDIA_URL': settings.MEDIA_URL }
out = '<link rel="stylesheet" href="/media/CACHE/css/68da639dbb24.css" type="text/css" media="all" charset="utf-8">' out = '<link rel="stylesheet" href="/media/CACHE/css/1c1c0855907b.css" type="text/css" charset="utf-8" />'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
def test_js_tag(self): def test_js_tag(self):
@@ -195,6 +198,17 @@ class TemplatetagTestCase(TestCase):
out = u'<script type="text/javascript" src="/media/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>' out = u'<script type="text/javascript" src="/media/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))
def test_nonascii_js_tag(self):
template = u"""{% load compress %}{% compress js %}
<script src="{{ MEDIA_URL }}js/nonasc.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
{% endcompress %}
"""
context = { 'MEDIA_URL': settings.MEDIA_URL }
out = u'<script type="text/javascript" src="/media/CACHE/js/5d5c0e1cb25f.js" charset="utf-8"></script>'
self.assertEqual(out, render(template, context))
class TestStorage(CompressorFileStorage): class TestStorage(CompressorFileStorage):
""" """
Test compressor storage that gzips storage files Test compressor storage that gzips storage files
@@ -225,5 +239,5 @@ class StorageTestCase(TestCase):
{% endcompress %} {% endcompress %}
""" """
context = { 'MEDIA_URL': settings.MEDIA_URL } context = { 'MEDIA_URL': settings.MEDIA_URL }
out = u'<link rel="stylesheet" href="/media/CACHE/css/5b231a62e9a6.css.gz" type="text/css" media="all" charset="utf-8">' out = u'<link rel="stylesheet" href="/media/CACHE/css/5b231a62e9a6.css.gz" type="text/css" charset="utf-8" />'
self.assertEqual(out, render(template, context)) self.assertEqual(out, render(template, context))

1
tests/media/js/nonasc.js Normal file
View File

@@ -0,0 +1 @@
var test_value = "—";