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
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.
#
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!
#
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:
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:
#
I have just uploaded much improved version with:
{% loadmacros %}tag{% usemacro name xyz|filter $}{% for %}loop (thanks for a hint, Leo).It's getting pretty usable now ;-)
#
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. :)
#
Correction (syntax highlighting messed it up):
*args and **kwargs
#
just like flask/jsp
#
The imports can be reduced to the following:
The other imports are unused.
#