From 9976ffb42918a321a54a61e5774aeec3ba0184f3 Mon Sep 17 00:00:00 2001 From: Antoine Motet Date: Sun, 2 Sep 2018 17:54:16 +0200 Subject: [PATCH] Optional container support Fix #5. --- jinjalint/ast.py | 23 ++++++++ jinjalint/parse.py | 117 +++++++++++++++++++++++++++++++++++----- jinjalint/parse_test.py | 17 ++++-- 3 files changed, 141 insertions(+), 16 deletions(-) diff --git a/jinjalint/ast.py b/jinjalint/ast.py index b7314d2..5246aba 100644 --- a/jinjalint/ast.py +++ b/jinjalint/ast.py @@ -170,6 +170,29 @@ def __str__(self): ) +@attr.s(frozen=True) +class JinjaOptionalContainer(Node): + first_opening_if = attr.ib() # JinjaTag + opening_tag = attr.ib() # OpeningTag + first_closing_if = attr.ib() # JinjaTag + content = attr.ib() # Interpolated + second_opening_if = attr.ib() # JinjaTag + closing_tag = attr.ib() # ClosingTag + second_closing_if = attr.ib() # JinjaTag + + def __str__(self): + nodes = [ + self.first_opening_if, + self.opening_tag, + self.first_closing_if, + self.content, + self.second_opening_if, + self.closing_tag, + self.second_closing_if, + ] + return ''.join(str(n) for n in nodes) + + @attr.s(frozen=True) class InterpolatedBase(Node): nodes = attr.ib() # [any | Jinja] diff --git a/jinjalint/parse.py b/jinjalint/parse.py index ada64fa..f6dd949 100644 --- a/jinjalint/parse.py +++ b/jinjalint/parse.py @@ -5,7 +5,7 @@ from .ast import ( Attribute, ClosingTag, Comment, Element, Integer, Interpolated, Jinja, JinjaComment, JinjaElement, JinjaElementPart, JinjaTag, - JinjaVariable, Location, OpeningTag, String, + JinjaVariable, JinjaOptionalContainer, Location, OpeningTag, String, ) from .util import flatten @@ -34,7 +34,7 @@ ('filter', 'endfilter'), ('for', 'else', 'empty', 'endfor'), ('if', 'elif', 'else', 'endif'), - ('ifchanged', 'endifchanged'), + ('ifchanged', 'else', 'endifchanged'), ('ifequal', 'endifequal'), ('ifnotequal', 'endifnotequal'), ('spaceless', 'endspaceless'), @@ -169,6 +169,13 @@ def make_jinja_element_part_parser(name_parser, content): def make_jinja_element_parser(name_parsers, content): + """ + `name_parsers` must be a list of tag name parsers. For example, + `name_parsers` can be defined as follow in order to parse `if` statements: + + name_parsers = [P.string(n) for n in ['if', 'elif', 'else', 'endif']] + """ + if len(name_parsers) == 1: tag = make_jinja_tag_parser(name_parsers[0]) part = locate(P.seq( @@ -454,12 +461,81 @@ def make_raw_text_element_parser(config, tag_name, jinja): quick_text = P.regex(r'[^{<]+', flags=re.DOTALL) -def make_jinja_parser(config, content): - jinja_structured_elements_names = ( - DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES + - config.get('jinja_custom_elements_names', []) +def _combine_optional_container(locations, nodes): + return JinjaOptionalContainer( + first_opening_if=nodes[0], + opening_tag=nodes[1], + first_closing_if=nodes[2], + content=nodes[3], + second_opening_if=nodes[4], + closing_tag=nodes[5], + second_closing_if=nodes[6], + **locations, + ) + + +# Awkward hack to handle optional HTML containers, for example: +# +# {% if a %} +#
+# {% endif %} +# foo +# {% if a %} +#
+# {% endif %} +# +# Currently, this only works with `if` statements and the two conditions +# must be exactly the same. +def make_jinja_optional_container_parser(config, content, jinja): + jinja_if = make_jinja_tag_parser(P.string('if')) + jinja_endif = make_jinja_tag_parser(P.string('endif')) + opening_tag = make_opening_tag_parser(config, jinja, allow_slash=False) + + @P.generate + def opt_container_impl(): + o_first_if_node = yield jinja_if + o_tag_node = yield opening_tag + c_first_if_node = yield jinja_endif + + content_nodes = yield content + + o_second_if_node = yield P.string(str(o_first_if_node)) + html_tag_name = o_tag_node.name + if isinstance(html_tag_name, str): + closing_tag = make_closing_tag_parser(P.string(html_tag_name)) + else: + assert isinstance(html_tag_name, Jinja) + closing_tag = make_closing_tag_parser(jinja) + c_tag_node = yield closing_tag + c_second_if_node = yield jinja_endif + + return [ + o_first_if_node, + o_tag_node, + c_first_if_node, + content_nodes, + o_second_if_node, + c_tag_node, + c_second_if_node, + ] + + return ( + locate(opt_container_impl) + .combine(_combine_optional_container) ) + +def make_jinja_parser(config, content): + # Allow to override elements with the configuration + jinja_structured_elements_names = dict( + (names[0], names) + for names + in ( + DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES + + config.get('jinja_custom_elements_names', []) + ) + ).values() + jinja_structured_element = P.alt(*[ make_jinja_element_parser( [P.string(name) for name in names], @@ -469,20 +545,29 @@ def make_jinja_parser(config, content): ]) # These tag names can't begin a Jinja element - jinja_intermediate_tag_names = [ + jinja_intermediate_tag_names = set( n for _, *sublist in jinja_structured_elements_names for n in sublist - ] + ) jinja_intermediate_tag_name = P.alt(*( P.string(n) for n in jinja_intermediate_tag_names )) jinja_element_single = make_jinja_element_parser( - [jinja_intermediate_tag_name - .should_fail('not an intermediate Jinja tag name') - .then(jinja_name)], + [ + P.alt( + # HACK: If we allow `{% if %}`s without `{% endif %}`s here, + # `make_jinja_optional_container_parser` doesn’t work. It + # is probably better to reject any structured tag name here. + P.string('if'), + + jinja_intermediate_tag_name + ) + .should_fail('not an intermediate Jinja tag name') + .then(jinja_name) + ], content=content, ) @@ -557,12 +642,18 @@ def content(): element = make_element_parser(config, content, jinja) + opt_container = make_jinja_optional_container_parser( + config, content, jinja) + content_ = interpolated( - (quick_text | comment | dtd | element | jinja | slow_text_char).many() + ( + quick_text | comment | dtd | element | opt_container | jinja | + slow_text_char + ).many() ) return { - 'content': content, + 'content': content_, 'jinja': jinja, 'element': element, } diff --git a/jinjalint/parse_test.py b/jinjalint/parse_test.py index 4af1c4f..81a7129 100644 --- a/jinjalint/parse_test.py +++ b/jinjalint/parse_test.py @@ -1,7 +1,7 @@ import parsy as P from .parse import ( - tag_name, tag_name_char, comment, + tag_name, tag_name_char, comment, jinja_comment, make_attribute_value_parser, make_attribute_parser, make_attributes_parser, make_closing_tag_parser, make_opening_tag_parser, make_parser, @@ -54,6 +54,7 @@ def create_node(*args, **kwargs): Element = with_dummy_locations(ast.Element) ClosingTag = with_dummy_locations(ast.ClosingTag) Comment = with_dummy_locations(ast.Comment) +JinjaComment = with_dummy_locations(ast.JinjaComment) Integer = with_dummy_locations(ast.Integer) Interp = with_dummy_locations(ast.Interpolated) JinjaComment = with_dummy_locations(ast.JinjaComment) @@ -168,6 +169,12 @@ def test_comment(): ) +def test_jinja_comment(): + assert jinja_comment.parse('{# hello world #}') == JinjaComment( + text='hello world', + ) + + def test_opening_tag(): assert opening_tag.parse('
') == OpeningTag( name='div', @@ -298,6 +305,9 @@ def test_self_closing_elements(): closing_tag=None, ) + src = '
' + assert src == str(element.parse(src)) + def test_jinja_blocks(): assert jinja.parse('{% name something == 123 %}') == JinjaElement( @@ -360,6 +370,7 @@ def test(): test_attribute_value() test_attribute() test_comment() + test_jinja_comment() test_opening_tag() test_closing_tag() test_raw_text_elements() @@ -372,5 +383,5 @@ def test(): src = 'Hello
' assert src == str(element.parse(src)) - src = '
' - assert src == str(element.parse(src)) + src = '{% if a %}{% endif %}cd{% if a %}{% endif %}' + assert src == str(content.parse(src))