Repeat blocks with new context / simple Jinja-like macro system

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
from django import template
from django.template import TemplateSyntaxError

register = template.Library()

"""
    The MacroRoot node (= %enablemacros% tag) functions quite similar to 
    the ExtendsNode from django.template.loader_tags. It will capture 
    everything that follows, and thus should be one of the first tags in 
    the template. Because %extends% also needs to be the first, if you are 
    using template inheritance, use %extends_with_macros% instead.
    
    This whole procedure is necessary because otherwise we would have no
    possiblity to access the blocktag referenced by a %repeat% (we could 
    do it for %macro%, but not for %block%, at least not without patching
    the django source).
    
    So what we do is add a custom attribute to the parser object and store
    a reference to the MacroRoot node there, which %repeat% object will
    later be able to access when they need to find a block.
    
    Apart from that, the node doesn't do much, except rendering it's childs.
"""
class MacroRoot(template.Node):
    def __init__(self, nodelist=[]):
        self.nodelist = nodelist
        
    def render(self, context):
        return self.nodelist.render(context)
        
    def find(self, block_name, parent_nodelist=None):        
        # parent_nodelist is internally for recusion, start with root nodelist
        if parent_nodelist is None: parent_nodelist = self.nodelist
        
        from django.template.loader_tags import BlockNode
        for node in parent_nodelist:
            if isinstance(node, (MacroNode, BlockNode)):                
                if node.name == block_name:
                    return node
            if hasattr(node, 'nodelist'):               
                result = self.find(block_name, node.nodelist)
                if result:
                    return result
        return None # nothing found
        
def do_enablemacros(parser, token):
    # check that there are no arguments
    bits = token.split_contents()
    if len(bits) != 1:
        raise TemplateSyntaxError, "'%s' takes no arguments" % bits[0]
    # create the Node object now, so we can assign it to the parser
    # before we continue with our call to parse(). this enables repeat
    # tags that follow later to already enforce at the parsing stage
    # that macros are correctly enabled.
    parser._macro_root = MacroRoot()
    # capture the rest of the template
    nodelist = parser.parse()
    if nodelist.get_nodes_by_type(MacroRoot):
        raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0]
    # update the nodelist on the previously created MacroRoot node and
    # return  it.
    parser._macro_root.nodelist = nodelist
    return parser._macro_root
    
def do_extends_with_macros(parser, token):
    from django.template.loader_tags import do_extends
    # parse it as an ExtendsNode, but also create a fake MacroRoot node
    # and add it to the parser, like we do in do_enablemacros().
    parser._macro_root = MacroRoot()
    extendsnode = do_extends(parser, token)
    parser._macro_root.nodelist = extendsnode.nodelist
    return extendsnode

"""
    %macro% is pretty much exactly like a %block%. Both can be repeated, but
    the macro does not output it's content by itself, but *only* if it is 
    called via a %repeat% tag.    
"""

from django.template.loader_tags import BlockNode, do_block

class MacroNode(BlockNode):
    def render(self, context):
        return ''
        
    # the render that actually works
    def repeat(self, context):        
        return super(MacroNode, self).render(context)

def do_macro(parser, token):
    # let the block parse itself
    result = do_block(parser, token)
    # "upgrade" the BlockNode to a MacroNode and return it. Yes, I was not 
    # completely comfortable with it either at first, but Google says it's ok.
    result.__class__ = MacroNode
    return result
      
"""
    This (the %repeast%) is the heart of the macro system. It will try to 
    find the specified %macro% or %block% tag and render it with the most 
    up-to-date context, including any number of additional parameters passed 
    to the repeat-tag itself.
"""    
class RepeatNode(template.Node):    
    def __init__(self, block_name, macro_root, extra_context):
        self.block_name = block_name
        self.macro_root = macro_root
        self.extra_context = extra_context

    def render(self, context):
        block = self.macro_root.find(self.block_name)
        if not block:
            # apparently we are not supposed to raise exceptions at rendering 
            # stage, but this is serious, and we cannot do it while parsing.
            # once again, it comes down to being able to support repeating of
            # standard blocks. If we would only support our own %macro% tags,
            # we would not need the whole %enablemacros% stuff and could do
            # things differently.
            raise TemplateSyntaxError, "cannot repeat '%s': block or macro not found" % self.block_name
        else:
            # resolve extra context variables
            resolved_context = {}
            for key, value in self.extra_context.items():
                resolved_context[key] = value.resolve(context)
            # render the block with the new context
            context.update(resolved_context)
            if isinstance(block, MacroNode):
                result = block.repeat(context)
            else:
                result = block.render(context)
            context.pop()
            return result

def do_repeat(parser, token):
    # Stolen from django.templatetags.i18n.BlockTranslateParser
    # Parses something like  "with x as y, i as j", and 
    # returns it as a  context dict.
    class RepeatTagParser(template.TokenParser):
        def top(self):
            extra_context = {}
            # first tag is the blockname
            try: block_name = self.tag()
            except TemplateSyntaxError: 
                raise TemplateSyntaxError("'%s' requires a block or macro name" % self.tagname)
            # read param bindings
            while self.more():
                tag = self.tag()
                if tag == 'with' or tag == 'and':
                    value = self.value()
                    if self.tag() != 'as':
                        raise TemplateSyntaxError, "variable bindings in %s must be 'with value as variable'" % self.tagname
                    extra_context[self.tag()] = parser.compile_filter(value)                
                else:
                    raise TemplateSyntaxError, "unknown subtag %s for '%s' found" % (tag, self.tagname)
            return self.tagname, block_name, extra_context
    
    # parse arguments
    (tag_name, block_name, extra_context) = \
        RepeatTagParser(token.contents).top()
    # return as a RepeatNode
    if not hasattr(parser, '_macro_root'):
        raise TemplateSyntaxError, "'%s' requires macros to be enabled first" % tag_name
    return RepeatNode(block_name, parser._macro_root, extra_context)

# register all our tags
register.tag('repeat', do_repeat)
register.tag('macro', do_macro)
register.tag('enablemacros', do_enablemacros)
register.tag('extends_with_macros', do_extends_with_macros)

More like this

  1. Markdown and Syntax Highlighting in Django by blinks 7 years, 1 month ago
  2. testdata tag for templates by showell 4 years, 11 months ago
  3. Render specific blocks from templates (useful for AJAX) by sciyoshi 5 years, 11 months ago
  4. FCKEditor replace all vLargeTextField in admin by aronchi 5 years, 6 months ago
  5. Showell markup--DRY up your templates by showell 4 years, 4 months ago

Comments

diegor (on December 11, 2007):

{% load macro %} <- load the macro.py

{% enablemacros %}

{% macro bar %}

macro: {{ pm }}

{% endblock %}

{% repeat bar with pm as 'fooo' %}

{% repeat bar with pm as 'fooooo' %}

This print twice only "macro:" without 'fooo' or 'fooooo'. Any ideas about this reaction?

#

miracle2k (on December 22, 2007):

The docs on this were wrong, you have to use:

{% repeat bar with "fooooo" as bar %}

#

miracle2k (on December 22, 2007):

Sorry:

{% repeat bar with "fooooo" as pm %}

#

(Forgotten your password?)