1import re
2
3import pytest
4
5from mako import compat
6from mako import exceptions
7from mako import parsetree
8from mako import util
9from mako.lexer import Lexer
10from mako.template import Template
11from mako.testing.assertions import assert_raises
12from mako.testing.assertions import assert_raises_message
13from mako.testing.assertions import eq_
14from mako.testing.fixtures import TemplateTest
15from mako.testing.helpers import flatten_result
16
17# create fake parsetree classes which are constructed
18# exactly as the repr() of a real parsetree object.
19# this allows us to use a Python construct as the source
20# of a comparable repr(), which is also hit by the 2to3 tool.
21
22
23def repr_arg(x):
24    if isinstance(x, dict):
25        return util.sorted_dict_repr(x)
26    else:
27        return repr(x)
28
29
30def _as_unicode(arg):
31    if isinstance(arg, dict):
32        return {k: _as_unicode(v) for k, v in arg.items()}
33    else:
34        return arg
35
36
37Node = None
38TemplateNode = None
39ControlLine = None
40Text = None
41Code = None
42Comment = None
43Expression = None
44_TagMeta = None
45Tag = None
46IncludeTag = None
47NamespaceTag = None
48TextTag = None
49DefTag = None
50BlockTag = None
51CallTag = None
52CallNamespaceTag = None
53InheritTag = None
54PageTag = None
55
56# go through all the elements in parsetree and build out
57# mocks of them
58for cls in list(parsetree.__dict__.values()):
59    if isinstance(cls, type) and issubclass(cls, parsetree.Node):
60        clsname = cls.__name__
61        exec(
62            (
63                """
64class %s:
65    def __init__(self, *args):
66        self.args = [_as_unicode(arg) for arg in args]
67    def __repr__(self):
68        return "%%s(%%s)" %% (
69            self.__class__.__name__,
70            ", ".join(repr_arg(x) for x in self.args)
71            )
72"""
73                % clsname
74            ),
75            locals(),
76        )
77
78# NOTE: most assertion expressions were generated, then formatted
79# by PyTidy, hence the dense formatting.
80
81
82class LexerTest(TemplateTest):
83    def _compare(self, node, expected):
84        eq_(repr(node), repr(expected))
85
86    def test_text_and_tag(self):
87        template = """
88<b>Hello world</b>
89        <%def name="foo()">
90                this is a def.
91        </%def>
92
93        and some more text.
94"""
95        node = Lexer(template).parse()
96        self._compare(
97            node,
98            TemplateNode(
99                {},
100                [
101                    Text("""\n<b>Hello world</b>\n        """, (1, 1)),
102                    DefTag(
103                        "def",
104                        {"name": "foo()"},
105                        (3, 9),
106                        [
107                            Text(
108                                "\n                this is a def.\n        ",
109                                (3, 28),
110                            )
111                        ],
112                    ),
113                    Text("""\n\n        and some more text.\n""", (5, 16)),
114                ],
115            ),
116        )
117
118    def test_unclosed_tag(self):
119        template = """
120
121            <%def name="foo()">
122             other text
123        """
124        try:
125            Lexer(template).parse()
126            assert False
127        except exceptions.SyntaxException:
128            eq_(
129                str(compat.exception_as()),
130                "Unclosed tag: <%def> at line: 5 char: 9",
131            )
132
133    def test_onlyclosed_tag(self):
134        template = """
135            <%def name="foo()">
136                foo
137            </%def>
138
139            </%namespace>
140
141            hi.
142        """
143        assert_raises(exceptions.SyntaxException, Lexer(template).parse)
144
145    def test_noexpr_allowed(self):
146        template = """
147            <%namespace name="${foo}"/>
148        """
149        assert_raises(exceptions.CompileException, Lexer(template).parse)
150
151    def test_closing_tag_many_spaces(self):
152        """test #367"""
153        template = '<%def name="foo()"> this is a def. </%' + " " * 10000
154        assert_raises(exceptions.SyntaxException, Lexer(template).parse)
155
156    def test_opening_tag_many_quotes(self):
157        """test #366"""
158        template = "<%0" + '"' * 3000
159        assert_raises(exceptions.SyntaxException, Lexer(template).parse)
160
161    def test_unmatched_tag(self):
162        template = """
163        <%namespace name="bar">
164        <%def name="foo()">
165            foo
166            </%namespace>
167        </%def>
168
169
170        hi.
171"""
172        assert_raises(exceptions.SyntaxException, Lexer(template).parse)
173
174    def test_nonexistent_tag(self):
175        template = """
176            <%lala x="5"/>
177        """
178        assert_raises(exceptions.CompileException, Lexer(template).parse)
179
180    def test_wrongcase_tag(self):
181        template = """
182            <%DEF name="foo()">
183            </%def>
184
185        """
186        assert_raises(exceptions.CompileException, Lexer(template).parse)
187
188    def test_percent_escape(self):
189        template = """
190
191%% some whatever.
192
193    %% more some whatever
194    % if foo:
195    % endif
196        """
197        node = Lexer(template).parse()
198        self._compare(
199            node,
200            TemplateNode(
201                {},
202                [
203                    Text("""\n\n""", (1, 1)),
204                    Text("""% some whatever.\n\n""", (3, 2)),
205                    Text("   %% more some whatever\n", (5, 2)),
206                    ControlLine("if", "if foo:", False, (6, 1)),
207                    ControlLine("if", "endif", True, (7, 1)),
208                    Text("        ", (8, 1)),
209                ],
210            ),
211        )
212
213    def test_old_multiline_comment(self):
214        template = """#*"""
215        node = Lexer(template).parse()
216        self._compare(node, TemplateNode({}, [Text("""#*""", (1, 1))]))
217
218    def test_text_tag(self):
219        template = """
220        ## comment
221        % if foo:
222            hi
223        % endif
224        <%text>
225            # more code
226
227            % more code
228            <%illegal compionent>/></>
229            <%def name="laal()">def</%def>
230
231
232        </%text>
233
234        <%def name="foo()">this is foo</%def>
235
236        % if bar:
237            code
238        % endif
239        """
240        node = Lexer(template).parse()
241        self._compare(
242            node,
243            TemplateNode(
244                {},
245                [
246                    Text("\n", (1, 1)),
247                    Comment("comment", (2, 1)),
248                    ControlLine("if", "if foo:", False, (3, 1)),
249                    Text("            hi\n", (4, 1)),
250                    ControlLine("if", "endif", True, (5, 1)),
251                    Text("        ", (6, 1)),
252                    TextTag(
253                        "text",
254                        {},
255                        (6, 9),
256                        [
257                            Text(
258                                "\n            # more code\n\n           "
259                                " % more code\n            "
260                                "<%illegal compionent>/></>\n"
261                                '            <%def name="laal()">def</%def>'
262                                "\n\n\n        ",
263                                (6, 16),
264                            )
265                        ],
266                    ),
267                    Text("\n\n        ", (14, 17)),
268                    DefTag(
269                        "def",
270                        {"name": "foo()"},
271                        (16, 9),
272                        [Text("this is foo", (16, 28))],
273                    ),
274                    Text("\n\n", (16, 46)),
275                    ControlLine("if", "if bar:", False, (18, 1)),
276                    Text("            code\n", (19, 1)),
277                    ControlLine("if", "endif", True, (20, 1)),
278                    Text("        ", (21, 1)),
279                ],
280            ),
281        )
282
283    def test_def_syntax(self):
284        template = """
285        <%def lala>
286            hi
287        </%def>
288"""
289        assert_raises(exceptions.CompileException, Lexer(template).parse)
290
291    def test_def_syntax_2(self):
292        template = """
293        <%def name="lala">
294            hi
295        </%def>
296    """
297        assert_raises(exceptions.CompileException, Lexer(template).parse)
298
299    def test_whitespace_equals(self):
300        template = """
301            <%def name = "adef()" >
302              adef
303            </%def>
304        """
305        node = Lexer(template).parse()
306        self._compare(
307            node,
308            TemplateNode(
309                {},
310                [
311                    Text("\n            ", (1, 1)),
312                    DefTag(
313                        "def",
314                        {"name": "adef()"},
315                        (2, 13),
316                        [
317                            Text(
318                                """\n              adef\n            """,
319                                (2, 36),
320                            )
321                        ],
322                    ),
323                    Text("\n        ", (4, 20)),
324                ],
325            ),
326        )
327
328    def test_ns_tag_closed(self):
329        template = """
330
331            <%self:go x="1" y="2" z="${'hi' + ' ' + 'there'}"/>
332        """
333        nodes = Lexer(template).parse()
334        self._compare(
335            nodes,
336            TemplateNode(
337                {},
338                [
339                    Text(
340                        """
341
342            """,
343                        (1, 1),
344                    ),
345                    CallNamespaceTag(
346                        "self:go",
347                        {"x": "1", "y": "2", "z": "${'hi' + ' ' + 'there'}"},
348                        (3, 13),
349                        [],
350                    ),
351                    Text("\n        ", (3, 64)),
352                ],
353            ),
354        )
355
356    def test_ns_tag_empty(self):
357        template = """
358            <%form:option value=""></%form:option>
359        """
360        nodes = Lexer(template).parse()
361        self._compare(
362            nodes,
363            TemplateNode(
364                {},
365                [
366                    Text("\n            ", (1, 1)),
367                    CallNamespaceTag(
368                        "form:option", {"value": ""}, (2, 13), []
369                    ),
370                    Text("\n        ", (2, 51)),
371                ],
372            ),
373        )
374
375    def test_ns_tag_open(self):
376        template = """
377
378            <%self:go x="1" y="${process()}">
379                this is the body
380            </%self:go>
381        """
382        nodes = Lexer(template).parse()
383        self._compare(
384            nodes,
385            TemplateNode(
386                {},
387                [
388                    Text(
389                        """
390
391            """,
392                        (1, 1),
393                    ),
394                    CallNamespaceTag(
395                        "self:go",
396                        {"x": "1", "y": "${process()}"},
397                        (3, 13),
398                        [
399                            Text(
400                                """
401                this is the body
402            """,
403                                (3, 46),
404                            )
405                        ],
406                    ),
407                    Text("\n        ", (5, 24)),
408                ],
409            ),
410        )
411
412    def test_expr_in_attribute(self):
413        """test some slightly trickier expressions.
414
415        you can still trip up the expression parsing, though, unless we
416        integrated really deeply somehow with AST."""
417
418        template = """
419            <%call expr="foo>bar and 'lala' or 'hoho'"/>
420            <%call expr='foo<bar and hoho>lala and "x" + "y"'/>
421        """
422        nodes = Lexer(template).parse()
423        self._compare(
424            nodes,
425            TemplateNode(
426                {},
427                [
428                    Text("\n            ", (1, 1)),
429                    CallTag(
430                        "call",
431                        {"expr": "foo>bar and 'lala' or 'hoho'"},
432                        (2, 13),
433                        [],
434                    ),
435                    Text("\n            ", (2, 57)),
436                    CallTag(
437                        "call",
438                        {"expr": 'foo<bar and hoho>lala and "x" + "y"'},
439                        (3, 13),
440                        [],
441                    ),
442                    Text("\n        ", (3, 64)),
443                ],
444            ),
445        )
446
447    @pytest.mark.parametrize("comma,numchars", [(",", 48), ("", 47)])
448    def test_pagetag(self, comma, numchars):
449        # note that the comma here looks like:
450        # <%page cached="True", args="a, b"/>
451        # that's what this test has looked like for decades, however, the
452        # comma there is not actually the right syntax.  When issue #366
453        # was fixed, the reg was altered to accommodate for this comma to allow
454        # backwards compat
455        template = f"""
456            <%page cached="True"{comma} args="a, b"/>
457
458            some template
459        """
460        nodes = Lexer(template).parse()
461        self._compare(
462            nodes,
463            TemplateNode(
464                {},
465                [
466                    Text("\n            ", (1, 1)),
467                    PageTag(
468                        "page", {"args": "a, b", "cached": "True"}, (2, 13), []
469                    ),
470                    Text(
471                        """
472
473            some template
474        """,
475                        (2, numchars),
476                    ),
477                ],
478            ),
479        )
480
481    def test_nesting(self):
482        template = """
483
484        <%namespace name="ns">
485            <%def name="lala(hi, there)">
486                <%call expr="something()"/>
487            </%def>
488        </%namespace>
489
490        """
491        nodes = Lexer(template).parse()
492        self._compare(
493            nodes,
494            TemplateNode(
495                {},
496                [
497                    Text(
498                        """
499
500        """,
501                        (1, 1),
502                    ),
503                    NamespaceTag(
504                        "namespace",
505                        {"name": "ns"},
506                        (3, 9),
507                        [
508                            Text("\n            ", (3, 31)),
509                            DefTag(
510                                "def",
511                                {"name": "lala(hi, there)"},
512                                (4, 13),
513                                [
514                                    Text("\n                ", (4, 42)),
515                                    CallTag(
516                                        "call",
517                                        {"expr": "something()"},
518                                        (5, 17),
519                                        [],
520                                    ),
521                                    Text("\n            ", (5, 44)),
522                                ],
523                            ),
524                            Text("\n        ", (6, 20)),
525                        ],
526                    ),
527                    Text(
528                        """
529
530        """,
531                        (7, 22),
532                    ),
533                ],
534            ),
535        )
536
537    def test_code(self):
538        template = """text
539    <%
540        print("hi")
541        for x in range(1,5):
542            print(x)
543    %>
544more text
545    <%!
546        import foo
547    %>
548"""
549        nodes = Lexer(template).parse()
550        self._compare(
551            nodes,
552            TemplateNode(
553                {},
554                [
555                    Text("text\n    ", (1, 1)),
556                    Code(
557                        '\nprint("hi")\nfor x in range(1,5):\n    '
558                        "print(x)\n    \n",
559                        False,
560                        (2, 5),
561                    ),
562                    Text("\nmore text\n    ", (6, 7)),
563                    Code("\nimport foo\n    \n", True, (8, 5)),
564                    Text("\n", (10, 7)),
565                ],
566            ),
567        )
568
569    def test_code_and_tags(self):
570        template = """
571<%namespace name="foo">
572    <%def name="x()">
573        this is x
574    </%def>
575    <%def name="y()">
576        this is y
577    </%def>
578</%namespace>
579
580<%
581    result = []
582    data = get_data()
583    for x in data:
584        result.append(x+7)
585%>
586
587    result: <%call expr="foo.x(result)"/>
588"""
589        nodes = Lexer(template).parse()
590        self._compare(
591            nodes,
592            TemplateNode(
593                {},
594                [
595                    Text("\n", (1, 1)),
596                    NamespaceTag(
597                        "namespace",
598                        {"name": "foo"},
599                        (2, 1),
600                        [
601                            Text("\n    ", (2, 24)),
602                            DefTag(
603                                "def",
604                                {"name": "x()"},
605                                (3, 5),
606                                [
607                                    Text(
608                                        """\n        this is x\n    """,
609                                        (3, 22),
610                                    )
611                                ],
612                            ),
613                            Text("\n    ", (5, 12)),
614                            DefTag(
615                                "def",
616                                {"name": "y()"},
617                                (6, 5),
618                                [
619                                    Text(
620                                        """\n        this is y\n    """,
621                                        (6, 22),
622                                    )
623                                ],
624                            ),
625                            Text("\n", (8, 12)),
626                        ],
627                    ),
628                    Text("""\n\n""", (9, 14)),
629                    Code(
630                        """\nresult = []\ndata = get_data()\n"""
631                        """for x in data:\n    result.append(x+7)\n\n""",
632                        False,
633                        (11, 1),
634                    ),
635                    Text("""\n\n    result: """, (16, 3)),
636                    CallTag("call", {"expr": "foo.x(result)"}, (18, 13), []),
637                    Text("\n", (18, 42)),
638                ],
639            ),
640        )
641
642    def test_expression(self):
643        template = """
644        this is some ${text} and this is ${textwith | escapes, moreescapes}
645        <%def name="hi()">
646            give me ${foo()} and ${bar()}
647        </%def>
648        ${hi()}
649"""
650        nodes = Lexer(template).parse()
651        self._compare(
652            nodes,
653            TemplateNode(
654                {},
655                [
656                    Text("\n        this is some ", (1, 1)),
657                    Expression("text", [], (2, 22)),
658                    Text(" and this is ", (2, 29)),
659                    Expression(
660                        "textwith ", ["escapes", "moreescapes"], (2, 42)
661                    ),
662                    Text("\n        ", (2, 76)),
663                    DefTag(
664                        "def",
665                        {"name": "hi()"},
666                        (3, 9),
667                        [
668                            Text("\n            give me ", (3, 27)),
669                            Expression("foo()", [], (4, 21)),
670                            Text(" and ", (4, 29)),
671                            Expression("bar()", [], (4, 34)),
672                            Text("\n        ", (4, 42)),
673                        ],
674                    ),
675                    Text("\n        ", (5, 16)),
676                    Expression("hi()", [], (6, 9)),
677                    Text("\n", (6, 16)),
678                ],
679            ),
680        )
681
682    def test_tricky_expression(self):
683        template = """
684
685            ${x and "|" or "hi"}
686        """
687        nodes = Lexer(template).parse()
688        self._compare(
689            nodes,
690            TemplateNode(
691                {},
692                [
693                    Text("\n\n            ", (1, 1)),
694                    Expression('x and "|" or "hi"', [], (3, 13)),
695                    Text("\n        ", (3, 33)),
696                ],
697            ),
698        )
699
700        template = r"""
701
702            ${hello + '''heres '{|}' text | | }''' | escape1}
703            ${'Tricky string: ' + '\\\"\\\'|\\'}
704        """
705        nodes = Lexer(template).parse()
706        self._compare(
707            nodes,
708            TemplateNode(
709                {},
710                [
711                    Text("\n\n            ", (1, 1)),
712                    Expression(
713                        "hello + '''heres '{|}' text | | }''' ",
714                        ["escape1"],
715                        (3, 13),
716                    ),
717                    Text("\n            ", (3, 62)),
718                    Expression(
719                        r"""'Tricky string: ' + '\\\"\\\'|\\'""", [], (4, 13)
720                    ),
721                    Text("\n        ", (4, 49)),
722                ],
723            ),
724        )
725
726    def test_tricky_code(self):
727        template = """<% print('hi %>') %>"""
728        nodes = Lexer(template).parse()
729        self._compare(
730            nodes, TemplateNode({}, [Code("print('hi %>') \n", False, (1, 1))])
731        )
732
733    def test_tricky_code_2(self):
734        template = """<%
735        # someone's comment
736%>
737        """
738        nodes = Lexer(template).parse()
739        self._compare(
740            nodes,
741            TemplateNode(
742                {},
743                [
744                    Code(
745                        """
746        # someone's comment
747
748""",
749                        False,
750                        (1, 1),
751                    ),
752                    Text("\n        ", (3, 3)),
753                ],
754            ),
755        )
756
757    def test_tricky_code_3(self):
758        template = """<%
759        print('hi')
760        # this is a comment
761        # another comment
762        x = 7 # someone's '''comment
763        print('''
764    there
765    ''')
766        # someone else's comment
767%> '''and now some text '''"""
768        nodes = Lexer(template).parse()
769        self._compare(
770            nodes,
771            TemplateNode(
772                {},
773                [
774                    Code(
775                        """
776print('hi')
777# this is a comment
778# another comment
779x = 7 # someone's '''comment
780print('''
781    there
782    ''')
783# someone else's comment
784
785""",
786                        False,
787                        (1, 1),
788                    ),
789                    Text(" '''and now some text '''", (10, 3)),
790                ],
791            ),
792        )
793
794    def test_tricky_code_4(self):
795        template = """<% foo = "\\"\\\\" %>"""
796        nodes = Lexer(template).parse()
797        self._compare(
798            nodes,
799            TemplateNode({}, [Code("""foo = "\\"\\\\" \n""", False, (1, 1))]),
800        )
801
802    def test_tricky_code_5(self):
803        template = """before ${ {'key': 'value'} } after"""
804        nodes = Lexer(template).parse()
805        self._compare(
806            nodes,
807            TemplateNode(
808                {},
809                [
810                    Text("before ", (1, 1)),
811                    Expression(" {'key': 'value'} ", [], (1, 8)),
812                    Text(" after", (1, 29)),
813                ],
814            ),
815        )
816
817    def test_tricky_code_6(self):
818        template = """before ${ (0x5302 | 0x0400) } after"""
819        nodes = Lexer(template).parse()
820        self._compare(
821            nodes,
822            TemplateNode(
823                {},
824                [
825                    Text("before ", (1, 1)),
826                    Expression(" (0x5302 | 0x0400) ", [], (1, 8)),
827                    Text(" after", (1, 30)),
828                ],
829            ),
830        )
831
832    def test_control_lines(self):
833        template = """
834text text la la
835% if foo():
836 mroe text la la blah blah
837% endif
838
839        and osme more stuff
840        % for l in range(1,5):
841    tex tesl asdl l is ${l} kfmas d
842      % endfor
843    tetx text
844
845"""
846        nodes = Lexer(template).parse()
847        self._compare(
848            nodes,
849            TemplateNode(
850                {},
851                [
852                    Text("""\ntext text la la\n""", (1, 1)),
853                    ControlLine("if", "if foo():", False, (3, 1)),
854                    Text(" mroe text la la blah blah\n", (4, 1)),
855                    ControlLine("if", "endif", True, (5, 1)),
856                    Text("""\n        and osme more stuff\n""", (6, 1)),
857                    ControlLine("for", "for l in range(1,5):", False, (8, 1)),
858                    Text("    tex tesl asdl l is ", (9, 1)),
859                    Expression("l", [], (9, 24)),
860                    Text(" kfmas d\n", (9, 28)),
861                    ControlLine("for", "endfor", True, (10, 1)),
862                    Text("""    tetx text\n\n""", (11, 1)),
863                ],
864            ),
865        )
866
867    def test_control_lines_2(self):
868        template = """% for file in requestattr['toc'].filenames:
869    x
870% endfor
871"""
872        nodes = Lexer(template).parse()
873        self._compare(
874            nodes,
875            TemplateNode(
876                {},
877                [
878                    ControlLine(
879                        "for",
880                        "for file in requestattr['toc'].filenames:",
881                        False,
882                        (1, 1),
883                    ),
884                    Text("    x\n", (2, 1)),
885                    ControlLine("for", "endfor", True, (3, 1)),
886                ],
887            ),
888        )
889
890    def test_long_control_lines(self):
891        template = """
892    % for file in \\
893        requestattr['toc'].filenames:
894        x
895    % endfor
896        """
897        nodes = Lexer(template).parse()
898        self._compare(
899            nodes,
900            TemplateNode(
901                {},
902                [
903                    Text("\n", (1, 1)),
904                    ControlLine(
905                        "for",
906                        "for file in \\\n        "
907                        "requestattr['toc'].filenames:",
908                        False,
909                        (2, 1),
910                    ),
911                    Text("        x\n", (4, 1)),
912                    ControlLine("for", "endfor", True, (5, 1)),
913                    Text("        ", (6, 1)),
914                ],
915            ),
916        )
917
918    def test_unmatched_control(self):
919        template = """
920
921        % if foo:
922            % for x in range(1,5):
923        % endif
924"""
925        assert_raises_message(
926            exceptions.SyntaxException,
927            "Keyword 'endif' doesn't match keyword 'for' at line: 5 char: 1",
928            Lexer(template).parse,
929        )
930
931    def test_unmatched_control_2(self):
932        template = """
933
934        % if foo:
935            % for x in range(1,5):
936            % endfor
937"""
938
939        assert_raises_message(
940            exceptions.SyntaxException,
941            "Unterminated control keyword: 'if' at line: 3 char: 1",
942            Lexer(template).parse,
943        )
944
945    def test_unmatched_control_3(self):
946        template = """
947
948        % if foo:
949            % for x in range(1,5):
950            % endlala
951        % endif
952"""
953        assert_raises_message(
954            exceptions.SyntaxException,
955            "Keyword 'endlala' doesn't match keyword 'for' at line: 5 char: 1",
956            Lexer(template).parse,
957        )
958
959    def test_ternary_control(self):
960        template = """
961        % if x:
962            hi
963        % elif y+7==10:
964            there
965        % elif lala:
966            lala
967        % else:
968            hi
969        % endif
970"""
971        nodes = Lexer(template).parse()
972        self._compare(
973            nodes,
974            TemplateNode(
975                {},
976                [
977                    Text("\n", (1, 1)),
978                    ControlLine("if", "if x:", False, (2, 1)),
979                    Text("            hi\n", (3, 1)),
980                    ControlLine("elif", "elif y+7==10:", False, (4, 1)),
981                    Text("            there\n", (5, 1)),
982                    ControlLine("elif", "elif lala:", False, (6, 1)),
983                    Text("            lala\n", (7, 1)),
984                    ControlLine("else", "else:", False, (8, 1)),
985                    Text("            hi\n", (9, 1)),
986                    ControlLine("if", "endif", True, (10, 1)),
987                ],
988            ),
989        )
990
991    def test_integration(self):
992        template = """<%namespace name="foo" file="somefile.html"/>
993 ## inherit from foobar.html
994<%inherit file="foobar.html"/>
995
996<%def name="header()">
997     <div>header</div>
998</%def>
999<%def name="footer()">
1000    <div> footer</div>
1001</%def>
1002
1003<table>
1004    % for j in data():
1005    <tr>
1006        % for x in j:
1007            <td>Hello ${x| h}</td>
1008        % endfor
1009    </tr>
1010    % endfor
1011</table>
1012"""
1013        nodes = Lexer(template).parse()
1014        self._compare(
1015            nodes,
1016            TemplateNode(
1017                {},
1018                [
1019                    NamespaceTag(
1020                        "namespace",
1021                        {"file": "somefile.html", "name": "foo"},
1022                        (1, 1),
1023                        [],
1024                    ),
1025                    Text("\n", (1, 46)),
1026                    Comment("inherit from foobar.html", (2, 1)),
1027                    InheritTag("inherit", {"file": "foobar.html"}, (3, 1), []),
1028                    Text("""\n\n""", (3, 31)),
1029                    DefTag(
1030                        "def",
1031                        {"name": "header()"},
1032                        (5, 1),
1033                        [Text("""\n     <div>header</div>\n""", (5, 23))],
1034                    ),
1035                    Text("\n", (7, 8)),
1036                    DefTag(
1037                        "def",
1038                        {"name": "footer()"},
1039                        (8, 1),
1040                        [Text("""\n    <div> footer</div>\n""", (8, 23))],
1041                    ),
1042                    Text("""\n\n<table>\n""", (10, 8)),
1043                    ControlLine("for", "for j in data():", False, (13, 1)),
1044                    Text("    <tr>\n", (14, 1)),
1045                    ControlLine("for", "for x in j:", False, (15, 1)),
1046                    Text("            <td>Hello ", (16, 1)),
1047                    Expression("x", ["h"], (16, 23)),
1048                    Text("</td>\n", (16, 30)),
1049                    ControlLine("for", "endfor", True, (17, 1)),
1050                    Text("    </tr>\n", (18, 1)),
1051                    ControlLine("for", "endfor", True, (19, 1)),
1052                    Text("</table>\n", (20, 1)),
1053                ],
1054            ),
1055        )
1056
1057    def test_comment_after_statement(self):
1058        template = """
1059        % if x: #comment
1060            hi
1061        % else: #next
1062            hi
1063        % endif #end
1064"""
1065        nodes = Lexer(template).parse()
1066        self._compare(
1067            nodes,
1068            TemplateNode(
1069                {},
1070                [
1071                    Text("\n", (1, 1)),
1072                    ControlLine("if", "if x: #comment", False, (2, 1)),
1073                    Text("            hi\n", (3, 1)),
1074                    ControlLine("else", "else: #next", False, (4, 1)),
1075                    Text("            hi\n", (5, 1)),
1076                    ControlLine("if", "endif #end", True, (6, 1)),
1077                ],
1078            ),
1079        )
1080
1081    def test_crlf(self):
1082        template = util.read_file(self._file_path("crlf.html"))
1083        nodes = Lexer(template).parse()
1084        self._compare(
1085            nodes,
1086            TemplateNode(
1087                {},
1088                [
1089                    Text("<html>\r\n\r\n", (1, 1)),
1090                    PageTag(
1091                        "page",
1092                        {"args": "a=['foo',\n                'bar']"},
1093                        (3, 1),
1094                        [],
1095                    ),
1096                    Text("\r\n\r\nlike the name says.\r\n\r\n", (4, 26)),
1097                    ControlLine("for", "for x in [1,2,3]:", False, (8, 1)),
1098                    Text("        ", (9, 1)),
1099                    Expression("x", [], (9, 9)),
1100                    ControlLine("for", "endfor", True, (10, 1)),
1101                    Text("\r\n", (11, 1)),
1102                    Expression(
1103                        "trumpeter == 'Miles' and "
1104                        "trumpeter or \\\n      'Dizzy'",
1105                        [],
1106                        (12, 1),
1107                    ),
1108                    Text("\r\n\r\n", (13, 15)),
1109                    DefTag(
1110                        "def",
1111                        {"name": "hi()"},
1112                        (15, 1),
1113                        [Text("\r\n    hi!\r\n", (15, 19))],
1114                    ),
1115                    Text("\r\n\r\n</html>\r\n", (17, 8)),
1116                ],
1117            ),
1118        )
1119        assert (
1120            flatten_result(Template(template).render())
1121            == """<html> like the name says. 1 2 3 Dizzy </html>"""
1122        )
1123
1124    def test_comments(self):
1125        template = """
1126<style>
1127 #someselector
1128 # other non comment stuff
1129</style>
1130## a comment
1131
1132# also not a comment
1133
1134   ## this is a comment
1135
1136this is ## not a comment
1137
1138<%doc> multiline
1139comment
1140</%doc>
1141
1142hi
1143"""
1144        nodes = Lexer(template).parse()
1145        self._compare(
1146            nodes,
1147            TemplateNode(
1148                {},
1149                [
1150                    Text(
1151                        """\n<style>\n #someselector\n # """
1152                        """other non comment stuff\n</style>\n""",
1153                        (1, 1),
1154                    ),
1155                    Comment("a comment", (6, 1)),
1156                    Text("""\n# also not a comment\n\n""", (7, 1)),
1157                    Comment("this is a comment", (10, 1)),
1158                    Text("""\nthis is ## not a comment\n\n""", (11, 1)),
1159                    Comment(""" multiline\ncomment\n""", (14, 1)),
1160                    Text(
1161                        """
1162
1163hi
1164""",
1165                        (16, 8),
1166                    ),
1167                ],
1168            ),
1169        )
1170
1171    def test_docs(self):
1172        template = """
1173        <%doc>
1174            this is a comment
1175        </%doc>
1176        <%def name="foo()">
1177            <%doc>
1178                this is the foo func
1179            </%doc>
1180        </%def>
1181        """
1182        nodes = Lexer(template).parse()
1183        self._compare(
1184            nodes,
1185            TemplateNode(
1186                {},
1187                [
1188                    Text("\n        ", (1, 1)),
1189                    Comment(
1190                        """\n            this is a comment\n        """, (2, 9)
1191                    ),
1192                    Text("\n        ", (4, 16)),
1193                    DefTag(
1194                        "def",
1195                        {"name": "foo()"},
1196                        (5, 9),
1197                        [
1198                            Text("\n            ", (5, 28)),
1199                            Comment(
1200                                """\n                this is the foo func\n"""
1201                                """            """,
1202                                (6, 13),
1203                            ),
1204                            Text("\n        ", (8, 20)),
1205                        ],
1206                    ),
1207                    Text("\n        ", (9, 16)),
1208                ],
1209            ),
1210        )
1211
1212    def test_preprocess(self):
1213        def preproc(text):
1214            return re.sub(r"(?<=\n)\s*#[^#]", "##", text)
1215
1216        template = """
1217    hi
1218    # old style comment
1219# another comment
1220"""
1221        nodes = Lexer(template, preprocessor=preproc).parse()
1222        self._compare(
1223            nodes,
1224            TemplateNode(
1225                {},
1226                [
1227                    Text("""\n    hi\n""", (1, 1)),
1228                    Comment("old style comment", (3, 1)),
1229                    Comment("another comment", (4, 1)),
1230                ],
1231            ),
1232        )
1233