Merge remote branch 'muhuk/master'
Conflicts: compressor/__init__.py tests/core/tests.py
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,6 @@
|
|||||||
build
|
build
|
||||||
*CACHE*
|
*CACHE*
|
||||||
dist
|
dist
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
*.pyc
|
||||||
|
*.egg-info
|
10
README.rst
10
README.rst
@@ -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:**
|
||||||
|
|
||||||
|
@@ -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):
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -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)
|
|
||||||
|
@@ -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')
|
||||||
|
@@ -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" />
|
||||||
|
@@ -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
1
tests/media/js/nonasc.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
var test_value = "—";
|
Reference in New Issue
Block a user