Tag library that provides support for macros in Django templates.
Usage example:
0) Save this file as
<yourapp>/templatetags/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 }}
{% endmacro %}`
3a) Use the macro with a String parameter:
{% usemacro my_macro "String parameter" %}
3b) or with a variable parameter (provided the context defines 'somearg' variable, e.g. with value "Variable parameter"):
{% usemacro my_macro somearg %}
3c) !!NEW!! usemacro
now also supports filters attached to parameters:
{% usemacro my_macro "lowercase parameter"|upper %}
Output of the above code would be:
Parameter: String parameter
Parameter: Variable parameter
Parameter: LOWERCASE PARAMETER
4) !!NEW!! Alternatively save your macros in a separate file, e.g. "mymacros.html" and load it into the current template with:
{% loadmacros "mymacros.html" %}
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 Macros are local to each template file and are not inherited through {% extends ... %}
blocks.
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 <[email protected]>
# 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
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
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.
#
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
#
#
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.
#
Here's a gist that seems to have adapted this, but has been updated more recently: https://gist.github.com/skyl/1715202
#
Please login first before commenting.