Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding option to concatenate files before processing #289

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 38 additions & 7 deletions compressor/base.py
Expand Up @@ -31,7 +31,7 @@ class Compressor(object):
"""
type = None

def __init__(self, content=None, output_prefix=None, context=None, *args, **kwargs):
def __init__(self, content=None, output_prefix=None, context=None, opts=None, *args, **kwargs):
self.content = content or ""
self.output_prefix = output_prefix or "compressed"
self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
Expand All @@ -42,6 +42,7 @@ def __init__(self, content=None, output_prefix=None, context=None, *args, **kwar
self.extra_context = {}
self.all_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
self.finders = staticfiles.finders
self.opts = opts or {}

def split_contents(self):
"""
Expand All @@ -50,6 +51,32 @@ def split_contents(self):
"""
raise NotImplementedError

def group_contents(self):
contents = []
groups = {}
for kind, value, basename, elems in self.split_contents():
attrs = self.parser.elem_attribs(elems[0])
charset = attrs.get("charset", self.charset)
mimetype = attrs.get("type", None)
if mimetype:
idx = groups.get(mimetype, -1)
if idx >= 0:
# get the content of this elem if it's a file
if kind == SOURCE_FILE:
value = self.get_filecontent(value, charset)
# if there is only 1 existing and it's a file, convert it too
if len(contents[idx][3]) == 1 and contents[idx][0] == SOURCE_FILE:
contents[idx][1] = self.get_filecontent(contents[idx][1], charset)
contents[idx][0] = SOURCE_HUNK
contents[idx][1] += value
contents[idx][3].extend(elems)
else:
groups[mimetype] = len(contents)
contents.append([kind, value, smart_unicode(basename), elems])
else:
contents.append([kind, value, basename, elems])
return contents

def get_template_name(self, mode):
"""
Returns the template path for the given mode.
Expand Down Expand Up @@ -147,17 +174,21 @@ def hunks(self, forced=False):
bunch of precompiled and/or rendered hunks.
"""
enabled = settings.COMPRESS_ENABLED or forced
group_first = self.opts.get('group_first', 'false').lower() == 'true'
contents = group_first and self.group_contents() or self.split_contents()

for kind, value, basename, elem in self.split_contents():
for kind, value, basename, elems in contents:
precompiled = False
attribs = self.parser.elem_attribs(elem)
# If it's a grouped set, they should all have the same charset and type
attribs = self.parser.elem_attribs(elems[0])
charset = attribs.get("charset", self.charset)
options = {
'method': METHOD_INPUT,
'elem': elem,
'elems': elems,
'kind': kind,
'basename': basename,
}
options.update(self.opts)

if kind == SOURCE_FILE:
options = dict(options, filename=value)
Expand All @@ -174,7 +205,7 @@ def hunks(self, forced=False):
value = self.handle_output(kind, value, forced=True, basename=basename)
yield smart_unicode(value, charset.lower())
else:
yield self.parser.elem_str(elem)
yield "\n".join([self.parser.elem_str(e) for e in elems])

def filter_output(self, content):
"""
Expand All @@ -193,10 +224,10 @@ def filter_input(self, forced=False):
content.append(hunk)
return content

def precompile(self, content, kind=None, elem=None, filename=None, **kwargs):
def precompile(self, content, kind=None, elems=None, filename=None, **kwargs):
if not kind:
return False, content
attrs = self.parser.elem_attribs(elem)
attrs = self.parser.elem_attribs(elems[0])
mimetype = attrs.get("type", None)
if mimetype:
command = self.all_mimetypes.get(mimetype)
Expand Down
10 changes: 5 additions & 5 deletions compressor/css.py
Expand Up @@ -4,9 +4,9 @@

class CssCompressor(Compressor):

def __init__(self, content=None, output_prefix="css", context=None):
def __init__(self, content=None, output_prefix="css", context=None, opts=None):
super(CssCompressor, self).__init__(content=content,
output_prefix=output_prefix, context=context)
output_prefix=output_prefix, context=context, opts=opts)
self.filters = list(settings.COMPRESS_CSS_FILTERS)
self.type = output_prefix

Expand All @@ -21,9 +21,9 @@ def split_contents(self):
if elem_name == 'link' and elem_attribs['rel'].lower() == 'stylesheet':
basename = self.get_basename(elem_attribs['href'])
filename = self.get_filename(basename)
data = (SOURCE_FILE, filename, basename, elem)
data = (SOURCE_FILE, filename, basename, [elem])
elif elem_name == 'style':
data = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem)
data = (SOURCE_HUNK, self.parser.elem_content(elem), None, [elem])
if data:
self.split_content.append(data)
media = elem_attribs.get('media', None)
Expand All @@ -34,7 +34,7 @@ def split_contents(self):
self.media_nodes[-1][1].split_content.append(data)
else:
node = CssCompressor(content=self.parser.elem_str(elem),
context=self.context)
context=self.context, opts=self.opts)
node.split_content.append(data)
self.media_nodes.append((media, node))
return self.split_content
Expand Down
8 changes: 4 additions & 4 deletions compressor/js.py
Expand Up @@ -4,8 +4,8 @@

class JsCompressor(Compressor):

def __init__(self, content=None, output_prefix="js", context=None):
super(JsCompressor, self).__init__(content, output_prefix, context)
def __init__(self, content=None, output_prefix="js", context=None, opts=None):
super(JsCompressor, self).__init__(content, output_prefix, context, opts)
self.filters = list(settings.COMPRESS_JS_FILTERS)
self.type = output_prefix

Expand All @@ -17,9 +17,9 @@ def split_contents(self):
if 'src' in attribs:
basename = self.get_basename(attribs['src'])
filename = self.get_filename(basename)
content = (SOURCE_FILE, filename, basename, elem)
content = (SOURCE_FILE, filename, basename, [elem])
self.split_content.append(content)
else:
content = self.parser.elem_content(elem)
self.split_content.append((SOURCE_HUNK, content, None, elem))
self.split_content.append((SOURCE_HUNK, content, None, [elem]))
return self.split_content
86 changes: 59 additions & 27 deletions compressor/templatetags/compress.py
@@ -1,3 +1,5 @@
import re

from django import template
from django.core.exceptions import ImproperlyConfigured

Expand Down Expand Up @@ -33,9 +35,9 @@ def compressor_cls(self, kind, *args, **kwargs):
return get_class(self.compressors.get(kind),
exception=ImproperlyConfigured)(*args, **kwargs)

def get_compressor(self, context, kind):
def get_compressor(self, context, kind, tag_opts):
return self.compressor_cls(kind,
content=self.get_original_content(context), context=context)
content=self.get_original_content(context), context=context, opts=tag_opts)

def debug_mode(self, context):
if settings.COMPRESS_DEBUG_TOGGLE:
Expand Down Expand Up @@ -82,50 +84,63 @@ def render_cached(self, compressor, kind, mode, forced=False):
return cache_key, cache_content
return None, None

def render_compressed(self, context, kind, mode, forced=False):
def render_compressed(self, context, kind, mode, tag_opts=None, forced=False):
deferred = getattr(self, 'name', None) and tag_opts and tag_opts.get('deferred', 'false') == 'true'

# See if it has been rendered offline
cached_offline = self.render_offline(context, forced=forced)
if cached_offline:
return cached_offline
return self.render_result(cached_offline, context, deferred, mode)

# Take a shortcut if we really don't have anything to do
if ((not settings.COMPRESS_ENABLED and
not settings.COMPRESS_PRECOMPILERS) and not forced):
return self.get_original_content(context)
return self.render_result(self.get_original_content(context), context, deferred, mode)

context['compressed'] = {'name': getattr(self, 'name', None)}
compressor = self.get_compressor(context, kind)
compressor = self.get_compressor(context, kind, tag_opts)

# Prepare the actual compressor and check cache
cache_key, cache_content = self.render_cached(compressor, kind, mode, forced=forced)
if cache_content is not None:
return cache_content
return self.render_result(cache_content, context, deferred, mode)

# call compressor output method and handle exceptions
try:
rendered_output = self.render_output(compressor, mode, forced=forced)
if cache_key:
cache_set(cache_key, rendered_output)
return rendered_output.decode('utf-8')
return self.render_result(rendered_output.decode('utf-8'), context, deferred, mode)
except Exception:
if settings.DEBUG or forced:
raise

# Or don't do anything in production
return self.get_original_content(context)
return self.render_result(self.get_original_content(context), context, deferred, mode)

def render_output(self, compressor, mode, forced=False):
return compressor.output(mode, forced=forced)

def render_result(self, content, context, deferred, mode):
if deferred:
if mode == OUTPUT_FILE:
match = re.search(r'(?:src|href)=["\']([^"\']+)', content)
if match:
context[self.name] = match.group(1)
return ""
context[self.name] = content
return ""
return content


class CompressorNode(CompressorMixin, template.Node):

def __init__(self, nodelist, kind=None, mode=OUTPUT_FILE, name=None):
def __init__(self, nodelist, kind=None, mode=OUTPUT_FILE, name=None, tag_opts={}):
self.nodelist = nodelist
self.kind = kind
self.mode = mode
self.name = name
self.tag_opts = tag_opts

def get_original_content(self, context):
return self.nodelist.render(context)
Expand All @@ -144,7 +159,16 @@ def render(self, context, forced=False):
if self.debug_mode(context):
return self.get_original_content(context)

return self.render_compressed(context, self.kind, self.mode, forced=forced)
self.resolve_variables(context)
return self.render_compressed(context, self.kind, self.mode, self.tag_opts, forced=forced)

def resolve_variables(self, context):
for option, value in self.tag_opts.items():
try:
value = value.resolve(context)
except template.VariableDoesNotExist:
value = unicode(value)
self.tag_opts[option] = value


@register.tag
Expand All @@ -154,7 +178,7 @@ def compress(parser, token):

Syntax::

{% compress <js/css> %}
{% compress js|css [file|inline] [<option>=<value>[ <option>=<value>...]] [as <variable_name>] %}
<html of inline or linked JS/CSS>
{% endcompress %}

Expand Down Expand Up @@ -191,22 +215,30 @@ def compress(parser, token):

args = token.split_contents()

if not len(args) in (2, 3, 4):
if not len(args) >= 2:
raise template.TemplateSyntaxError(
"%r tag requires either one, two or three arguments." % args[0])
"%r tag requires at least one argument." % args[0])

kind = args[1]

name = None
mode = OUTPUT_FILE
tag_opts = {}
if len(args) >= 3:
mode = args[2]
if not mode in OUTPUT_MODES:
raise template.TemplateSyntaxError(
"%r's second argument must be '%s' or '%s'." %
(args[0], OUTPUT_FILE, OUTPUT_INLINE))
else:
mode = OUTPUT_FILE
if len(args) == 4:
name = args[3]
else:
name = None
return CompressorNode(nodelist, kind, mode, name)
looking_for_name = False
for i in range(2, len(args)):
if looking_for_name:
name = args[i]
looking_for_name = False
elif args[i] == "as":
looking_for_name = True
elif '=' in args[i]:
option, value = args[i].split("=")
tag_opts[option] = template.Variable(value)
elif args[i] in OUTPUT_MODES:
mode = args[i]
else:
raise template.TemplateSyntaxError(
"%r's third argument on must either be (file|input) or <option>=<value> or 'as <name>'" %
args[0])

return CompressorNode(nodelist, kind, mode, name, tag_opts)
1 change: 1 addition & 0 deletions compressor/tests/media/css/one.less
@@ -0,0 +1 @@
body { background:#990; }
1 change: 1 addition & 0 deletions compressor/tests/media/css/two.less
@@ -0,0 +1 @@
body { color:#fff; }
1 change: 1 addition & 0 deletions compressor/tests/media/js/two.coffee
@@ -0,0 +1 @@
# this is a comment.