Support for {% macro %} tags in templates, version 2

  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
# 
# templatetags/macros.py - Support for macros in Django templates
# 
# Author: Michal Ludvig <michal@logix.cz>
#         http://www.logix.cz/michal
# 

"""
Tag library that provides support for "macros" in
Django templates.

Usage example:

0) Save this file as
        <yourapp>/taglibrary/macros.py

1) In your template load the library:
        {% load macros %}

2) Define a new macro called 'my_macro' with
   parameter 'arg1':
        {% macro my_macro arg1 %}
        Parameter: {{ arg1 }} <br/>
        {% endmacro %}

3) Use the macro with a String parameter:
        {% usemacro my_macro "String parameter" %}

   or with a variable parameter (provided the 
   context defines 'somearg' variable, e.g. with
   value "Variable parameter"):
        {% usemacro my_macro somearg %}

   The output of the above code would be:
        Parameter: String parameter <br/>
        Parameter: Variable parameter <br/>

4) Alternatively save your macros in a separate
   file, e.g. "mymacros.html" and load it to the 
   current template with:
        {% loadmacros "mymacros.html" %}
   Then use these loaded macros in {% usemacro %} 
   as described above.

Macros can take zero or more arguments and both
context variables and macro arguments are resolved
in macro body when used in {% usemacro ... %} tag.

Bear in mind that defined and loaded Macros are local 
to each template file and are not inherited 
through {% extends ... %} tags.
"""

from django import template
from django.template import resolve_variable, FilterExpression
from django.template.loader import get_template, get_template_from_string, find_template_source
from django.conf import settings
import re

register = template.Library()

def _setup_macros_dict(parser):
    ## Metadata of each macro are stored in a new attribute 
    ## of 'parser' class. That way we can access it later
    ## in the template when processing 'usemacro' tags.
    try:
        ## Only try to access it to eventually trigger an exception
        parser._macros
    except AttributeError:
        parser._macros = {}

class DefineMacroNode(template.Node):
    def __init__(self, name, nodelist, args):
        self.name = name
        self.nodelist = nodelist
        self.args = args

    def render(self, context):
        ## empty string - {% macro %} tag does no output
        return ''

@register.tag(name="macro")
def do_macro(parser, token):
    try:
        args = token.split_contents()
        tag_name, macro_name, args = args[0], args[1], args[2:]
    except IndexError:
        raise template.TemplateSyntaxError, "'%s' tag requires at least one argument (macro name)" % token.contents.split()[0]
    # TODO: check that 'args' are all simple strings ([a-zA-Z0-9_]+)
    r_valid_arg_name = re.compile(r'^[a-zA-Z0-9_]+$')
    for arg in args:
        if not r_valid_arg_name.match(arg):
            raise template.TemplateSyntaxError, "Argument '%s' to macro '%s' contains illegal characters. Only alphanumeric characters and '_' are allowed." % (arg, macro_name)
    nodelist = parser.parse(('endmacro', ))
    parser.delete_first_token()

    ## Metadata of each macro are stored in a new attribute 
    ## of 'parser' class. That way we can access it later
    ## in the template when processing 'usemacro' tags.
    _setup_macros_dict(parser)

    parser._macros[macro_name] = DefineMacroNode(macro_name, nodelist, args)
    return parser._macros[macro_name]

class LoadMacrosNode(template.Node):
    def render(self, context):
        ## empty string - {% loadmacros %} tag does no output
        return ''

@register.tag(name="loadmacros")
def do_loadmacros(parser, token):
    try:
        tag_name, filename = token.split_contents()
    except IndexError:
        raise template.TemplateSyntaxError, "'%s' tag requires at least one argument (macro name)" % token.contents.split()[0]
    if filename[0] in ('"', "'") and filename[-1] == filename[0]:
        filename = filename[1:-1]
    t = get_template(filename)
    macros = t.nodelist.get_nodes_by_type(DefineMacroNode)
    ## Metadata of each macro are stored in a new attribute 
    ## of 'parser' class. That way we can access it later
    ## in the template when processing 'usemacro' tags.
    _setup_macros_dict(parser)
    for macro in macros:
        parser._macros[macro.name] = macro
    return LoadMacrosNode()
    
class UseMacroNode(template.Node):
    def __init__(self, macro, filter_expressions):
        self.nodelist = macro.nodelist
        self.args = macro.args
        self.filter_expressions = filter_expressions
    def render(self, context):
        for (arg, fe) in [(self.args[i], self.filter_expressions[i]) for i in range(len(self.args))]:
            context[arg] = fe.resolve(context)
        return self.nodelist.render(context)

@register.tag(name="usemacro")
def do_usemacro(parser, token):
    try:
        args = token.split_contents()
        tag_name, macro_name, values = args[0], args[1], args[2:]
    except IndexError:
        raise template.TemplateSyntaxError, "'%s' tag requires at least one argument (macro name)" % token.contents.split()[0]
    try:
        macro = parser._macros[macro_name]
    except (AttributeError, KeyError):
        raise template.TemplateSyntaxError, "Macro '%s' is not defined" % macro_name

    if (len(values) != len(macro.args)):
        raise template.TemplateSyntaxError, "Macro '%s' was declared with %d parameters and used with %d parameter" % (
            macro_name,
            len(macro.args),
            len(values))
    filter_expressions = []
    for val in values:
        if (val[0] == "'" or val[0] == '"') and (val[0] != val[-1]):
            raise template.TemplateSyntaxError, "Non-terminated string argument: %s" % val[1:]
        filter_expressions.append(FilterExpression(val, parser))
    return UseMacroNode(macro, filter_expressions)

Comments

miracle2k (on August 11, 2007):

I think when you're adding variables to the context during a use_macro call, they are not cleared afterwards, and will be available outside of the macro (although I haven't tested it, so I might be mistaken).

But you probably want to use context.push() to add a new context dict to the stack, and context.pop() when you're done.

#

santuri (on September 16, 2007):

Awesome instructions. Truth be told, I haven't even looked over the code yet, just installed the tag, followed the instructions and chopped a ton of fat out of one of my templates! Thanks!

#

Leo Hourvitz (on October 3, 2007):

Cool stuff. When I tried it inside a {% for ... %} block though, I got an exception. I think it's because in UseMacroNode.render() where it says:

    for arg in self.args:
        val = self.values.pop(0)

the values in the Node get stripped out on the first time the UseMacroNode is parsed, so the second time around the {% for %} loop there's nothing there. Instead, I changed to a list comprehension and it works fine:

   for (arg,val) in [(self.args[i],self.values[i]) for i in range(len(self.args))]:

#

mludvig (on December 18, 2007):

I have just uploaded much improved version with:

  • support for {% loadmacros %} tag
  • support for filters in {% usemacro name xyz|filter $}
  • fixed error when used in the {% for %} loop (thanks for a hint, Leo).

It's getting pretty usable now ;-)

#

(Forgotten your password?)

You may use Markdown syntax here, but raw HTML will be removed.