1import re, sys
2from inspect import getargspec
3
4from template.context import Context, RequestContext, ContextPopException
5from helper import *
6
7__all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
8
9TOKEN_TEXT = 0
10TOKEN_VAR = 1
11TOKEN_BLOCK = 2
12TOKEN_COMMENT = 3
13
14# template syntax constants
15FILTER_SEPARATOR = '|'
16FILTER_ARGUMENT_SEPARATOR = ':'
17VARIABLE_ATTRIBUTE_SEPARATOR = '.'
18BLOCK_TAG_START = '{%'
19BLOCK_TAG_END = '%}'
20VARIABLE_TAG_START = '{{'
21VARIABLE_TAG_END = '}}'
22COMMENT_TAG_START = '{#'
23COMMENT_TAG_END = '#}'
24SINGLE_BRACE_START = '{'
25SINGLE_BRACE_END = '}'
26
27ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'
28
29# what to report as the origin for templates that come from non-loader sources
30# (e.g. strings)
31UNKNOWN_SOURCE="<unknown source>"
32
33# match a variable or block tag and capture the entire tag, including start/end delimiters
34tag_re = re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
35                                          re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END),
36                                          re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END)))
37
38# global dictionary of libraries that have been loaded using get_library
39libraries = {}
40# global list of libraries to load by default for a new parser
41builtins = []
42
43# True if TEMPLATE_STRING_IF_INVALID contains a format string (%s). None means
44# uninitialised.
45invalid_var_format_string = None
46
47class TemplateSyntaxError(Exception):
48    def __str__(self):
49        try:
50            import cStringIO as StringIO
51        except ImportError:
52            import StringIO
53        output = StringIO.StringIO()
54        output.write(Exception.__str__(self))
55        # Check if we wrapped an exception and print that too.
56        if hasattr(self, 'exc_info'):
57            import traceback
58            output.write('\n\nOriginal ')
59            e = self.exc_info
60            traceback.print_exception(e[0], e[1], e[2], 500, output)
61        return output.getvalue()
62
63class TemplateDoesNotExist(Exception):
64    pass
65
66class TemplateEncodingError(Exception):
67    pass
68
69class VariableDoesNotExist(Exception):
70
71    def __init__(self, msg, params=()):
72        self.msg = msg
73        self.params = params
74
75    def __str__(self):
76        return unicode(self).encode('utf-8')
77
78class InvalidTemplateLibrary(Exception):
79    pass
80
81class Origin(object):
82    def __init__(self, name):
83        self.name = name
84
85    def reload(self):
86        raise NotImplementedError
87
88    def __str__(self):
89        return self.name
90
91class StringOrigin(Origin):
92    def __init__(self, source):
93        super(StringOrigin, self).__init__(UNKNOWN_SOURCE)
94        self.source = source
95
96    def reload(self):
97        return self.source
98
99class Template(object):
100    def __init__(self, template_string, origin=None, name='<Unknown Template>'):
101        self.nodelist = compile_string(template_string, origin)
102        self.name = name
103
104    def __iter__(self):
105        for node in self.nodelist:
106            for subnode in node:
107                yield subnode
108
109    def render(self, context):
110        "Display stage -- can be called many times"
111        return self.nodelist.render(context)
112
113def compile_string(template_string, origin):
114    "Compiles template_string into NodeList ready for rendering"
115    lexer_class, parser_class = Lexer, Parser
116    lexer = lexer_class(template_string, origin)
117    parser = parser_class(lexer.tokenize())
118    return parser.parse()
119
120class Token(object):
121    def __init__(self, token_type, contents):
122        # token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT.
123        self.token_type, self.contents = token_type, contents
124
125    def __str__(self):
126        return '<%s token: "%s...">' % \
127            ({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block', TOKEN_COMMENT: 'Comment'}[self.token_type],
128            self.contents[:20].replace('\n', ''))
129
130    def split_contents(self):
131        split = []
132        bits = iter(smart_split(self.contents))
133        for bit in bits:
134            # Handle translation-marked template pieces
135            if bit.startswith('_("') or bit.startswith("_('"):
136                sentinal = bit[2] + ')'
137                trans_bit = [bit]
138                while not bit.endswith(sentinal):
139                    bit = bits.next()
140                    trans_bit.append(bit)
141                bit = ' '.join(trans_bit)
142            split.append(bit)
143        return split
144
145class Lexer(object):
146    def __init__(self, template_string, origin):
147        self.template_string = template_string
148        self.origin = origin
149
150    def tokenize(self):
151        "Return a list of tokens from a given template_string."
152        in_tag = False
153        result = []
154        for bit in tag_re.split(self.template_string):
155            if bit:
156                result.append(self.create_token(bit, in_tag))
157            in_tag = not in_tag
158        return result
159
160    def create_token(self, token_string, in_tag):
161        """
162        Convert the given token string into a new Token object and return it.
163        If in_tag is True, we are processing something that matched a tag,
164        otherwise it should be treated as a literal string.
165        """
166        if in_tag:
167            if token_string.startswith(VARIABLE_TAG_START):
168                token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
169            elif token_string.startswith(BLOCK_TAG_START):
170                token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
171            elif token_string.startswith(COMMENT_TAG_START):
172                token = Token(TOKEN_COMMENT, '')
173        else:
174            token = Token(TOKEN_TEXT, token_string)
175        return token
176
177class Parser(object):
178    def __init__(self, tokens):
179        self.tokens = tokens
180        self.tags = {}
181        self.filters = {}
182        for lib in builtins:
183            self.add_library(lib)
184
185    def parse(self, parse_until=None):
186        if parse_until is None: parse_until = []
187        nodelist = self.create_nodelist()
188        while self.tokens:
189            token = self.next_token()
190            if token.token_type == TOKEN_TEXT:
191                self.extend_nodelist(nodelist, TextNode(token.contents), token)
192            elif token.token_type == TOKEN_VAR:
193                if not token.contents:
194                    self.empty_variable(token)
195                var_node = self.create_variable_node(token.contents)
196                self.extend_nodelist(nodelist, var_node,token)
197            elif token.token_type == TOKEN_BLOCK:
198                if token.contents in parse_until:
199                    # put token back on token list so calling code knows why it terminated
200                    self.prepend_token(token)
201                    return nodelist
202                try:
203                    command = token.contents.split()[0]
204                except IndexError:
205                    self.empty_block_tag(token)
206                # execute callback function for this tag and append resulting node
207                self.enter_command(command, token)
208                try:
209                    compile_func = self.tags[command]
210                except KeyError:
211                    self.invalid_block_tag(token, command)
212                try:
213                    compiled_result = compile_func(self, token)
214                except TemplateSyntaxError, e:
215                    if not self.compile_function_error(token, e):
216                        raise
217                self.extend_nodelist(nodelist, compiled_result, token)
218                self.exit_command()
219        if parse_until:
220            self.unclosed_block_tag(parse_until)
221        return nodelist
222
223    def skip_past(self, endtag):
224        while self.tokens:
225            token = self.next_token()
226            if token.token_type == TOKEN_BLOCK and token.contents == endtag:
227                return
228        self.unclosed_block_tag([endtag])
229
230    def create_variable_node(self, content):
231        return VariableNode(content)
232
233    def create_nodelist(self):
234        return NodeList()
235
236    def extend_nodelist(self, nodelist, node, token):
237        if node.must_be_first and nodelist:
238            try:
239                if nodelist.contains_nontext:
240                    raise AttributeError
241            except AttributeError:
242                raise TemplateSyntaxError("%r must be the first tag in the template." % node)
243        if isinstance(nodelist, NodeList) and not isinstance(node, TextNode):
244            nodelist.contains_nontext = True
245        nodelist.append(node)
246
247    def enter_command(self, command, token):
248        pass
249
250    def exit_command(self):
251        pass
252
253    def error(self, token, msg):
254        return TemplateSyntaxError(msg)
255
256    def empty_variable(self, token):
257        raise self.error(token, "Empty variable tag")
258
259    def empty_block_tag(self, token):
260        raise self.error(token, "Empty block tag")
261
262    def invalid_block_tag(self, token, command):
263        raise self.error(token, "Invalid block tag: '%s'" % command)
264
265    def unclosed_block_tag(self, parse_until):
266        raise self.error(None, "Unclosed tags: %s " %  ', '.join(parse_until))
267
268    def compile_function_error(self, token, e):
269        pass
270
271    def next_token(self):
272        return self.tokens.pop(0)
273
274    def prepend_token(self, token):
275        self.tokens.insert(0, token)
276
277    def delete_first_token(self):
278        del self.tokens[0]
279
280    def add_library(self, lib):
281        self.tags.update(lib.tags)
282        self.filters.update(lib.filters)
283
284    def find_filter(self, filter_name):
285        if filter_name in self.filters:
286            return self.filters[filter_name]
287        else:
288            raise TemplateSyntaxError("Invalid filter: '%s'" % filter_name)
289
290class TokenParser(object):
291    """
292    Subclass this and implement the top() method to parse a template line. When
293    instantiating the parser, pass in the line from the Django template parser.
294
295    The parser's "tagname" instance-variable stores the name of the tag that
296    the filter was called with.
297    """
298    def __init__(self, subject):
299        self.subject = subject
300        self.pointer = 0
301        self.backout = []
302        self.tagname = self.tag()
303
304    def top(self):
305        "Overload this method to do the actual parsing and return the result."
306        raise NotImplementedError()
307
308    def more(self):
309        "Returns True if there is more stuff in the tag."
310        return self.pointer < len(self.subject)
311
312    def back(self):
313        "Undoes the last microparser. Use this for lookahead and backtracking."
314        if not len(self.backout):
315            raise TemplateSyntaxError("back called without some previous parsing")
316        self.pointer = self.backout.pop()
317
318    def tag(self):
319        "A microparser that just returns the next tag from the line."
320        subject = self.subject
321        i = self.pointer
322        if i >= len(subject):
323            raise TemplateSyntaxError("expected another tag, found end of string: %s" % subject)
324        p = i
325        while i < len(subject) and subject[i] not in (' ', '\t'):
326            i += 1
327        s = subject[p:i]
328        while i < len(subject) and subject[i] in (' ', '\t'):
329            i += 1
330        self.backout.append(self.pointer)
331        self.pointer = i
332        return s
333
334    def value(self):
335        "A microparser that parses for a value: some string constant or variable name."
336        subject = self.subject
337        i = self.pointer
338        if i >= len(subject):
339            raise TemplateSyntaxError("Searching for value. Expected another value but found end of string: %s" % subject)
340        if subject[i] in ('"', "'"):
341            p = i
342            i += 1
343            while i < len(subject) and subject[i] != subject[p]:
344                i += 1
345            if i >= len(subject):
346                raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
347            i += 1
348            res = subject[p:i]
349            while i < len(subject) and subject[i] in (' ', '\t'):
350                i += 1
351            self.backout.append(self.pointer)
352            self.pointer = i
353            return res
354        else:
355            p = i
356            while i < len(subject) and subject[i] not in (' ', '\t'):
357                if subject[i] in ('"', "'"):
358                    c = subject[i]
359                    i += 1
360                    while i < len(subject) and subject[i] != c:
361                        i += 1
362                    if i >= len(subject):
363                        raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
364                i += 1
365            s = subject[p:i]
366            while i < len(subject) and subject[i] in (' ', '\t'):
367                i += 1
368            self.backout.append(self.pointer)
369            self.pointer = i
370            return s
371
372class Node(object):
373    # Set this to True for nodes that must be first in the template (although
374    # they can be preceded by text nodes.
375    must_be_first = False
376
377    def render(self, context):
378        "Return the node rendered as a string"
379        pass
380
381    def __iter__(self):
382        yield self
383
384    def get_nodes_by_type(self, nodetype):
385        "Return a list of all nodes (within this node and its nodelist) of the given type"
386        nodes = []
387        if isinstance(self, nodetype):
388            nodes.append(self)
389        if hasattr(self, 'nodelist'):
390            nodes.extend(self.nodelist.get_nodes_by_type(nodetype))
391        return nodes
392
393class NodeList(list):
394    # Set to True the first time a non-TextNode is inserted by
395    # extend_nodelist().
396    contains_nontext = False
397
398    def render(self, context):
399        bits = []
400        for node in self:
401            if isinstance(node, Node):
402                bits.append(self.render_node(node, context))
403            else:
404                bits.append(node)
405        return str(''.join([str(b) for b in bits]))
406
407    def get_nodes_by_type(self, nodetype):
408        "Return a list of all nodes of the given type"
409        nodes = []
410        for node in self:
411            nodes.extend(node.get_nodes_by_type(nodetype))
412        return nodes
413
414    def render_node(self, node, context):
415        return node.render(context)
416
417class TextNode(Node):
418    def __init__(self, s):
419        self.s = s
420
421    def __repr__(self):
422        return "<Text Node: '%s'>" % str(self.s[:25], 'ascii',
423                errors='replace')
424
425    def render(self, context):
426        return self.s
427
428def _render_value_in_context(value, context):
429    """
430    Converts any value to a string to become part of a rendered template. This
431    means escaping, if required, and conversion to a unicode object. If value
432    is a string, it is expected to have already been translated.
433    """
434    return str(value)
435
436class VariableNode(Node):
437    def __init__(self,expr):
438        self.expr = expr
439
440    def render(self,context):
441        return str(eval(self.expr,context))
442
443def generic_tag_compiler(params, defaults, name, node_class, parser, token):
444    "Returns a template.Node subclass."
445    bits = token.split_contents()[1:]
446    bmax = len(params)
447    def_len = defaults and len(defaults) or 0
448    bmin = bmax - def_len
449    if(len(bits) < bmin or len(bits) > bmax):
450        if bmin == bmax:
451            message = "%s takes %s arguments" % (name, bmin)
452        else:
453            message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
454        raise TemplateSyntaxError(message)
455    return node_class(bits)
456
457class Library(object):
458    def __init__(self):
459        self.filters = {}
460        self.tags = {}
461
462    def tag(self, name=None, compile_function=None):
463        if name == None and compile_function == None:
464            # @register.tag()
465            return self.tag_function
466        elif name != None and compile_function == None:
467            if(callable(name)):
468                # @register.tag
469                return self.tag_function(name)
470            else:
471                # @register.tag('somename') or @register.tag(name='somename')
472                def dec(func):
473                    return self.tag(name, func)
474                return dec
475        elif name != None and compile_function != None:
476            # register.tag('somename', somefunc)
477            self.tags[name] = compile_function
478            return compile_function
479        else:
480            raise InvalidTemplateLibrary("Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function))
481
482    def tag_function(self,func):
483        self.tags[getattr(func, "_decorated_function", func).__name__] = func
484        return func
485
486    def filter(self, name=None, filter_func=None):
487        if name == None and filter_func == None:
488            # @register.filter()
489            return self.filter_function
490        elif filter_func == None:
491            if(callable(name)):
492                # @register.filter
493                return self.filter_function(name)
494            else:
495                # @register.filter('somename') or @register.filter(name='somename')
496                def dec(func):
497                    return self.filter(name, func)
498                return dec
499        elif name != None and filter_func != None:
500            # register.filter('somename', somefunc)
501            self.filters[name] = filter_func
502            return filter_func
503        else:
504            raise InvalidTemplateLibrary("Unsupported arguments to Library.filter: (%r, %r)", (name, filter_func))
505
506    def filter_function(self, func):
507        self.filters[getattr(func, "_decorated_function", func).__name__] = func
508        return func
509
510    def simple_tag(self,func):
511        params, xx, xxx, defaults = getargspec(func)
512
513        class SimpleNode(Node):
514            def __init__(self, vars_to_resolve):
515                self.vars_to_resolve = map(Variable, vars_to_resolve)
516
517            def render(self, context):
518                resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
519                return func(*resolved_vars)
520
521        compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, SimpleNode)
522        compile_func.__doc__ = func.__doc__
523        self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
524        return func
525
526    def inclusion_tag(self, file_name, context_class=Context, takes_context=False):
527        def dec(func):
528            params, xx, xxx, defaults = getargspec(func)
529            if takes_context:
530                if params[0] == 'context':
531                    params = params[1:]
532                else:
533                    raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
534
535            class InclusionNode(Node):
536                def __init__(self, vars_to_resolve):
537                    self.vars_to_resolve = map(Variable, vars_to_resolve)
538
539                def render(self, context):
540                    resolved_vars = [var.resolve(context) for var in self.vars_to_resolve]
541                    if takes_context:
542                        args = [context] + resolved_vars
543                    else:
544                        args = resolved_vars
545
546                    dict = func(*args)
547
548                    if not getattr(self, 'nodelist', False):
549                        from template.loader import get_template, select_template
550                        if not isinstance(file_name, basestring) and is_iterable(file_name):
551                            t = select_template(file_name)
552                        else:
553                            t = get_template(file_name)
554                        self.nodelist = t.nodelist
555                    return self.nodelist.render(context_class(dict,
556                            autoescape=context.autoescape))
557
558            compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode)
559            compile_func.__doc__ = func.__doc__
560            self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
561            return func
562        return dec
563
564def get_library(module_name):
565    lib = libraries.get(module_name, None)
566    if not lib:
567        try:
568            mod = import_module(module_name)
569        except ImportError, e:
570            raise InvalidTemplateLibrary("Could not load template library from %s, %s" % (module_name, e))
571        try:
572            lib = mod.register
573            libraries[module_name] = lib
574        except AttributeError:
575            raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % module_name)
576    return lib
577
578def add_to_builtins(module_name):
579    builtins.append(get_library(module_name))
580
581add_to_builtins('template.defaulttags')
582