Skip to content

Commit 515f617

Browse files
committed
Merge with master
2 parents c1c2fa6 + 1811c8f commit 515f617

File tree

16 files changed

+522
-289
lines changed

16 files changed

+522
-289
lines changed

README.rst

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ A list of filters that will be applied to javascript.
148148
The dotted path to a Django Storage backend to be used to save the
149149
compressed files.
150150

151+
``COMPRESS_PARSER``
152+
--------------------
153+
154+
:Default: ``'compressor.parser.BeautifulSoupParser'``
155+
156+
The backend to use when parsing the JavaScript or Stylesheet files.
157+
The backends included in ``compressor``:
158+
159+
- ``compressor.parser.BeautifulSoupParser``
160+
- ``compressor.parser.LxmlParser``
161+
162+
See `Dependencies`_ for more info about the packages you need for each parser.
163+
151164
``COMPRESS_REBUILD_TIMEOUT``
152165
----------------------------
153166

@@ -175,8 +188,21 @@ modification timestamp of a file. Disabled by default. Should be smaller
175188
than ``COMPRESS_REBUILD_TIMEOUT`` and ``COMPRESS_MINT_DELAY``.
176189

177190

178-
Dependecies
179-
***********
191+
Dependencies
192+
************
193+
194+
* BeautifulSoup_ (for the default ``compressor.parser.BeautifulSoupParser``)
195+
196+
::
197+
198+
pip install BeautifulSoup
199+
200+
* lxml_ (for the optional ``compressor.parser.LxmlParser``, requires libxml2_)
201+
202+
::
180203

181-
* BeautifulSoup
204+
STATIC_DEPS=true pip install lxml
182205

206+
.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
207+
.. _lxml: http://codespeak.net/lxml/
208+
.. _libxml2: http://xmlsoft.org/

compressor/__init__.py

Lines changed: 5 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,230 +1,5 @@
1-
import os
2-
from BeautifulSoup import BeautifulSoup
3-
4-
from django import template
5-
from django.conf import settings as django_settings
6-
from django.template.loader import render_to_string
7-
8-
from django.core.cache import cache
9-
from django.core.files.base import ContentFile
10-
from django.core.files.storage import get_storage_class
11-
12-
from compressor.conf import settings
13-
from compressor import filters
14-
15-
16-
register = template.Library()
17-
18-
19-
class UncompressableFileError(Exception):
20-
pass
21-
22-
23-
def get_hexdigest(plaintext):
24-
try:
25-
import hashlib
26-
return hashlib.sha1(plaintext).hexdigest()
27-
except ImportError:
28-
import sha
29-
return sha.new(plaintext).hexdigest()
30-
31-
32-
def get_mtime(filename):
33-
if settings.MTIME_DELAY:
34-
key = "django_compressor.mtime.%s" % filename
35-
mtime = cache.get(key)
36-
if mtime is None:
37-
mtime = os.path.getmtime(filename)
38-
cache.set(key, mtime, settings.MTIME_DELAY)
39-
return mtime
40-
return os.path.getmtime(filename)
41-
42-
class Compressor(object):
43-
44-
def __init__(self, content, output_prefix="compressed"):
45-
self.content = content
46-
self.type = None
47-
self.output_prefix = output_prefix
48-
self.split_content = []
49-
50-
def split_contents(self):
51-
raise NotImplementedError('split_contents must be defined in a subclass')
52-
53-
def get_filename(self, url):
54-
if not url.startswith(self.storage.base_url):
55-
raise UncompressableFileError('"%s" is not in COMPRESS_URL ("%s") and can not be compressed' % (url, self.storage.base_url))
56-
basename = url.replace(self.storage.base_url, "", 1)
57-
if not self.storage.exists(basename):
58-
raise UncompressableFileError('"%s" does not exist' % self.storage.path(basename))
59-
if hasattr(self.storage, 'source_path'):
60-
return self.storage.source_path(basename)
61-
return self.storage.path(basename)
62-
63-
@property
64-
def soup(self):
65-
return BeautifulSoup(self.content)
66-
67-
@property
68-
def mtimes(self):
69-
return [get_mtime(h[1]) for h in self.split_contents() if h[0] == 'file']
70-
71-
@property
72-
def cachekey(self):
73-
cachebits = [self.content]
74-
cachebits.extend([str(m) for m in self.mtimes])
75-
cachestr = "".join(cachebits).encode(django_settings.DEFAULT_CHARSET)
76-
return "django_compressor.%s" % get_hexdigest(cachestr)[:12]
77-
78-
@property
79-
def storage(self):
80-
return get_storage_class(settings.STORAGE)()
81-
82-
@property
83-
def hunks(self):
84-
if getattr(self, '_hunks', ''):
85-
return self._hunks
86-
self._hunks = []
87-
for kind, v, elem in self.split_contents():
88-
if kind == 'hunk':
89-
input = v
90-
if self.filters:
91-
input = self.filter(input, 'input', elem=elem)
92-
# Let's cast BeautifulSoup element to unicode here since
93-
# it will try to encode using ascii internally later
94-
self._hunks.append(unicode(input))
95-
if kind == 'file':
96-
# TODO: wrap this in a try/except for IoErrors(?)
97-
fd = open(v, 'rb')
98-
input = fd.read()
99-
if self.filters:
100-
input = self.filter(input, 'input', filename=v, elem=elem)
101-
charset = elem.get('charset', django_settings.DEFAULT_CHARSET)
102-
self._hunks.append(unicode(input, charset))
103-
fd.close()
104-
return self._hunks
105-
106-
def concat(self):
107-
# Design decision needed: either everything should be unicode up to
108-
# here or we encode strings as soon as we acquire them. Currently
109-
# concat() expects all hunks to be unicode and does the encoding
110-
return "\n".join([hunk.encode(django_settings.DEFAULT_CHARSET) for hunk in self.hunks])
111-
112-
def filter(self, content, method, **kwargs):
113-
for f in self.filters:
114-
filter = getattr(filters.get_class(f)(content, filter_type=self.type), method)
115-
try:
116-
if callable(filter):
117-
content = filter(**kwargs)
118-
except NotImplementedError:
119-
pass
120-
return content
121-
122-
@property
123-
def combined(self):
124-
if getattr(self, '_output', ''):
125-
return self._output
126-
output = self.concat()
127-
if self.filters:
128-
output = self.filter(output, 'output')
129-
self._output = output
130-
return self._output
131-
132-
@property
133-
def hash(self):
134-
return get_hexdigest(self.combined)[:12]
135-
136-
@property
137-
def new_filepath(self):
138-
filename = "".join([self.hash, self.extension])
139-
return os.path.join(
140-
settings.OUTPUT_DIR.strip(os.sep), self.output_prefix, filename)
141-
142-
def save_file(self):
143-
if self.storage.exists(self.new_filepath):
144-
return False
145-
self.storage.save(self.new_filepath, ContentFile(self.combined))
146-
return True
147-
148-
def output(self):
149-
if not settings.COMPRESS:
150-
return self.content
151-
self.save_file()
152-
context = getattr(self, 'extra_context', {})
153-
context['url'] = self.storage.url(self.new_filepath)
154-
return render_to_string(self.template_name, context)
155-
156-
157-
class CssCompressor(Compressor):
158-
159-
def __init__(self, content, output_prefix="css"):
160-
self.extension = ".css"
161-
self.template_name = "compressor/css.html"
162-
self.filters = ['compressor.filters.css_default.CssAbsoluteFilter']
163-
self.filters.extend(settings.COMPRESS_CSS_FILTERS)
164-
self.type = 'css'
165-
super(CssCompressor, self).__init__(content, output_prefix)
166-
167-
def split_contents(self):
168-
if self.split_content:
169-
return self.split_content
170-
split = self.soup.findAll({'link' : True, 'style' : True})
171-
self.media_nodes = []
172-
for elem in split:
173-
data = None
174-
if elem.name == 'link' and elem['rel'] == 'stylesheet':
175-
try:
176-
data = ('file', self.get_filename(elem['href']), elem)
177-
except UncompressableFileError:
178-
if django_settings.DEBUG:
179-
raise
180-
elif elem.name == 'style':
181-
data = ('hunk', elem.string, elem)
182-
if data:
183-
self.split_content.append(data)
184-
media = elem.get('media', None)
185-
# Append to the previous node if it had the same media type,
186-
# otherwise create a new node.
187-
if self.media_nodes and self.media_nodes[-1][0] == media:
188-
self.media_nodes[-1][1].split_content.append(data)
189-
else:
190-
node = CssCompressor(content='')
191-
node.split_content.append(data)
192-
self.media_nodes.append((media, node))
193-
return self.split_content
194-
195-
def output(self):
196-
self.split_contents()
197-
if not hasattr(self, 'media_nodes'):
198-
return super(CssCompressor, self).output()
199-
if not settings.COMPRESS:
200-
return self.content
201-
ret = []
202-
for media, subnode in self.media_nodes:
203-
subnode.extra_context = {'media': media}
204-
ret.append(subnode.output())
205-
return ''.join(ret)
206-
207-
208-
class JsCompressor(Compressor):
209-
210-
def __init__(self, content, output_prefix="js"):
211-
self.extension = ".js"
212-
self.template_name = "compressor/js.html"
213-
self.filters = settings.COMPRESS_JS_FILTERS
214-
self.type = 'js'
215-
super(JsCompressor, self).__init__(content, output_prefix)
216-
217-
def split_contents(self):
218-
if self.split_content:
219-
return self.split_content
220-
split = self.soup.findAll('script')
221-
for elem in split:
222-
if elem.has_key('src'):
223-
try:
224-
self.split_content.append(('file', self.get_filename(elem['src']), elem))
225-
except UncompressableFileError:
226-
if django_settings.DEBUG:
227-
raise
228-
else:
229-
self.split_content.append(('hunk', elem.string, elem))
230-
return self.split_content
1+
from compressor.base import Compressor
2+
from compressor.js import JsCompressor
3+
from compressor.css import CssCompressor
4+
from compressor.utils import get_hexdigest, get_mtime
5+
from compressor.exceptions import UncompressableFileError

0 commit comments

Comments
 (0)