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)

More like this

  1. Database file storage by powerfox 5 years, 2 months ago
  2. "Partial Templates" - an alternative to "include" by vigrid 5 years, 2 months ago
  3. template tag for highlighting currently active page by adunar 5 years, 5 months ago
  4. Easy Conditional Template Tags by fragsworth 4 years, 10 months ago
  5. Tags & filters for rendering search results by exogen 6 years ago

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 ;-)

#

ffsffd (on July 27, 2011):

I just converted the tag to be able to use args and *kwargs, since I badly needed that in one of my templates. Saved me from having to make a template tag just for that, which would've been insane. This one's better since it's reusable, and can go in the same file as a macro. :)

#

ffsffd (on July 27, 2011):

Correction (syntax highlighting messed it up):

*args and **kwargs

#

pyleaf (on November 23, 2011):

just like flask/jsp

#

gwrtheyrn (on April 6, 2012):

The imports can be reduced to the following:

54 from django import template
55 from django.template import FilterExpression
56 from django.template.loader import get_template
57 import re

The other imports are unused.

#

qmax (on August 8, 2012):

macros args overwrite global context

there should be: UseMacroNode.render(self, context): context.push() for (arg, fe) in [(self.args[i], self.filter_expressions[i]) for i in range(len(self.args))]: context[arg] = fe.resolve(context) result = self.nodelist.render(context) context.pop() return result

#

qmax (on August 8, 2012):
def render(self, context):
    context.push()
    for (arg, fe) in [(self.args[i], self.filter_expressions[i]) for i in range(len(self.args))]:
        context[arg] = fe.resolve(context)
    result = self.nodelist.render(context)
    context.pop()
    return result

#

erwinjulius (on November 10, 2012):

This code is not compatible with django 1.4.

There is no find_template_source on django.template.loader. But it was easy to resolve. Just removed it as it's not being used.

#

seafangs (on February 7, 2013):

Here's a gist that seems to have adapted this, but has been updated more recently: https://gist.github.com/skyl/1715202

#

(Forgotten your password?)