diff options
Diffstat (limited to 'compressor/offline')
| -rw-r--r-- | compressor/offline/__init__.py | 0 | ||||
| -rw-r--r-- | compressor/offline/django.py | 143 | ||||
| -rw-r--r-- | compressor/offline/jinja2.py | 125 |
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 |
