1# mako/parsetree.py
2# Copyright (C) 2006-2015 the Mako authors and contributors <see AUTHORS file>
3#
4# This module is part of Mako and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7"""defines the parse tree components for Mako templates."""
8
9from mako import exceptions, ast, util, filters, compat
10import re
11
12class Node(object):
13    """base class for a Node in the parse tree."""
14
15    def __init__(self, source, lineno, pos, filename):
16        self.source = source
17        self.lineno = lineno
18        self.pos = pos
19        self.filename = filename
20
21    @property
22    def exception_kwargs(self):
23        return {'source': self.source, 'lineno': self.lineno,
24                'pos': self.pos, 'filename': self.filename}
25
26    def get_children(self):
27        return []
28
29    def accept_visitor(self, visitor):
30        def traverse(node):
31            for n in node.get_children():
32                n.accept_visitor(visitor)
33
34        method = getattr(visitor, "visit" + self.__class__.__name__, traverse)
35        method(self)
36
37class TemplateNode(Node):
38    """a 'container' node that stores the overall collection of nodes."""
39
40    def __init__(self, filename):
41        super(TemplateNode, self).__init__('', 0, 0, filename)
42        self.nodes = []
43        self.page_attributes = {}
44
45    def get_children(self):
46        return self.nodes
47
48    def __repr__(self):
49        return "TemplateNode(%s, %r)" % (
50                    util.sorted_dict_repr(self.page_attributes),
51                    self.nodes)
52
53class ControlLine(Node):
54    """defines a control line, a line-oriented python line or end tag.
55
56    e.g.::
57
58        % if foo:
59            (markup)
60        % endif
61
62    """
63
64    has_loop_context = False
65
66    def __init__(self, keyword, isend, text, **kwargs):
67        super(ControlLine, self).__init__(**kwargs)
68        self.text = text
69        self.keyword = keyword
70        self.isend = isend
71        self.is_primary = keyword in ['for', 'if', 'while', 'try', 'with']
72        self.nodes = []
73        if self.isend:
74            self._declared_identifiers = []
75            self._undeclared_identifiers = []
76        else:
77            code = ast.PythonFragment(text, **self.exception_kwargs)
78            self._declared_identifiers = code.declared_identifiers
79            self._undeclared_identifiers = code.undeclared_identifiers
80
81    def get_children(self):
82        return self.nodes
83
84    def declared_identifiers(self):
85        return self._declared_identifiers
86
87    def undeclared_identifiers(self):
88        return self._undeclared_identifiers
89
90    def is_ternary(self, keyword):
91        """return true if the given keyword is a ternary keyword
92        for this ControlLine"""
93
94        return keyword in {
95            'if':set(['else', 'elif']),
96            'try':set(['except', 'finally']),
97            'for':set(['else'])
98        }.get(self.keyword, [])
99
100    def __repr__(self):
101        return "ControlLine(%r, %r, %r, %r)" % (
102            self.keyword,
103            self.text,
104            self.isend,
105            (self.lineno, self.pos)
106        )
107
108class Text(Node):
109    """defines plain text in the template."""
110
111    def __init__(self, content, **kwargs):
112        super(Text, self).__init__(**kwargs)
113        self.content = content
114
115    def __repr__(self):
116        return "Text(%r, %r)" % (self.content, (self.lineno, self.pos))
117
118class Code(Node):
119    """defines a Python code block, either inline or module level.
120
121    e.g.::
122
123        inline:
124        <%
125            x = 12
126        %>
127
128        module level:
129        <%!
130            import logger
131        %>
132
133    """
134
135    def __init__(self, text, ismodule, **kwargs):
136        super(Code, self).__init__(**kwargs)
137        self.text = text
138        self.ismodule = ismodule
139        self.code = ast.PythonCode(text, **self.exception_kwargs)
140
141    def declared_identifiers(self):
142        return self.code.declared_identifiers
143
144    def undeclared_identifiers(self):
145        return self.code.undeclared_identifiers
146
147    def __repr__(self):
148        return "Code(%r, %r, %r)" % (
149            self.text,
150            self.ismodule,
151            (self.lineno, self.pos)
152        )
153
154class Comment(Node):
155    """defines a comment line.
156
157    # this is a comment
158
159    """
160
161    def __init__(self, text, **kwargs):
162        super(Comment, self).__init__(**kwargs)
163        self.text = text
164
165    def __repr__(self):
166        return "Comment(%r, %r)" % (self.text, (self.lineno, self.pos))
167
168class Expression(Node):
169    """defines an inline expression.
170
171    ${x+y}
172
173    """
174
175    def __init__(self, text, escapes, **kwargs):
176        super(Expression, self).__init__(**kwargs)
177        self.text = text
178        self.escapes = escapes
179        self.escapes_code = ast.ArgumentList(escapes, **self.exception_kwargs)
180        self.code = ast.PythonCode(text, **self.exception_kwargs)
181
182    def declared_identifiers(self):
183        return []
184
185    def undeclared_identifiers(self):
186        # TODO: make the "filter" shortcut list configurable at parse/gen time
187        return self.code.undeclared_identifiers.union(
188                self.escapes_code.undeclared_identifiers.difference(
189                    set(filters.DEFAULT_ESCAPES.keys())
190                )
191            ).difference(self.code.declared_identifiers)
192
193    def __repr__(self):
194        return "Expression(%r, %r, %r)" % (
195            self.text,
196            self.escapes_code.args,
197            (self.lineno, self.pos)
198        )
199
200class _TagMeta(type):
201    """metaclass to allow Tag to produce a subclass according to
202    its keyword"""
203
204    _classmap = {}
205
206    def __init__(cls, clsname, bases, dict):
207        if getattr(cls, '__keyword__', None) is not None:
208            cls._classmap[cls.__keyword__] = cls
209        super(_TagMeta, cls).__init__(clsname, bases, dict)
210
211    def __call__(cls, keyword, attributes, **kwargs):
212        if ":" in keyword:
213            ns, defname = keyword.split(':')
214            return type.__call__(CallNamespaceTag, ns, defname,
215                                        attributes, **kwargs)
216
217        try:
218            cls = _TagMeta._classmap[keyword]
219        except KeyError:
220            raise exceptions.CompileException(
221                "No such tag: '%s'" % keyword,
222                source=kwargs['source'],
223                lineno=kwargs['lineno'],
224                pos=kwargs['pos'],
225                filename=kwargs['filename']
226            )
227        return type.__call__(cls, keyword, attributes, **kwargs)
228
229class Tag(compat.with_metaclass(_TagMeta, Node)):
230    """abstract base class for tags.
231
232    <%sometag/>
233
234    <%someothertag>
235        stuff
236    </%someothertag>
237
238    """
239    __keyword__ = None
240
241    def __init__(self, keyword, attributes, expressions,
242                        nonexpressions, required, **kwargs):
243        """construct a new Tag instance.
244
245        this constructor not called directly, and is only called
246        by subclasses.
247
248        :param keyword: the tag keyword
249
250        :param attributes: raw dictionary of attribute key/value pairs
251
252        :param expressions: a set of identifiers that are legal attributes,
253         which can also contain embedded expressions
254
255        :param nonexpressions: a set of identifiers that are legal
256         attributes, which cannot contain embedded expressions
257
258        :param \**kwargs:
259         other arguments passed to the Node superclass (lineno, pos)
260
261        """
262        super(Tag, self).__init__(**kwargs)
263        self.keyword = keyword
264        self.attributes = attributes
265        self._parse_attributes(expressions, nonexpressions)
266        missing = [r for r in required if r not in self.parsed_attributes]
267        if len(missing):
268            raise exceptions.CompileException(
269                "Missing attribute(s): %s" %
270                    ",".join([repr(m) for m in missing]),
271                **self.exception_kwargs)
272        self.parent = None
273        self.nodes = []
274
275    def is_root(self):
276        return self.parent is None
277
278    def get_children(self):
279        return self.nodes
280
281    def _parse_attributes(self, expressions, nonexpressions):
282        undeclared_identifiers = set()
283        self.parsed_attributes = {}
284        for key in self.attributes:
285            if key in expressions:
286                expr = []
287                for x in re.compile(r'(\${.+?})',
288                                    re.S).split(self.attributes[key]):
289                    m = re.compile(r'^\${(.+?)}$', re.S).match(x)
290                    if m:
291                        code = ast.PythonCode(m.group(1).rstrip(),
292                                **self.exception_kwargs)
293                        # we aren't discarding "declared_identifiers" here,
294                        # which we do so that list comprehension-declared
295                        # variables aren't counted.   As yet can't find a
296                        # condition that requires it here.
297                        undeclared_identifiers = \
298                            undeclared_identifiers.union(
299                                    code.undeclared_identifiers)
300                        expr.append('(%s)' % m.group(1))
301                    else:
302                        if x:
303                            expr.append(repr(x))
304                self.parsed_attributes[key] = " + ".join(expr) or repr('')
305            elif key in nonexpressions:
306                if re.search(r'\${.+?}', self.attributes[key]):
307                    raise exceptions.CompileException(
308                           "Attibute '%s' in tag '%s' does not allow embedded "
309                           "expressions"  % (key, self.keyword),
310                           **self.exception_kwargs)
311                self.parsed_attributes[key] = repr(self.attributes[key])
312            else:
313                raise exceptions.CompileException(
314                                    "Invalid attribute for tag '%s': '%s'" %
315                                    (self.keyword, key),
316                                    **self.exception_kwargs)
317        self.expression_undeclared_identifiers = undeclared_identifiers
318
319    def declared_identifiers(self):
320        return []
321
322    def undeclared_identifiers(self):
323        return self.expression_undeclared_identifiers
324
325    def __repr__(self):
326        return "%s(%r, %s, %r, %r)" % (self.__class__.__name__,
327                                    self.keyword,
328                                    util.sorted_dict_repr(self.attributes),
329                                    (self.lineno, self.pos),
330                                    self.nodes
331                                )
332
333class IncludeTag(Tag):
334    __keyword__ = 'include'
335
336    def __init__(self, keyword, attributes, **kwargs):
337        super(IncludeTag, self).__init__(
338                                    keyword,
339                                    attributes,
340                                    ('file', 'import', 'args'),
341                                    (), ('file',), **kwargs)
342        self.page_args = ast.PythonCode(
343                                "__DUMMY(%s)" % attributes.get('args', ''),
344                                 **self.exception_kwargs)
345
346    def declared_identifiers(self):
347        return []
348
349    def undeclared_identifiers(self):
350        identifiers = self.page_args.undeclared_identifiers.\
351                            difference(set(["__DUMMY"])).\
352                            difference(self.page_args.declared_identifiers)
353        return identifiers.union(super(IncludeTag, self).
354                                    undeclared_identifiers())
355
356class NamespaceTag(Tag):
357    __keyword__ = 'namespace'
358
359    def __init__(self, keyword, attributes, **kwargs):
360        super(NamespaceTag, self).__init__(
361                                        keyword, attributes,
362                                        ('file',),
363                                        ('name','inheritable',
364                                        'import','module'),
365                                        (), **kwargs)
366
367        self.name = attributes.get('name', '__anon_%s' % hex(abs(id(self))))
368        if not 'name' in attributes and not 'import' in attributes:
369            raise exceptions.CompileException(
370                "'name' and/or 'import' attributes are required "
371                "for <%namespace>",
372                **self.exception_kwargs)
373        if 'file' in attributes and 'module' in attributes:
374            raise exceptions.CompileException(
375                "<%namespace> may only have one of 'file' or 'module'",
376                **self.exception_kwargs
377            )
378
379    def declared_identifiers(self):
380        return []
381
382class TextTag(Tag):
383    __keyword__ = 'text'
384
385    def __init__(self, keyword, attributes, **kwargs):
386        super(TextTag, self).__init__(
387                                    keyword,
388                                    attributes, (),
389                                    ('filter'), (), **kwargs)
390        self.filter_args = ast.ArgumentList(
391                                    attributes.get('filter', ''),
392                                    **self.exception_kwargs)
393
394    def undeclared_identifiers(self):
395        return self.filter_args.\
396                            undeclared_identifiers.\
397                            difference(filters.DEFAULT_ESCAPES.keys()).union(
398                        self.expression_undeclared_identifiers
399                    )
400
401class DefTag(Tag):
402    __keyword__ = 'def'
403
404    def __init__(self, keyword, attributes, **kwargs):
405        expressions = ['buffered', 'cached'] + [
406                c for c in attributes if c.startswith('cache_')]
407
408
409        super(DefTag, self).__init__(
410                keyword,
411                attributes,
412                expressions,
413                ('name', 'filter', 'decorator'),
414                ('name',),
415                **kwargs)
416        name = attributes['name']
417        if re.match(r'^[\w_]+$', name):
418            raise exceptions.CompileException(
419                                "Missing parenthesis in %def",
420                                **self.exception_kwargs)
421        self.function_decl = ast.FunctionDecl("def " + name + ":pass",
422                                                    **self.exception_kwargs)
423        self.name = self.function_decl.funcname
424        self.decorator = attributes.get('decorator', '')
425        self.filter_args = ast.ArgumentList(
426                                attributes.get('filter', ''),
427                                **self.exception_kwargs)
428
429    is_anonymous = False
430    is_block = False
431
432    @property
433    def funcname(self):
434        return self.function_decl.funcname
435
436    def get_argument_expressions(self, **kw):
437        return self.function_decl.get_argument_expressions(**kw)
438
439    def declared_identifiers(self):
440        return self.function_decl.allargnames
441
442    def undeclared_identifiers(self):
443        res = []
444        for c in self.function_decl.defaults:
445            res += list(ast.PythonCode(c, **self.exception_kwargs).
446                                    undeclared_identifiers)
447        return set(res).union(
448            self.filter_args.\
449                            undeclared_identifiers.\
450                            difference(filters.DEFAULT_ESCAPES.keys())
451        ).union(
452            self.expression_undeclared_identifiers
453        ).difference(
454            self.function_decl.allargnames
455        )
456
457class BlockTag(Tag):
458    __keyword__ = 'block'
459
460    def __init__(self, keyword, attributes, **kwargs):
461        expressions = ['buffered', 'cached', 'args'] + [
462                 c for c in attributes if c.startswith('cache_')]
463
464        super(BlockTag, self).__init__(
465                keyword,
466                attributes,
467                expressions,
468                ('name','filter', 'decorator'),
469                (),
470                **kwargs)
471        name = attributes.get('name')
472        if name and not re.match(r'^[\w_]+$',name):
473            raise exceptions.CompileException(
474                               "%block may not specify an argument signature",
475                               **self.exception_kwargs)
476        if not name and attributes.get('args', None):
477            raise exceptions.CompileException(
478                                "Only named %blocks may specify args",
479                                **self.exception_kwargs
480                                )
481        self.body_decl = ast.FunctionArgs(attributes.get('args', ''),
482                                            **self.exception_kwargs)
483
484        self.name = name
485        self.decorator = attributes.get('decorator', '')
486        self.filter_args = ast.ArgumentList(
487                                attributes.get('filter', ''),
488                                **self.exception_kwargs)
489
490
491    is_block = True
492
493    @property
494    def is_anonymous(self):
495        return self.name is None
496
497    @property
498    def funcname(self):
499        return self.name or "__M_anon_%d" % (self.lineno, )
500
501    def get_argument_expressions(self, **kw):
502        return self.body_decl.get_argument_expressions(**kw)
503
504    def declared_identifiers(self):
505        return self.body_decl.allargnames
506
507    def undeclared_identifiers(self):
508        return (self.filter_args.\
509                            undeclared_identifiers.\
510                            difference(filters.DEFAULT_ESCAPES.keys())
511                ).union(self.expression_undeclared_identifiers)
512
513
514
515class CallTag(Tag):
516    __keyword__ = 'call'
517
518    def __init__(self, keyword, attributes, **kwargs):
519        super(CallTag, self).__init__(keyword, attributes,
520                                    ('args'), ('expr',), ('expr',), **kwargs)
521        self.expression = attributes['expr']
522        self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
523        self.body_decl = ast.FunctionArgs(attributes.get('args', ''),
524                                            **self.exception_kwargs)
525
526    def declared_identifiers(self):
527        return self.code.declared_identifiers.union(self.body_decl.allargnames)
528
529    def undeclared_identifiers(self):
530        return self.code.undeclared_identifiers.\
531                    difference(self.code.declared_identifiers)
532
533class CallNamespaceTag(Tag):
534
535    def __init__(self, namespace, defname, attributes, **kwargs):
536        super(CallNamespaceTag, self).__init__(
537                    namespace + ":" + defname,
538                    attributes,
539                    tuple(attributes.keys()) + ('args', ),
540                    (),
541                    (),
542                    **kwargs)
543
544        self.expression = "%s.%s(%s)" % (
545                                namespace,
546                                defname,
547                                ",".join(["%s=%s" % (k, v) for k, v in
548                                            self.parsed_attributes.items()
549                                            if k != 'args'])
550                            )
551        self.code = ast.PythonCode(self.expression, **self.exception_kwargs)
552        self.body_decl = ast.FunctionArgs(
553                                    attributes.get('args', ''),
554                                    **self.exception_kwargs)
555
556    def declared_identifiers(self):
557        return self.code.declared_identifiers.union(self.body_decl.allargnames)
558
559    def undeclared_identifiers(self):
560        return self.code.undeclared_identifiers.\
561                    difference(self.code.declared_identifiers)
562
563class InheritTag(Tag):
564    __keyword__ = 'inherit'
565
566    def __init__(self, keyword, attributes, **kwargs):
567        super(InheritTag, self).__init__(
568                                keyword, attributes,
569                                ('file',), (), ('file',), **kwargs)
570
571class PageTag(Tag):
572    __keyword__ = 'page'
573
574    def __init__(self, keyword, attributes, **kwargs):
575        expressions =   ['cached', 'args', 'expression_filter', 'enable_loop'] + [
576                    c for c in attributes if c.startswith('cache_')]
577
578        super(PageTag, self).__init__(
579                keyword,
580                attributes,
581                expressions,
582                (),
583                (),
584                **kwargs)
585        self.body_decl = ast.FunctionArgs(attributes.get('args', ''),
586                                            **self.exception_kwargs)
587        self.filter_args = ast.ArgumentList(
588                                attributes.get('expression_filter', ''),
589                                **self.exception_kwargs)
590
591    def declared_identifiers(self):
592        return self.body_decl.allargnames
593
594
595