181a497d783450f096843aaa50c0ec7fe3e07b47
[fnpdjango.git] / fnpdjango / templatetags / macros.py
1
2 # templatetags/macros.py - Support for macros in Django templates
3
4 # Author: Michal Ludvig <michal@logix.cz>
5 #         http://www.logix.cz/michal
6
7
8 """
9 Tag library that provides support for "macros" in
10 Django templates.
11
12 Usage example:
13
14 0) Save this file as
15         <yourapp>/taglibrary/macros.py
16
17 1) In your template load the library:
18         {% load macros %}
19
20 2) Define a new macro called 'my_macro' with
21    parameter 'arg1':
22         {% macro my_macro arg1 %}
23         Parameter: {{ arg1 }} <br/>
24         {% endmacro %}
25
26 3) Use the macro with a String parameter:
27         {% usemacro my_macro "String parameter" %}
28
29    or with a variable parameter (provided the 
30    context defines 'somearg' variable, e.g. with
31    value "Variable parameter"):
32         {% usemacro my_macro somearg %}
33
34    The output of the above code would be:
35         Parameter: String parameter <br/>
36         Parameter: Variable parameter <br/>
37
38 4) Alternatively save your macros in a separate
39    file, e.g. "mymacros.html" and load it to the 
40    current template with:
41         {% loadmacros "mymacros.html" %}
42    Then use these loaded macros in {% usemacro %} 
43    as described above.
44
45 Macros can take zero or more arguments and both
46 context variables and macro arguments are resolved
47 in macro body when used in {% usemacro ... %} tag.
48
49 Bear in mind that defined and loaded Macros are local 
50 to each template file and are not inherited 
51 through {% extends ... %} tags.
52 """
53
54 from django import template
55 from django.template import resolve_variable, FilterExpression
56 from django.template.loader import get_template, get_template_from_string, find_template_source
57 from django.conf import settings
58 import re
59
60 register = template.Library()
61
62 def _setup_macros_dict(parser):
63     ## Metadata of each macro are stored in a new attribute 
64     ## of 'parser' class. That way we can access it later
65     ## in the template when processing 'usemacro' tags.
66     try:
67         ## Only try to access it to eventually trigger an exception
68         parser._macros
69     except AttributeError:
70         parser._macros = {}
71
72 class DefineMacroNode(template.Node):
73     def __init__(self, name, nodelist, args):
74         self.name = name
75         self.nodelist = nodelist
76         self.args = args
77
78     def render(self, context):
79         ## empty string - {% macro %} tag does no output
80         return ''
81
82 @register.tag(name="macro")
83 def do_macro(parser, token):
84     try:
85         args = token.split_contents()
86         tag_name, macro_name, args = args[0], args[1], args[2:]
87     except IndexError:
88         raise template.TemplateSyntaxError, "'%s' tag requires at least one argument (macro name)" % token.contents.split()[0]
89     # TODO: check that 'args' are all simple strings ([a-zA-Z0-9_]+)
90     r_valid_arg_name = re.compile(r'^[a-zA-Z0-9_]+$')
91     for arg in args:
92         if not r_valid_arg_name.match(arg):
93             raise template.TemplateSyntaxError, "Argument '%s' to macro '%s' contains illegal characters. Only alphanumeric characters and '_' are allowed." % (arg, macro_name)
94     nodelist = parser.parse(('endmacro', ))
95     parser.delete_first_token()
96
97     ## Metadata of each macro are stored in a new attribute 
98     ## of 'parser' class. That way we can access it later
99     ## in the template when processing 'usemacro' tags.
100     _setup_macros_dict(parser)
101
102     parser._macros[macro_name] = DefineMacroNode(macro_name, nodelist, args)
103     return parser._macros[macro_name]
104
105 class LoadMacrosNode(template.Node):
106     def render(self, context):
107         ## empty string - {% loadmacros %} tag does no output
108         return ''
109
110 @register.tag(name="loadmacros")
111 def do_loadmacros(parser, token):
112     try:
113         tag_name, filename = token.split_contents()
114     except IndexError:
115         raise template.TemplateSyntaxError, "'%s' tag requires at least one argument (macro name)" % token.contents.split()[0]
116     if filename[0] in ('"', "'") and filename[-1] == filename[0]:
117         filename = filename[1:-1]
118     t = get_template(filename)
119     macros = t.nodelist.get_nodes_by_type(DefineMacroNode)
120     ## Metadata of each macro are stored in a new attribute 
121     ## of 'parser' class. That way we can access it later
122     ## in the template when processing 'usemacro' tags.
123     _setup_macros_dict(parser)
124     for macro in macros:
125         parser._macros[macro.name] = macro
126     return LoadMacrosNode()
127     
128 class UseMacroNode(template.Node):
129     def __init__(self, macro, filter_expressions):
130         self.nodelist = macro.nodelist
131         self.args = macro.args
132         self.filter_expressions = filter_expressions
133     def render(self, context):
134         for (arg, fe) in [(self.args[i], self.filter_expressions[i]) for i in range(len(self.args))]:
135             context[arg] = fe.resolve(context)
136         return self.nodelist.render(context)
137
138 @register.tag(name="usemacro")
139 def do_usemacro(parser, token):
140     try:
141         args = token.split_contents()
142         tag_name, macro_name, values = args[0], args[1], args[2:]
143     except IndexError:
144         raise template.TemplateSyntaxError, "'%s' tag requires at least one argument (macro name)" % token.contents.split()[0]
145     try:
146         macro = parser._macros[macro_name]
147     except (AttributeError, KeyError):
148         raise template.TemplateSyntaxError, "Macro '%s' is not defined" % macro_name
149
150     if (len(values) != len(macro.args)):
151         raise template.TemplateSyntaxError, "Macro '%s' was declared with %d parameters and used with %d parameter" % (
152             macro_name,
153             len(macro.args),
154             len(values))
155     filter_expressions = []
156     for val in values:
157         if (val[0] == "'" or val[0] == '"') and (val[0] != val[-1]):
158             raise template.TemplateSyntaxError, "Non-terminated string argument: %s" % val[1:]
159         filter_expressions.append(FilterExpression(val, parser))
160     return UseMacroNode(macro, filter_expressions)