| 1 | """
|
|---|
| 2 | This is the Django template system.
|
|---|
| 3 |
|
|---|
| 4 | How it works:
|
|---|
| 5 |
|
|---|
| 6 | The Lexer.tokenize() function converts a template string (i.e., a string containing
|
|---|
| 7 | markup with custom template tags) to tokens, which can be either plain text
|
|---|
| 8 | (TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK).
|
|---|
| 9 |
|
|---|
| 10 | The Parser() class takes a list of tokens in its constructor, and its parse()
|
|---|
| 11 | method returns a compiled template -- which is, under the hood, a list of
|
|---|
| 12 | Node objects.
|
|---|
| 13 |
|
|---|
| 14 | Each Node is responsible for creating some sort of output -- e.g. simple text
|
|---|
| 15 | (TextNode), variable values in a given context (VariableNode), results of basic
|
|---|
| 16 | logic (IfNode), results of looping (ForNode), or anything else. The core Node
|
|---|
| 17 | types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can
|
|---|
| 18 | define their own custom node types.
|
|---|
| 19 |
|
|---|
| 20 | Each Node has a render() method, which takes a Context and returns a string of
|
|---|
| 21 | the rendered node. For example, the render() method of a Variable Node returns
|
|---|
| 22 | the variable's value as a string. The render() method of an IfNode returns the
|
|---|
| 23 | rendered output of whatever was inside the loop, recursively.
|
|---|
| 24 |
|
|---|
| 25 | The Template class is a convenient wrapper that takes care of template
|
|---|
| 26 | compilation and rendering.
|
|---|
| 27 |
|
|---|
| 28 | Usage:
|
|---|
| 29 |
|
|---|
| 30 | The only thing you should ever use directly in this file is the Template class.
|
|---|
| 31 | Create a compiled template object with a template_string, then call render()
|
|---|
| 32 | with a context. In the compilation stage, the TemplateSyntaxError exception
|
|---|
| 33 | will be raised if the template doesn't have proper syntax.
|
|---|
| 34 |
|
|---|
| 35 | Sample code:
|
|---|
| 36 |
|
|---|
| 37 | >>> import template
|
|---|
| 38 | >>> s = '''
|
|---|
| 39 | ... <html>
|
|---|
| 40 | ... {% if test %}
|
|---|
| 41 | ... <h1>{{ varvalue }}</h1>
|
|---|
| 42 | ... {% endif %}
|
|---|
| 43 | ... </html>
|
|---|
| 44 | ... '''
|
|---|
| 45 | >>> t = template.Template(s)
|
|---|
| 46 |
|
|---|
| 47 | (t is now a compiled template, and its render() method can be called multiple
|
|---|
| 48 | times with multiple contexts)
|
|---|
| 49 |
|
|---|
| 50 | >>> c = template.Context({'test':True, 'varvalue': 'Hello'})
|
|---|
| 51 | >>> t.render(c)
|
|---|
| 52 | '\n<html>\n\n <h1>Hello</h1>\n\n</html>\n'
|
|---|
| 53 | >>> c = template.Context({'test':False, 'varvalue': 'Hello'})
|
|---|
| 54 | >>> t.render(c)
|
|---|
| 55 | '\n<html>\n\n</html>\n'
|
|---|
| 56 | """
|
|---|
| 57 | import re
|
|---|
| 58 | from inspect import getargspec
|
|---|
| 59 | from django.conf import settings
|
|---|
| 60 | from django.template.context import Context, RequestContext, ContextPopException
|
|---|
| 61 | from django.utils.functional import curry
|
|---|
| 62 | from django.utils.text import smart_split
|
|---|
| 63 |
|
|---|
| 64 | __all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
|
|---|
| 65 |
|
|---|
| 66 | TOKEN_TEXT = 0
|
|---|
| 67 | TOKEN_VAR = 1
|
|---|
| 68 | TOKEN_BLOCK = 2
|
|---|
| 69 |
|
|---|
| 70 | # template syntax constants
|
|---|
| 71 | FILTER_SEPARATOR = '|'
|
|---|
| 72 | FILTER_ARGUMENT_SEPARATOR = ':'
|
|---|
| 73 | VARIABLE_ATTRIBUTE_SEPARATOR = '.'
|
|---|
| 74 | BLOCK_TAG_START = '{%'
|
|---|
| 75 | BLOCK_TAG_END = '%}'
|
|---|
| 76 | VARIABLE_TAG_START = '{{'
|
|---|
| 77 | VARIABLE_TAG_END = '}}'
|
|---|
| 78 | SINGLE_VARIABLE_TAG_START = '{'
|
|---|
| 79 | SINGLE_VARIABLE_TAG_END = '}'
|
|---|
| 80 |
|
|---|
| 81 | ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'
|
|---|
| 82 |
|
|---|
| 83 | # what to report as the origin for templates that come from non-loader sources
|
|---|
| 84 | # (e.g. strings)
|
|---|
| 85 | UNKNOWN_SOURCE="<unknown source>"
|
|---|
| 86 |
|
|---|
| 87 | # match a variable or block tag and capture the entire tag, including start/end delimiters
|
|---|
| 88 | tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
|
|---|
| 89 | re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))
|
|---|
| 90 |
|
|---|
| 91 | # global dictionary of libraries that have been loaded using get_library
|
|---|
| 92 | libraries = {}
|
|---|
| 93 | # global list of libraries to load by default for a new parser
|
|---|
| 94 | builtins = []
|
|---|
| 95 |
|
|---|
| 96 | class TemplateSyntaxError(Exception):
|
|---|
| 97 | def __str__(self):
|
|---|
| 98 | try:
|
|---|
| 99 | import cStringIO as StringIO
|
|---|
| 100 | except ImportError:
|
|---|
| 101 | import StringIO
|
|---|
| 102 | output = StringIO.StringIO()
|
|---|
| 103 | output.write(Exception.__str__(self))
|
|---|
| 104 | # Check if we wrapped an exception and print that too.
|
|---|
| 105 | if hasattr(self, 'exc_info'):
|
|---|
| 106 | import traceback
|
|---|
| 107 | output.write('\n\nOriginal ')
|
|---|
| 108 | e = self.exc_info
|
|---|
| 109 | traceback.print_exception(e[0], e[1], e[2], 500, output)
|
|---|
| 110 | return output.getvalue()
|
|---|
| 111 |
|
|---|
| 112 | class TemplateDoesNotExist(Exception):
|
|---|
| 113 | pass
|
|---|
| 114 |
|
|---|
| 115 | class VariableDoesNotExist(Exception):
|
|---|
| 116 | pass
|
|---|
| 117 |
|
|---|
| 118 | class InvalidTemplateLibrary(Exception):
|
|---|
| 119 | pass
|
|---|
| 120 |
|
|---|
| 121 | class Origin(object):
|
|---|
| 122 | def __init__(self, name):
|
|---|
| 123 | self.name = name
|
|---|
| 124 |
|
|---|
| 125 | def reload(self):
|
|---|
| 126 | raise NotImplementedError
|
|---|
| 127 |
|
|---|
| 128 | def __str__(self):
|
|---|
| 129 | return self.name
|
|---|
| 130 |
|
|---|
| 131 | class StringOrigin(Origin):
|
|---|
| 132 | def __init__(self, source):
|
|---|
| 133 | super(StringOrigin, self).__init__(UNKNOWN_SOURCE)
|
|---|
| 134 | self.source = source
|
|---|
| 135 |
|
|---|
| 136 | def reload(self):
|
|---|
| 137 | return self.source
|
|---|
| 138 |
|
|---|
| 139 | class Template(object):
|
|---|
| 140 | def __init__(self, template_string, origin=None):
|
|---|
| 141 | "Compilation stage"
|
|---|
| 142 | if settings.TEMPLATE_DEBUG and origin == None:
|
|---|
| 143 | origin = StringOrigin(template_string)
|
|---|
| 144 | # Could do some crazy stack-frame stuff to record where this string
|
|---|
| 145 | # came from...
|
|---|
| 146 | self.nodelist = compile_string(template_string, origin)
|
|---|
| 147 |
|
|---|
| 148 | def __iter__(self):
|
|---|
| 149 | for node in self.nodelist:
|
|---|
| 150 | for subnode in node:
|
|---|
| 151 | yield subnode
|
|---|
| 152 |
|
|---|
| 153 | def render(self, context):
|
|---|
| 154 | "Display stage -- can be called many times"
|
|---|
| 155 | return self.nodelist.render(context)
|
|---|
| 156 |
|
|---|
| 157 | def compile_string(template_string, origin):
|
|---|
| 158 | "Compiles template_string into NodeList ready for rendering"
|
|---|
| 159 | lexer = lexer_factory(template_string, origin)
|
|---|
| 160 | parser = parser_factory(lexer.tokenize())
|
|---|
| 161 | return parser.parse()
|
|---|
| 162 |
|
|---|
| 163 | class Token(object):
|
|---|
| 164 | def __init__(self, token_type, contents):
|
|---|
| 165 | "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK"
|
|---|
| 166 | self.token_type, self.contents = token_type, contents
|
|---|
| 167 |
|
|---|
| 168 | def __str__(self):
|
|---|
| 169 | return '<%s token: "%s...">' % \
|
|---|
| 170 | ({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type],
|
|---|
| 171 | self.contents[:20].replace('\n', ''))
|
|---|
| 172 |
|
|---|
| 173 | def split_contents(self):
|
|---|
| 174 | return smart_split(self.contents)
|
|---|
| 175 |
|
|---|
| 176 | class Lexer(object):
|
|---|
| 177 | def __init__(self, template_string, origin):
|
|---|
| 178 | self.template_string = template_string
|
|---|
| 179 | self.origin = origin
|
|---|
| 180 |
|
|---|
| 181 | def tokenize(self):
|
|---|
| 182 | "Return a list of tokens from a given template_string"
|
|---|
| 183 | # remove all empty strings, because the regex has a tendency to add them
|
|---|
| 184 | bits = filter(None, tag_re.split(self.template_string))
|
|---|
| 185 | return map(self.create_token, bits)
|
|---|
| 186 |
|
|---|
| 187 | def create_token(self,token_string):
|
|---|
| 188 | "Convert the given token string into a new Token object and return it"
|
|---|
| 189 | if token_string.startswith(VARIABLE_TAG_START):
|
|---|
| 190 | token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
|
|---|
| 191 | elif token_string.startswith(BLOCK_TAG_START):
|
|---|
| 192 | token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
|
|---|
| 193 | else:
|
|---|
| 194 | token = Token(TOKEN_TEXT, token_string)
|
|---|
| 195 | return token
|
|---|
| 196 |
|
|---|
| 197 | class DebugLexer(Lexer):
|
|---|
| 198 | def __init__(self, template_string, origin):
|
|---|
| 199 | super(DebugLexer, self).__init__(template_string, origin)
|
|---|
| 200 |
|
|---|
| 201 | def tokenize(self):
|
|---|
| 202 | "Return a list of tokens from a given template_string"
|
|---|
| 203 | token_tups, upto = [], 0
|
|---|
| 204 | for match in tag_re.finditer(self.template_string):
|
|---|
| 205 | start, end = match.span()
|
|---|
| 206 | if start > upto:
|
|---|
| 207 | token_tups.append( (self.template_string[upto:start], (upto, start)) )
|
|---|
| 208 | upto = start
|
|---|
| 209 | token_tups.append( (self.template_string[start:end], (start,end)) )
|
|---|
| 210 | upto = end
|
|---|
| 211 | last_bit = self.template_string[upto:]
|
|---|
| 212 | if last_bit:
|
|---|
| 213 | token_tups.append( (last_bit, (upto, upto + len(last_bit))) )
|
|---|
| 214 | return [self.create_token(tok, (self.origin, loc)) for tok, loc in token_tups]
|
|---|
| 215 |
|
|---|
| 216 | def create_token(self, token_string, source):
|
|---|
| 217 | token = super(DebugLexer, self).create_token(token_string)
|
|---|
| 218 | token.source = source
|
|---|
| 219 | return token
|
|---|
| 220 |
|
|---|
| 221 | class Parser(object):
|
|---|
| 222 | def __init__(self, tokens):
|
|---|
| 223 | self.tokens = tokens
|
|---|
| 224 | self.tags = {}
|
|---|
| 225 | self.filters = {}
|
|---|
| 226 | for lib in builtins:
|
|---|
| 227 | self.add_library(lib)
|
|---|
| 228 |
|
|---|
| 229 | def parse(self, parse_until=None):
|
|---|
| 230 | if parse_until is None: parse_until = []
|
|---|
| 231 | nodelist = self.create_nodelist()
|
|---|
| 232 | while self.tokens:
|
|---|
| 233 | token = self.next_token()
|
|---|
| 234 | if token.token_type == TOKEN_TEXT:
|
|---|
| 235 | self.extend_nodelist(nodelist, TextNode(token.contents), token)
|
|---|
| 236 | elif token.token_type == TOKEN_VAR:
|
|---|
| 237 | if not token.contents:
|
|---|
| 238 | self.empty_variable(token)
|
|---|
| 239 | filter_expression = self.compile_filter(token.contents)
|
|---|
| 240 | var_node = self.create_variable_node(filter_expression)
|
|---|
| 241 | self.extend_nodelist(nodelist, var_node,token)
|
|---|
| 242 | elif token.token_type == TOKEN_BLOCK:
|
|---|
| 243 | if token.contents in parse_until:
|
|---|
| 244 | # put token back on token list so calling code knows why it terminated
|
|---|
| 245 | self.prepend_token(token)
|
|---|
| 246 | return nodelist
|
|---|
| 247 | try:
|
|---|
| 248 | command = token.contents.split()[0]
|
|---|
| 249 | except IndexError:
|
|---|
| 250 | self.empty_block_tag(token)
|
|---|
| 251 | # execute callback function for this tag and append resulting node
|
|---|
| 252 | self.enter_command(command, token)
|
|---|
| 253 | try:
|
|---|
| 254 | compile_func = self.tags[command]
|
|---|
| 255 | except KeyError:
|
|---|
| 256 | self.invalid_block_tag(token, command)
|
|---|
| 257 | try:
|
|---|
| 258 | compiled_result = compile_func(self, token)
|
|---|
| 259 | except TemplateSyntaxError, e:
|
|---|
| 260 | if not self.compile_function_error(token, e):
|
|---|
| 261 | raise
|
|---|
| 262 | self.extend_nodelist(nodelist, compiled_result, token)
|
|---|
| 263 | self.exit_command()
|
|---|
| 264 | if parse_until:
|
|---|
| 265 | self.unclosed_block_tag(parse_until)
|
|---|
| 266 | return nodelist
|
|---|
| 267 |
|
|---|
| 268 | def skip_past(self, endtag):
|
|---|
| 269 | while self.tokens:
|
|---|
| 270 | token = self.next_token()
|
|---|
| 271 | if token.token_type == TOKEN_BLOCK and token.contents == endtag:
|
|---|
| 272 | return
|
|---|
| 273 | self.unclosed_block_tag([endtag])
|
|---|
| 274 |
|
|---|
| 275 | def create_variable_node(self, filter_expression):
|
|---|
| 276 | return VariableNode(filter_expression)
|
|---|
| 277 |
|
|---|
| 278 | def create_nodelist(self):
|
|---|
| 279 | return NodeList()
|
|---|
| 280 |
|
|---|
| 281 | def extend_nodelist(self, nodelist, node, token):
|
|---|
| 282 | nodelist.append(node)
|
|---|
| 283 |
|
|---|
| 284 | def enter_command(self, command, token):
|
|---|
| 285 | pass
|
|---|
| 286 |
|
|---|
| 287 | def exit_command(self):
|
|---|
| 288 | pass
|
|---|
| 289 |
|
|---|
| 290 | def error(self, token, msg ):
|
|---|
| 291 | return TemplateSyntaxError(msg)
|
|---|
| 292 |
|
|---|
| 293 | def empty_variable(self, token):
|
|---|
| 294 | raise self.error( token, "Empty variable tag")
|
|---|
| 295 |
|
|---|
| 296 | def empty_block_tag(self, token):
|
|---|
| 297 | raise self.error( token, "Empty block tag")
|
|---|
| 298 |
|
|---|
| 299 | def invalid_block_tag(self, token, command):
|
|---|
| 300 | raise self.error( token, "Invalid block tag: '%s'" % command)
|
|---|
| 301 |
|
|---|
| 302 | def unclosed_block_tag(self, parse_until):
|
|---|
| 303 | raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
|
|---|
| 304 |
|
|---|
| 305 | def compile_function_error(self, token, e):
|
|---|
| 306 | pass
|
|---|
| 307 |
|
|---|
| 308 | def next_token(self):
|
|---|
| 309 | return self.tokens.pop(0)
|
|---|
| 310 |
|
|---|
| 311 | def prepend_token(self, token):
|
|---|
| 312 | self.tokens.insert(0, token)
|
|---|
| 313 |
|
|---|
| 314 | def delete_first_token(self):
|
|---|
| 315 | del self.tokens[0]
|
|---|
| 316 |
|
|---|
| 317 | def add_library(self, lib):
|
|---|
| 318 | self.tags.update(lib.tags)
|
|---|
| 319 | self.filters.update(lib.filters)
|
|---|
| 320 |
|
|---|
| 321 | def compile_filter(self,token):
|
|---|
| 322 | "Convenient wrapper for FilterExpression"
|
|---|
| 323 | return FilterExpression(token, self)
|
|---|
| 324 |
|
|---|
| 325 | def find_filter(self, filter_name):
|
|---|
| 326 | if self.filters.has_key(filter_name):
|
|---|
| 327 | return self.filters[filter_name]
|
|---|
| 328 | else:
|
|---|
| 329 | raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name
|
|---|
| 330 |
|
|---|
| 331 | class DebugParser(Parser):
|
|---|
| 332 | def __init__(self, lexer):
|
|---|
| 333 | super(DebugParser, self).__init__(lexer)
|
|---|
| 334 | self.command_stack = []
|
|---|
| 335 |
|
|---|
| 336 | def enter_command(self, command, token):
|
|---|
| 337 | self.command_stack.append( (command, token.source) )
|
|---|
| 338 |
|
|---|
| 339 | def exit_command(self):
|
|---|
| 340 | self.command_stack.pop()
|
|---|
| 341 |
|
|---|
| 342 | def error(self, token, msg):
|
|---|
| 343 | return self.source_error(token.source, msg)
|
|---|
| 344 |
|
|---|
| 345 | def source_error(self, source,msg):
|
|---|
| 346 | e = TemplateSyntaxError(msg)
|
|---|
| 347 | e.source = source
|
|---|
| 348 | return e
|
|---|
| 349 |
|
|---|
| 350 | def create_nodelist(self):
|
|---|
| 351 | return DebugNodeList()
|
|---|
| 352 |
|
|---|
| 353 | def create_variable_node(self, contents):
|
|---|
| 354 | return DebugVariableNode(contents)
|
|---|
| 355 |
|
|---|
| 356 | def extend_nodelist(self, nodelist, node, token):
|
|---|
| 357 | node.source = token.source
|
|---|
| 358 | super(DebugParser, self).extend_nodelist(nodelist, node, token)
|
|---|
| 359 |
|
|---|
| 360 | def unclosed_block_tag(self, parse_until):
|
|---|
| 361 | (command, source) = self.command_stack.pop()
|
|---|
| 362 | msg = "Unclosed tag '%s'. Looking for one of: %s " % (command, ', '.join(parse_until))
|
|---|
| 363 | raise self.source_error( source, msg)
|
|---|
| 364 |
|
|---|
| 365 | def compile_function_error(self, token, e):
|
|---|
| 366 | if not hasattr(e, 'source'):
|
|---|
| 367 | e.source = token.source
|
|---|
| 368 |
|
|---|
| 369 | def lexer_factory(*args, **kwargs):
|
|---|
| 370 | if settings.TEMPLATE_DEBUG:
|
|---|
| 371 | return DebugLexer(*args, **kwargs)
|
|---|
| 372 | else:
|
|---|
| 373 | return Lexer(*args, **kwargs)
|
|---|
| 374 |
|
|---|
| 375 | def parser_factory(*args, **kwargs):
|
|---|
| 376 | if settings.TEMPLATE_DEBUG:
|
|---|
| 377 | return DebugParser(*args, **kwargs)
|
|---|
| 378 | else:
|
|---|
| 379 | return Parser(*args, **kwargs)
|
|---|
| 380 |
|
|---|
| 381 | class TokenParser(object):
|
|---|
| 382 | """
|
|---|
| 383 | Subclass this and implement the top() method to parse a template line. When
|
|---|
| 384 | instantiating the parser, pass in the line from the Django template parser.
|
|---|
| 385 |
|
|---|
| 386 | The parser's "tagname" instance-variable stores the name of the tag that
|
|---|
| 387 | the filter was called with.
|
|---|
| 388 | """
|
|---|
| 389 | def __init__(self, subject):
|
|---|
| 390 | self.subject = subject
|
|---|
| 391 | self.pointer = 0
|
|---|
| 392 | self.backout = []
|
|---|
| 393 | self.tagname = self.tag()
|
|---|
| 394 |
|
|---|
| 395 | def top(self):
|
|---|
| 396 | "Overload this method to do the actual parsing and return the result."
|
|---|
| 397 | raise NotImplemented
|
|---|
| 398 |
|
|---|
| 399 | def more(self):
|
|---|
| 400 | "Returns True if there is more stuff in the tag."
|
|---|
| 401 | return self.pointer < len(self.subject)
|
|---|
| 402 |
|
|---|
| 403 | def back(self):
|
|---|
| 404 | "Undoes the last microparser. Use this for lookahead and backtracking."
|
|---|
| 405 | if not len(self.backout):
|
|---|
| 406 | raise TemplateSyntaxError, "back called without some previous parsing"
|
|---|
| 407 | self.pointer = self.backout.pop()
|
|---|
| 408 |
|
|---|
| 409 | def tag(self):
|
|---|
| 410 | "A microparser that just returns the next tag from the line."
|
|---|
| 411 | subject = self.subject
|
|---|
| 412 | i = self.pointer
|
|---|
| 413 | if i >= len(subject):
|
|---|
| 414 | raise TemplateSyntaxError, "expected another tag, found end of string: %s" % subject
|
|---|
| 415 | p = i
|
|---|
| 416 | while i < len(subject) and subject[i] not in (' ', '\t'):
|
|---|
| 417 | i += 1
|
|---|
| 418 | s = subject[p:i]
|
|---|
| 419 | while i < len(subject) and subject[i] in (' ', '\t'):
|
|---|
| 420 | i += 1
|
|---|
| 421 | self.backout.append(self.pointer)
|
|---|
| 422 | self.pointer = i
|
|---|
| 423 | return s
|
|---|
| 424 |
|
|---|
| 425 | def value(self):
|
|---|
| 426 | "A microparser that parses for a value: some string constant or variable name."
|
|---|
| 427 | subject = self.subject
|
|---|
| 428 | i = self.pointer
|
|---|
| 429 | if i >= len(subject):
|
|---|
| 430 | raise TemplateSyntaxError, "Searching for value. Expected another value but found end of string: %s" % subject
|
|---|
| 431 | if subject[i] in ('"', "'"):
|
|---|
| 432 | p = i
|
|---|
| 433 | i += 1
|
|---|
| 434 | while i < len(subject) and subject[i] != subject[p]:
|
|---|
| 435 | i += 1
|
|---|
| 436 | if i >= len(subject):
|
|---|
| 437 | raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject
|
|---|
| 438 | i += 1
|
|---|
| 439 | res = subject[p:i]
|
|---|
| 440 | while i < len(subject) and subject[i] in (' ', '\t'):
|
|---|
| 441 | i += 1
|
|---|
| 442 | self.backout.append(self.pointer)
|
|---|
| 443 | self.pointer = i
|
|---|
| 444 | return res
|
|---|
| 445 | else:
|
|---|
| 446 | p = i
|
|---|
| 447 | while i < len(subject) and subject[i] not in (' ', '\t'):
|
|---|
| 448 | if subject[i] in ('"', "'"):
|
|---|
| 449 | c = subject[i]
|
|---|
| 450 | i += 1
|
|---|
| 451 | while i < len(subject) and subject[i] != c:
|
|---|
| 452 | i += 1
|
|---|
| 453 | if i >= len(subject):
|
|---|
| 454 | raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject
|
|---|
| 455 | i += 1
|
|---|
| 456 | s = subject[p:i]
|
|---|
| 457 | while i < len(subject) and subject[i] in (' ', '\t'):
|
|---|
| 458 | i += 1
|
|---|
| 459 | self.backout.append(self.pointer)
|
|---|
| 460 | self.pointer = i
|
|---|
| 461 | return s
|
|---|
| 462 |
|
|---|
| 463 |
|
|---|
| 464 |
|
|---|
| 465 |
|
|---|
| 466 | filter_raw_string = r"""
|
|---|
| 467 | ^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s|
|
|---|
| 468 | ^"(?P<constant>%(str)s)"|
|
|---|
| 469 | ^(?P<var>[%(var_chars)s]+)|
|
|---|
| 470 | (?:%(filter_sep)s
|
|---|
| 471 | (?P<filter_name>\w+)
|
|---|
| 472 | (?:%(arg_sep)s
|
|---|
| 473 | (?:
|
|---|
| 474 | %(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s|
|
|---|
| 475 | "(?P<constant_arg>%(str)s)"|
|
|---|
| 476 | (?P<var_arg>[%(var_chars)s]+)
|
|---|
| 477 | )
|
|---|
| 478 | )?
|
|---|
| 479 | )""" % {
|
|---|
| 480 | 'str': r"""[^"\\]*(?:\\.[^"\\]*)*""",
|
|---|
| 481 | 'var_chars': "A-Za-z0-9\_\." ,
|
|---|
| 482 | 'filter_sep': re.escape(FILTER_SEPARATOR),
|
|---|
| 483 | 'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
|
|---|
| 484 | 'i18n_open' : re.escape("_("),
|
|---|
| 485 | 'i18n_close' : re.escape(")"),
|
|---|
| 486 | }
|
|---|
| 487 |
|
|---|
| 488 | filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
|
|---|
| 489 | filter_re = re.compile(filter_raw_string)
|
|---|
| 490 |
|
|---|
| 491 | class FilterExpression(object):
|
|---|
| 492 | """
|
|---|
| 493 | Parses a variable token and its optional filters (all as a single string),
|
|---|
| 494 | and return a list of tuples of the filter name and arguments.
|
|---|
| 495 | Sample:
|
|---|
| 496 | >>> token = 'variable|default:"Default value"|date:"Y-m-d"'
|
|---|
| 497 | >>> p = FilterParser(token)
|
|---|
| 498 | >>> p.filters
|
|---|
| 499 | [('default', 'Default value'), ('date', 'Y-m-d')]
|
|---|
| 500 | >>> p.var
|
|---|
| 501 | 'variable'
|
|---|
| 502 |
|
|---|
| 503 | This class should never be instantiated outside of the
|
|---|
| 504 | get_filters_from_token helper function.
|
|---|
| 505 | """
|
|---|
| 506 | def __init__(self, token, parser):
|
|---|
| 507 | self.token = token
|
|---|
| 508 | matches = filter_re.finditer(token)
|
|---|
| 509 | var = None
|
|---|
| 510 | filters = []
|
|---|
| 511 | upto = 0
|
|---|
| 512 | for match in matches:
|
|---|
| 513 | start = match.start()
|
|---|
| 514 | if upto != start:
|
|---|
| 515 | raise TemplateSyntaxError, "Could not parse some characters: %s|%s|%s" % \
|
|---|
| 516 | (token[:upto], token[upto:start], token[start:])
|
|---|
| 517 | if var == None:
|
|---|
| 518 | var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
|
|---|
| 519 | if i18n_constant:
|
|---|
| 520 | var = '"%s"' % _(i18n_constant)
|
|---|
| 521 | elif constant:
|
|---|
| 522 | var = '"%s"' % constant
|
|---|
| 523 | upto = match.end()
|
|---|
| 524 | if var == None:
|
|---|
| 525 | raise TemplateSyntaxError, "Could not find variable at start of %s" % token
|
|---|
| 526 | elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
|
|---|
| 527 | raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var
|
|---|
| 528 | else:
|
|---|
| 529 | filter_name = match.group("filter_name")
|
|---|
| 530 | args = []
|
|---|
| 531 | constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg")
|
|---|
| 532 | if i18n_arg:
|
|---|
| 533 | args.append((False, _(i18n_arg.replace(r'\"', '"'))))
|
|---|
| 534 | elif constant_arg:
|
|---|
| 535 | args.append((False, constant_arg.replace(r'\"', '"')))
|
|---|
| 536 | elif var_arg:
|
|---|
| 537 | args.append((True, var_arg))
|
|---|
| 538 | filter_func = parser.find_filter(filter_name)
|
|---|
| 539 | self.args_check(filter_name,filter_func, args)
|
|---|
| 540 | filters.append( (filter_func,args))
|
|---|
| 541 | upto = match.end()
|
|---|
| 542 | if upto != len(token):
|
|---|
| 543 | raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:]
|
|---|
| 544 | self.var, self.filters = var, filters
|
|---|
| 545 |
|
|---|
| 546 | def resolve(self, context):
|
|---|
| 547 | try:
|
|---|
| 548 | obj = resolve_variable(self.var, context)
|
|---|
| 549 | except VariableDoesNotExist:
|
|---|
| 550 | obj = settings.TEMPLATE_STRING_IF_INVALID
|
|---|
| 551 | for func, args in self.filters:
|
|---|
| 552 | arg_vals = []
|
|---|
| 553 | for lookup, arg in args:
|
|---|
| 554 | if not lookup:
|
|---|
| 555 | arg_vals.append(arg)
|
|---|
| 556 | else:
|
|---|
| 557 | arg_vals.append(resolve_variable(arg, context))
|
|---|
| 558 | obj = func(obj, *arg_vals)
|
|---|
| 559 | return obj
|
|---|
| 560 |
|
|---|
| 561 | def args_check(name, func, provided):
|
|---|
| 562 | provided = list(provided)
|
|---|
| 563 | plen = len(provided)
|
|---|
| 564 | args, varargs, varkw, defaults = getargspec(func)
|
|---|
| 565 | # First argument is filter input.
|
|---|
| 566 | args.pop(0)
|
|---|
| 567 | if defaults:
|
|---|
| 568 | nondefs = args[:-len(defaults)]
|
|---|
| 569 | else:
|
|---|
| 570 | nondefs = args
|
|---|
| 571 | # Args without defaults must be provided.
|
|---|
| 572 | try:
|
|---|
| 573 | for arg in nondefs:
|
|---|
| 574 | provided.pop(0)
|
|---|
| 575 | except IndexError:
|
|---|
| 576 | # Not enough
|
|---|
| 577 | raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen)
|
|---|
| 578 |
|
|---|
| 579 | # Defaults can be overridden.
|
|---|
| 580 | defaults = defaults and list(defaults) or []
|
|---|
| 581 | try:
|
|---|
| 582 | for parg in provided:
|
|---|
| 583 | defaults.pop(0)
|
|---|
| 584 | except IndexError:
|
|---|
| 585 | # Too many.
|
|---|
| 586 | raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen)
|
|---|
| 587 |
|
|---|
| 588 | return True
|
|---|
| 589 | args_check = staticmethod(args_check)
|
|---|
| 590 |
|
|---|
| 591 | def __str__(self):
|
|---|
| 592 | return self.token
|
|---|
| 593 |
|
|---|
| 594 | def resolve_variable(path, context):
|
|---|
| 595 | """
|
|---|
| 596 | Returns the resolved variable, which may contain attribute syntax, within
|
|---|
| 597 | the given context. The variable may be a hard-coded string (if it begins
|
|---|
| 598 | and ends with single or double quote marks).
|
|---|
| 599 |
|
|---|
| 600 | >>> c = {'article': {'section':'News'}}
|
|---|
| 601 | >>> resolve_variable('article.section', c)
|
|---|
| 602 | 'News'
|
|---|
| 603 | >>> resolve_variable('article', c)
|
|---|
| 604 | {'section': 'News'}
|
|---|
| 605 | >>> class AClass: pass
|
|---|
| 606 | >>> c = AClass()
|
|---|
| 607 | >>> c.article = AClass()
|
|---|
| 608 | >>> c.article.section = 'News'
|
|---|
| 609 | >>> resolve_variable('article.section', c)
|
|---|
| 610 | 'News'
|
|---|
| 611 |
|
|---|
| 612 | (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
|
|---|
| 613 | """
|
|---|
| 614 | if path[0].isdigit():
|
|---|
| 615 | number_type = '.' in path and float or int
|
|---|
| 616 | try:
|
|---|
| 617 | current = number_type(path)
|
|---|
| 618 | except ValueError:
|
|---|
| 619 | current = settings.TEMPLATE_STRING_IF_INVALID
|
|---|
| 620 | elif path[0] in ('"', "'") and path[0] == path[-1]:
|
|---|
| 621 | current = path[1:-1]
|
|---|
| 622 | else:
|
|---|
| 623 | current = context
|
|---|
| 624 | bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR)
|
|---|
| 625 | while bits:
|
|---|
| 626 | try: # dictionary lookup
|
|---|
| 627 | current = current[bits[0]]
|
|---|
| 628 | except (TypeError, AttributeError, KeyError):
|
|---|
| 629 | try: # attribute lookup
|
|---|
| 630 | current = getattr(current, bits[0])
|
|---|
| 631 | if callable(current):
|
|---|
| 632 | if getattr(current, 'alters_data', False):
|
|---|
| 633 | current = settings.TEMPLATE_STRING_IF_INVALID
|
|---|
| 634 | else:
|
|---|
| 635 | try: # method call (assuming no args required)
|
|---|
| 636 | current = current()
|
|---|
| 637 | except TypeError: # arguments *were* required
|
|---|
| 638 | # GOTCHA: This will also catch any TypeError
|
|---|
| 639 | # raised in the function itself.
|
|---|
| 640 | current = settings.TEMPLATE_STRING_IF_INVALID # invalid method call
|
|---|
| 641 | except Exception, e:
|
|---|
| 642 | if getattr(e, 'silent_variable_failure', False):
|
|---|
| 643 | current = settings.TEMPLATE_STRING_IF_INVALID
|
|---|
| 644 | else:
|
|---|
| 645 | raise
|
|---|
| 646 | except (TypeError, AttributeError):
|
|---|
| 647 | try: # list-index lookup
|
|---|
| 648 | current = current[int(bits[0])]
|
|---|
| 649 | except (IndexError, ValueError, KeyError):
|
|---|
| 650 | raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute
|
|---|
| 651 | except Exception, e:
|
|---|
| 652 | if getattr(e, 'silent_variable_failure', False):
|
|---|
| 653 | current = settings.TEMPLATE_STRING_IF_INVALID
|
|---|
| 654 | else:
|
|---|
| 655 | raise
|
|---|
| 656 | del bits[0]
|
|---|
| 657 | return current
|
|---|
| 658 |
|
|---|
| 659 | class Node(object):
|
|---|
| 660 | def render(self, context):
|
|---|
| 661 | "Return the node rendered as a string"
|
|---|
| 662 | pass
|
|---|
| 663 |
|
|---|
| 664 | def __iter__(self):
|
|---|
| 665 | yield self
|
|---|
| 666 |
|
|---|
| 667 | def get_nodes_by_type(self, nodetype):
|
|---|
| 668 | "Return a list of all nodes (within this node and its nodelist) of the given type"
|
|---|
| 669 | nodes = []
|
|---|
| 670 | if isinstance(self, nodetype):
|
|---|
| 671 | nodes.append(self)
|
|---|
| 672 | if hasattr(self, 'nodelist'):
|
|---|
| 673 | nodes.extend(self.nodelist.get_nodes_by_type(nodetype))
|
|---|
| 674 | return nodes
|
|---|
| 675 |
|
|---|
| 676 | class NodeList(list):
|
|---|
| 677 | def render(self, context):
|
|---|
| 678 | bits = []
|
|---|
| 679 | for node in self:
|
|---|
| 680 | if isinstance(node, Node):
|
|---|
| 681 | bits.append(self.render_node(node, context))
|
|---|
| 682 | else:
|
|---|
| 683 | bits.append(node)
|
|---|
| 684 | return ''.join(bits)
|
|---|
| 685 |
|
|---|
| 686 | def get_nodes_by_type(self, nodetype):
|
|---|
| 687 | "Return a list of all nodes of the given type"
|
|---|
| 688 | nodes = []
|
|---|
| 689 | for node in self:
|
|---|
| 690 | nodes.extend(node.get_nodes_by_type(nodetype))
|
|---|
| 691 | return nodes
|
|---|
| 692 |
|
|---|
| 693 | def render_node(self, node, context):
|
|---|
| 694 | return(node.render(context))
|
|---|
| 695 |
|
|---|
| 696 | class DebugNodeList(NodeList):
|
|---|
| 697 | def render_node(self, node, context):
|
|---|
| 698 | try:
|
|---|
| 699 | result = node.render(context)
|
|---|
| 700 | except TemplateSyntaxError, e:
|
|---|
| 701 | if not hasattr(e, 'source'):
|
|---|
| 702 | e.source = node.source
|
|---|
| 703 | raise
|
|---|
| 704 | except Exception:
|
|---|
| 705 | from sys import exc_info
|
|---|
| 706 | wrapped = TemplateSyntaxError('Caught an exception while rendering.')
|
|---|
| 707 | wrapped.source = node.source
|
|---|
| 708 | wrapped.exc_info = exc_info()
|
|---|
| 709 | raise wrapped
|
|---|
| 710 | return result
|
|---|
| 711 |
|
|---|
| 712 | class TextNode(Node):
|
|---|
| 713 | def __init__(self, s):
|
|---|
| 714 | self.s = s
|
|---|
| 715 |
|
|---|
| 716 | def __repr__(self):
|
|---|
| 717 | return "<Text Node: '%s'>" % self.s[:25]
|
|---|
| 718 |
|
|---|
| 719 | def render(self, context):
|
|---|
| 720 | return self.s
|
|---|
| 721 |
|
|---|
| 722 | class VariableNode(Node):
|
|---|
| 723 | def __init__(self, filter_expression):
|
|---|
| 724 | self.filter_expression = filter_expression
|
|---|
| 725 |
|
|---|
| 726 | def __repr__(self):
|
|---|
| 727 | return "<Variable Node: %s>" % self.filter_expression
|
|---|
| 728 |
|
|---|
| 729 | def encode_output(self, output):
|
|---|
| 730 | # Check type so that we don't run str() on a Unicode object
|
|---|
| 731 | if not isinstance(output, basestring):
|
|---|
| 732 | return str(output)
|
|---|
| 733 | elif isinstance(output, unicode):
|
|---|
| 734 | return output.encode(settings.DEFAULT_CHARSET)
|
|---|
| 735 | else:
|
|---|
| 736 | return output
|
|---|
| 737 |
|
|---|
| 738 | def render(self, context):
|
|---|
| 739 | output = self.filter_expression.resolve(context)
|
|---|
| 740 | return self.encode_output(output)
|
|---|
| 741 |
|
|---|
| 742 | class DebugVariableNode(VariableNode):
|
|---|
| 743 | def render(self, context):
|
|---|
| 744 | try:
|
|---|
| 745 | output = self.filter_expression.resolve(context)
|
|---|
| 746 | except TemplateSyntaxError, e:
|
|---|
| 747 | if not hasattr(e, 'source'):
|
|---|
| 748 | e.source = self.source
|
|---|
| 749 | raise
|
|---|
| 750 | return self.encode_output(output)
|
|---|
| 751 |
|
|---|
| 752 | def generic_tag_compiler(params, defaults, name, node_class, parser, token):
|
|---|
| 753 | "Returns a template.Node subclass."
|
|---|
| 754 | bits = token.contents.split()[1:]
|
|---|
| 755 | bmax = len(params)
|
|---|
| 756 | def_len = defaults and len(defaults) or 0
|
|---|
| 757 | bmin = bmax - def_len
|
|---|
| 758 | if(len(bits) < bmin or len(bits) > bmax):
|
|---|
| 759 | if bmin == bmax:
|
|---|
| 760 | message = "%s takes %s arguments" % (name, bmin)
|
|---|
| 761 | else:
|
|---|
| 762 | message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
|
|---|
| 763 | raise TemplateSyntaxError, message
|
|---|
| 764 | return node_class(bits)
|
|---|
| 765 |
|
|---|
| 766 | class Library(object):
|
|---|
| 767 | def __init__(self):
|
|---|
| 768 | self.filters = {}
|
|---|
| 769 | self.tags = {}
|
|---|
| 770 |
|
|---|
| 771 | def tag(self, name=None, compile_function=None):
|
|---|
| 772 | if name == None and compile_function == None:
|
|---|
| 773 | # @register.tag()
|
|---|
| 774 | return self.tag_function
|
|---|
| 775 | elif name != None and compile_function == None:
|
|---|
| 776 | if(callable(name)):
|
|---|
| 777 | # @register.tag
|
|---|
| 778 | return self.tag_function(name)
|
|---|
| 779 | else:
|
|---|
| 780 | # @register.tag('somename') or @register.tag(name='somename')
|
|---|
| 781 | def dec(func):
|
|---|
| 782 | return self.tag(name, func)
|
|---|
| 783 | return dec
|
|---|
| 784 | elif name != None and compile_function != None:
|
|---|
| 785 | # register.tag('somename', somefunc)
|
|---|
| 786 | self.tags[name] = compile_function
|
|---|
| 787 | return compile_function
|
|---|
| 788 | else:
|
|---|
| 789 | raise InvalidTemplateLibrary, "Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function)
|
|---|
| 790 |
|
|---|
| 791 | def tag_function(self,func):
|
|---|
| 792 | self.tags[func.__name__] = func
|
|---|
| 793 | return func
|
|---|
| 794 |
|
|---|
| 795 | def filter(self, name=None, filter_func=None):
|
|---|
| 796 | if name == None and filter_func == None:
|
|---|
| 797 | # @register.filter()
|
|---|
| 798 | return self.filter_function
|
|---|
| 799 | elif filter_func == None:
|
|---|
| 800 | if(callable(name)):
|
|---|
| 801 | # @register.filter
|
|---|
| 802 | return self.filter_function(name)
|
|---|
| 803 | else:
|
|---|
| 804 | # @register.filter('somename') or @register.filter(name='somename')
|
|---|
| 805 | def dec(func):
|
|---|
| 806 | return self.filter(name, func)
|
|---|
| 807 | return dec
|
|---|
| 808 | elif name != None and filter_func != None:
|
|---|
| 809 | # register.filter('somename', somefunc)
|
|---|
| 810 | self.filters[name] = filter_func
|
|---|
| 811 | return filter_func
|
|---|
| 812 | else:
|
|---|
| 813 | raise InvalidTemplateLibrary, "Unsupported arguments to Library.filter: (%r, %r, %r)", (name, compile_function, has_arg)
|
|---|
| 814 |
|
|---|
| 815 | def filter_function(self, func):
|
|---|
| 816 | self.filters[func.__name__] = func
|
|---|
| 817 | return func
|
|---|
| 818 |
|
|---|
| 819 | def simple_tag(self,func):
|
|---|
| 820 | params, xx, xxx, defaults = getargspec(func)
|
|---|
| 821 |
|
|---|
| 822 | class SimpleNode(Node):
|
|---|
| 823 | def __init__(self, vars_to_resolve):
|
|---|
| 824 | self.vars_to_resolve = vars_to_resolve
|
|---|
| 825 |
|
|---|
| 826 | def render(self, context):
|
|---|
| 827 | resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
|
|---|
| 828 | return func(*resolved_vars)
|
|---|
| 829 |
|
|---|
| 830 | compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, SimpleNode)
|
|---|
| 831 | compile_func.__doc__ = func.__doc__
|
|---|
| 832 | self.tag(func.__name__, compile_func)
|
|---|
| 833 | return func
|
|---|
| 834 |
|
|---|
| 835 | def inclusion_tag(self, file_name, context_class=Context, takes_context=False):
|
|---|
| 836 | def dec(func):
|
|---|
| 837 | params, xx, xxx, defaults = getargspec(func)
|
|---|
| 838 | if takes_context:
|
|---|
| 839 | if params[0] == 'context':
|
|---|
| 840 | params = params[1:]
|
|---|
| 841 | else:
|
|---|
| 842 | raise TemplateSyntaxError, "Any tag function decorated with takes_context=True must have a first argument of 'context'"
|
|---|
| 843 |
|
|---|
| 844 | class InclusionNode(Node):
|
|---|
| 845 | def __init__(self, vars_to_resolve):
|
|---|
| 846 | self.vars_to_resolve = vars_to_resolve
|
|---|
| 847 |
|
|---|
| 848 | def render(self, context):
|
|---|
| 849 | resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve]
|
|---|
| 850 | if takes_context:
|
|---|
| 851 | args = [context] + resolved_vars
|
|---|
| 852 | else:
|
|---|
| 853 | args = resolved_vars
|
|---|
| 854 |
|
|---|
| 855 | dict = func(*args)
|
|---|
| 856 |
|
|---|
| 857 | if not getattr(self, 'nodelist', False):
|
|---|
| 858 | from django.template.loader import get_template
|
|---|
| 859 | t = get_template(file_name)
|
|---|
| 860 | self.nodelist = t.nodelist
|
|---|
| 861 | return self.nodelist.render(context_class(dict))
|
|---|
| 862 |
|
|---|
| 863 | compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, InclusionNode)
|
|---|
| 864 | compile_func.__doc__ = func.__doc__
|
|---|
| 865 | self.tag(func.__name__, compile_func)
|
|---|
| 866 | return func
|
|---|
| 867 | return dec
|
|---|
| 868 |
|
|---|
| 869 | def get_library(module_name):
|
|---|
| 870 | lib = libraries.get(module_name, None)
|
|---|
| 871 | if not lib:
|
|---|
| 872 | try:
|
|---|
| 873 | mod = __import__(module_name, '', '', [''])
|
|---|
| 874 | except ImportError, e:
|
|---|
| 875 | raise InvalidTemplateLibrary, "Could not load template library from %s, %s" % (module_name, e)
|
|---|
| 876 | try:
|
|---|
| 877 | lib = mod.register
|
|---|
| 878 | libraries[module_name] = lib
|
|---|
| 879 | except AttributeError:
|
|---|
| 880 | raise InvalidTemplateLibrary, "Template library %s does not have a variable named 'register'" % module_name
|
|---|
| 881 | return lib
|
|---|
| 882 |
|
|---|
| 883 | def add_to_builtins(module_name):
|
|---|
| 884 | builtins.append(get_library(module_name))
|
|---|
| 885 |
|
|---|
| 886 | add_to_builtins('django.template.defaulttags')
|
|---|
| 887 | add_to_builtins('django.template.defaultfilters')
|
|---|