summaryrefslogtreecommitdiff
path: root/compressor/offline
diff options
context:
space:
mode:
Diffstat (limited to 'compressor/offline')
-rw-r--r--compressor/offline/__init__.py0
-rw-r--r--compressor/offline/django.py143
-rw-r--r--compressor/offline/jinja2.py125
3 files changed, 268 insertions, 0 deletions
diff --git a/compressor/offline/__init__.py b/compressor/offline/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compressor/offline/__init__.py
diff --git a/compressor/offline/django.py b/compressor/offline/django.py
new file mode 100644
index 0000000..6541471
--- /dev/null
+++ b/compressor/offline/django.py
@@ -0,0 +1,143 @@
+from __future__ import absolute_import
+import io
+from copy import copy
+
+from django import template
+from django.conf import settings
+from django.template import Template
+from django.template import Context
+from django.template.base import Node, VariableNode, TextNode, NodeList
+from django.template.defaulttags import IfNode
+from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext
+
+
+from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
+from compressor.templatetags.compress import CompressorNode
+
+
+def handle_extendsnode(extendsnode, block_context=None):
+ """Create a copy of Node tree of a derived template replacing
+ all blocks tags with the nodes of appropriate blocks.
+ Also handles {{ block.super }} tags.
+ """
+ if block_context is None:
+ block_context = BlockContext()
+ blocks = dict((n.name, n) for n in
+ extendsnode.nodelist.get_nodes_by_type(BlockNode))
+ block_context.add_blocks(blocks)
+
+ context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
+ compiled_parent = extendsnode.get_parent(context)
+ parent_nodelist = compiled_parent.nodelist
+ # If the parent template has an ExtendsNode it is not the root.
+ for node in parent_nodelist:
+ # The ExtendsNode has to be the first non-text node.
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ return handle_extendsnode(node, block_context)
+ break
+ # Add blocks of the root template to block context.
+ blocks = dict((n.name, n) for n in
+ parent_nodelist.get_nodes_by_type(BlockNode))
+ block_context.add_blocks(blocks)
+
+ block_stack = []
+ new_nodelist = remove_block_nodes(parent_nodelist, block_stack, block_context)
+ return new_nodelist
+
+
+def remove_block_nodes(nodelist, block_stack, block_context):
+ new_nodelist = NodeList()
+ for node in nodelist:
+ if isinstance(node, VariableNode):
+ var_name = node.filter_expression.token.strip()
+ if var_name == 'block.super':
+ if not block_stack:
+ continue
+ node = block_context.get_block(block_stack[-1].name)
+ if isinstance(node, BlockNode):
+ expanded_block = expand_blocknode(node, block_stack, block_context)
+ new_nodelist.extend(expanded_block)
+ else:
+ # IfNode has nodelist as a @property so we can not modify it
+ if isinstance(node, IfNode):
+ node = copy(node)
+ for i, (condition, sub_nodelist) in enumerate(node.conditions_nodelists):
+ sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context)
+ node.conditions_nodelists[i] = (condition, sub_nodelist)
+ else:
+ for attr in node.child_nodelists:
+ sub_nodelist = getattr(node, attr, None)
+ if sub_nodelist:
+ sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context)
+ node = copy(node)
+ setattr(node, attr, sub_nodelist)
+ new_nodelist.append(node)
+ return new_nodelist
+
+
+def expand_blocknode(node, block_stack, block_context):
+ popped_block = block = block_context.pop(node.name)
+ if block is None:
+ block = node
+ block_stack.append(block)
+ expanded_nodelist = remove_block_nodes(block.nodelist, block_stack, block_context)
+ block_stack.pop()
+ if popped_block is not None:
+ block_context.push(node.name, popped_block)
+ return expanded_nodelist
+
+
+class DjangoParser(object):
+ def __init__(self, charset):
+ self.charset = charset
+
+ def parse(self, template_name):
+ with io.open(template_name, mode='rb') as file:
+ try:
+ return Template(file.read().decode(self.charset))
+ except template.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except template.TemplateDoesNotExist as e:
+ raise TemplateDoesNotExist(str(e))
+
+ def process_template(self, template, context):
+ return True
+
+ def get_init_context(self, offline_context):
+ return offline_context
+
+ def process_node(self, template, context, node):
+ pass
+
+ def render_nodelist(self, template, context, node):
+ return node.nodelist.render(context)
+
+ def render_node(self, template, context, node):
+ return node.render(context, forced=True)
+
+ def get_nodelist(self, node):
+ if isinstance(node, ExtendsNode):
+ try:
+ return handle_extendsnode(node)
+ except template.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except template.TemplateDoesNotExist as e:
+ raise TemplateDoesNotExist(str(e))
+
+ # Check if node is an ```if``` switch with true and false branches
+ nodelist = []
+ if isinstance(node, Node):
+ for attr in node.child_nodelists:
+ nodelist += getattr(node, attr, [])
+ else:
+ nodelist = getattr(node, 'nodelist', [])
+ return nodelist
+
+ def walk_nodes(self, node):
+ for node in self.get_nodelist(node):
+ if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True):
+ yield node
+ else:
+ for node in self.walk_nodes(node):
+ yield node
diff --git a/compressor/offline/jinja2.py b/compressor/offline/jinja2.py
new file mode 100644
index 0000000..feee818
--- /dev/null
+++ b/compressor/offline/jinja2.py
@@ -0,0 +1,125 @@
+from __future__ import absolute_import
+import io
+
+import jinja2
+import jinja2.ext
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.nodes import CallBlock, Call, ExtensionAttribute
+
+from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
+
+
+def flatten_context(context):
+ if hasattr(context, 'dicts'):
+ context_dict = {}
+
+ for d in context.dicts:
+ context_dict.update(d)
+
+ return context_dict
+
+ return context
+
+
+class SpacelessExtension(Extension):
+ """
+ Functional "spaceless" extension equivalent to Django's.
+
+ See: https://github.com/django/django/blob/master/django/template/defaulttags.py
+ """
+
+ tags = set(['spaceless'])
+
+ def parse(self, parser):
+ lineno = next(parser.stream).lineno
+ body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
+
+ return nodes.CallBlock(self.call_method('_spaceless', []),
+ [], [], body).set_lineno(lineno)
+
+ def _spaceless(self, caller):
+ from django.utils.html import strip_spaces_between_tags
+
+ return strip_spaces_between_tags(caller().strip())
+
+
+def url_for(mod, filename):
+ """
+ Incomplete emulation of Flask's url_for.
+ """
+ from django.contrib.staticfiles.templatetags import staticfiles
+
+ if mod == "static":
+ return staticfiles.static(filename)
+
+ return ""
+
+
+class Jinja2Parser(object):
+ COMPRESSOR_ID = 'compressor.contrib.jinja2ext.CompressorExtension'
+
+ def __init__(self, charset, env):
+ self.charset = charset
+ self.env = env
+
+ def parse(self, template_name):
+ with io.open(template_name, mode='rb') as file:
+ try:
+ template = self.env.parse(file.read().decode(self.charset))
+ except jinja2.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except jinja2.TemplateNotFound as e:
+ raise TemplateDoesNotExist(str(e))
+
+ return template
+
+ def process_template(self, template, context):
+ return True
+
+ def get_init_context(self, offline_context):
+ # Don't need to add filters and tests to the context, as Jinja2 will
+ # automatically look for them in self.env.filters and self.env.tests.
+ # This is tested by test_complex and test_templatetag.
+
+ # Allow offline context to override the globals.
+ context = self.env.globals.copy()
+ context.update(offline_context)
+
+ return context
+
+ def process_node(self, template, context, node):
+ pass
+
+ def _render_nodes(self, template, context, nodes):
+ compiled_node = self.env.compile(jinja2.nodes.Template(nodes))
+ template = jinja2.Template.from_code(self.env, compiled_node, {})
+ flat_context = flatten_context(context)
+
+ return template.render(flat_context)
+
+ def render_nodelist(self, template, context, node):
+ return self._render_nodes(template, context, node.body)
+
+ def render_node(self, template, context, node):
+ return self._render_nodes(template, context, [node])
+
+ def get_nodelist(self, node):
+ body = getattr(node, "body", getattr(node, "nodes", []))
+
+ if isinstance(node, jinja2.nodes.If):
+ return body + node.else_
+
+ return body
+
+ def walk_nodes(self, node, block_name=None):
+ for node in self.get_nodelist(node):
+ if (isinstance(node, CallBlock) and
+ isinstance(node.call, Call) and
+ isinstance(node.call.node, ExtensionAttribute) and
+ node.call.node.identifier == self.COMPRESSOR_ID):
+ node.call.node.name = '_compress_forced'
+ yield node
+ else:
+ for node in self.walk_nodes(node, block_name=block_name):
+ yield node