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)