check-templates: Check Django/Handlebars templates differently.

This is a bit hacky, but avoids incorrectly checking Handlebars
templates for Django style templates, which in particular interacts
poorly with ICU Message plural syntax.
This commit is contained in:
Tim Abbott 2023-10-11 16:56:46 -07:00
parent b2ea3125b2
commit 533ff863a7
4 changed files with 81 additions and 44 deletions

View File

@ -101,7 +101,7 @@ def check_html_templates(templates: Iterable[str], all_dups: bool, fix: bool) ->
sys.exit(1) sys.exit(1)
for fn in templates: for fn in templates:
tokens = validate(fn) tokens = validate(fn, template_format="django")
if not validate_indent_html(fn, tokens, fix): if not validate_indent_html(fn, tokens, fix):
sys.exit(1) sys.exit(1)
@ -111,7 +111,7 @@ def check_handlebars_templates(templates: Iterable[str], fix: bool) -> None:
templates = [fn for fn in templates if fn.endswith(".hbs")] templates = [fn for fn in templates if fn.endswith(".hbs")]
for fn in templates: for fn in templates:
tokens = validate(fn) tokens = validate(fn, template_format="handlebars")
if not validate_indent_html(fn, tokens, fix): if not validate_indent_html(fn, tokens, fix):
sys.exit(1) sys.exit(1)

View File

@ -48,7 +48,7 @@ class Token:
self.parent_token: Optional[Token] = None self.parent_token: Optional[Token] = None
def tokenize(text: str) -> List[Token]: def tokenize(text: str, template_format: Optional[str] = None) -> List[Token]:
in_code_block = False in_code_block = False
def advance(n: int) -> None: def advance(n: int) -> None:
@ -70,13 +70,13 @@ def tokenize(text: str) -> List[Token]:
return looking_at("{{!") return looking_at("{{!")
def looking_at_djangocomment() -> bool: def looking_at_djangocomment() -> bool:
return looking_at("{#") return template_format == "django" and looking_at("{#")
def looking_at_handlebars_partial() -> bool: def looking_at_handlebars_partial() -> bool:
return looking_at("{{>") return template_format == "handlebars" and looking_at("{{>")
def looking_at_handlebars_partial_block() -> bool: def looking_at_handlebars_partial_block() -> bool:
return looking_at("{{#>") return template_format == "handlebars" and looking_at("{{#>")
def looking_at_html_start() -> bool: def looking_at_html_start() -> bool:
return looking_at("<") and not looking_at("</") return looking_at("<") and not looking_at("</")
@ -88,29 +88,29 @@ def tokenize(text: str) -> List[Token]:
return looking_at("{{#") or looking_at("{{^") or looking_at("{{~#") return looking_at("{{#") or looking_at("{{^") or looking_at("{{~#")
def looking_at_handlebars_else() -> bool: def looking_at_handlebars_else() -> bool:
return looking_at("{{else") return template_format == "handlebars" and looking_at("{{else")
def looking_at_template_var() -> bool: def looking_at_template_var() -> bool:
return looking_at("{") return looking_at("{")
def looking_at_handlebars_end() -> bool: def looking_at_handlebars_end() -> bool:
return looking_at("{{/") or looking_at("{{~/") return template_format == "handlebars" and (looking_at("{{/") or looking_at("{{~/"))
def looking_at_django_start() -> bool: def looking_at_django_start() -> bool:
return looking_at("{% ") return template_format == "django" and looking_at("{% ")
def looking_at_django_else() -> bool: def looking_at_django_else() -> bool:
return looking_at("{% else") or looking_at("{% elif") return template_format == "django" and (looking_at("{% else") or looking_at("{% elif"))
def looking_at_django_end() -> bool: def looking_at_django_end() -> bool:
return looking_at("{% end") return template_format == "django" and looking_at("{% end")
def looking_at_jinja2_end_whitespace_stripped() -> bool: def looking_at_jinja2_end_whitespace_stripped() -> bool:
return looking_at("{%- end") return template_format == "django" and looking_at("{%- end")
def looking_at_jinja2_start_whitespace_stripped_type2() -> bool: def looking_at_jinja2_start_whitespace_stripped_type2() -> bool:
# This function detects tag like {%- if foo -%}...{% endif %} # This function detects tag like {%- if foo -%}...{% endif %}
return looking_at("{%-") and not looking_at("{%- end") return template_format == "django" and looking_at("{%-") and not looking_at("{%- end")
def looking_at_whitespace() -> bool: def looking_at_whitespace() -> bool:
return looking_at("\n") or looking_at(" ") return looking_at("\n") or looking_at(" ")
@ -343,7 +343,11 @@ def tag_flavor(token: Token) -> Optional[str]:
raise AssertionError(f"tools programmer neglected to handle {kind} tokens") raise AssertionError(f"tools programmer neglected to handle {kind} tokens")
def validate(fn: Optional[str] = None, text: Optional[str] = None) -> List[Token]: def validate(
fn: Optional[str] = None,
text: Optional[str] = None,
template_format: Optional[str] = None,
) -> List[Token]:
assert fn or text assert fn or text
if fn is None: if fn is None:
@ -356,7 +360,7 @@ def validate(fn: Optional[str] = None, text: Optional[str] = None) -> List[Token
lines = text.split("\n") lines = text.split("\n")
try: try:
tokens = tokenize(text) tokens = tokenize(text, template_format=template_format)
except FormattedError as e: except FormattedError as e:
raise TemplateParserError( raise TemplateParserError(
f""" f"""

View File

@ -1,4 +1,5 @@
import unittest import unittest
from typing import Optional
from tools.lib.pretty_print import pretty_print_html from tools.lib.pretty_print import pretty_print_html
from tools.lib.template_parser import validate from tools.lib.template_parser import validate
@ -280,9 +281,9 @@ GOOD_HTML11 = """
""" """
def pretty_print(html: str) -> str: def pretty_print(html: str, template_format: Optional[str] = None) -> str:
fn = "<test str>" fn = "<test str>"
tokens = validate(fn=fn, text=html) tokens = validate(fn=fn, text=html, template_format=template_format)
return pretty_print_html(tokens, fn=fn) return pretty_print_html(tokens, fn=fn)
@ -294,12 +295,12 @@ class TestPrettyPrinter(unittest.TestCase):
self.compare(pretty_print(GOOD_HTML), GOOD_HTML) self.compare(pretty_print(GOOD_HTML), GOOD_HTML)
self.compare(pretty_print(BAD_HTML), GOOD_HTML) self.compare(pretty_print(BAD_HTML), GOOD_HTML)
self.compare(pretty_print(BAD_HTML1), GOOD_HTML1) self.compare(pretty_print(BAD_HTML1), GOOD_HTML1)
self.compare(pretty_print(BAD_HTML2), GOOD_HTML2) self.compare(pretty_print(BAD_HTML2, template_format="handlebars"), GOOD_HTML2)
self.compare(pretty_print(BAD_HTML4), GOOD_HTML4) self.compare(pretty_print(BAD_HTML4), GOOD_HTML4)
self.compare(pretty_print(BAD_HTML5), GOOD_HTML5) self.compare(pretty_print(BAD_HTML5, template_format="handlebars"), GOOD_HTML5)
self.compare(pretty_print(BAD_HTML6), GOOD_HTML6) self.compare(pretty_print(BAD_HTML6), GOOD_HTML6)
self.compare(pretty_print(BAD_HTML7), GOOD_HTML7) self.compare(pretty_print(BAD_HTML7, template_format="handlebars"), GOOD_HTML7)
self.compare(pretty_print(BAD_HTML8), GOOD_HTML8) self.compare(pretty_print(BAD_HTML8, template_format="handlebars"), GOOD_HTML8)
self.compare(pretty_print(BAD_HTML9), GOOD_HTML9) self.compare(pretty_print(BAD_HTML9, template_format="handlebars"), GOOD_HTML9)
self.compare(pretty_print(BAD_HTML10), GOOD_HTML10) self.compare(pretty_print(BAD_HTML10, template_format="django"), GOOD_HTML10)
self.compare(pretty_print(BAD_HTML11), GOOD_HTML11) self.compare(pretty_print(BAD_HTML11), GOOD_HTML11)

View File

@ -20,9 +20,10 @@ class ParserTest(unittest.TestCase):
error: str, error: str,
fn: Optional[str] = None, fn: Optional[str] = None,
text: Optional[str] = None, text: Optional[str] = None,
template_format: Optional[str] = None,
) -> None: ) -> None:
with self.assertRaisesRegex(TemplateParserError, error): with self.assertRaisesRegex(TemplateParserError, error):
validate(fn=fn, text=text) validate(fn=fn, text=text, template_format=template_format)
def test_is_django_block_tag(self) -> None: def test_is_django_block_tag(self) -> None:
self.assertTrue(is_django_block_tag("block")) self.assertTrue(is_django_block_tag("block"))
@ -47,7 +48,7 @@ class ParserTest(unittest.TestCase):
<p>{{stream}}</p> <p>{{stream}}</p>
{{/with}} {{/with}}
""" """
validate(text=my_html) validate(text=my_html, template_format="handlebars")
def test_validate_handlebars_partial_block(self) -> None: def test_validate_handlebars_partial_block(self) -> None:
my_html = """ my_html = """
@ -55,7 +56,7 @@ class ParserTest(unittest.TestCase):
<p>hello!</p> <p>hello!</p>
{{/generic_thing}} {{/generic_thing}}
""" """
validate(text=my_html) validate(text=my_html, template_format="handlebars")
def test_validate_bad_handlebars_partial_block(self) -> None: def test_validate_bad_handlebars_partial_block(self) -> None:
my_html = """ my_html = """
@ -63,7 +64,9 @@ class ParserTest(unittest.TestCase):
<p>hello!</p> <p>hello!</p>
{{# generic_thing}} {{# generic_thing}}
""" """
self._assert_validate_error("Missing end tag for the token at row 4 13!", text=my_html) self._assert_validate_error(
"Missing end tag for the token at row 4 13!", text=my_html, template_format="handlebars"
)
def test_validate_comment(self) -> None: def test_validate_comment(self) -> None:
my_html = """ my_html = """
@ -79,7 +82,7 @@ class ParserTest(unittest.TestCase):
<p>bar</p> <p>bar</p>
{% endif %} {% endif %}
""" """
validate(text=my_html) validate(text=my_html, template_format="django")
my_html = """ my_html = """
{% block "content" %} {% block "content" %}
@ -124,13 +127,18 @@ class ParserTest(unittest.TestCase):
'''Tag missing "}}" at line 2 col 13:"{{# foo '''Tag missing "}}" at line 2 col 13:"{{# foo
"''', "''',
text=my_html, text=my_html,
template_format="handlebars",
) )
def test_validate_incomplete_handlebars_tag_2(self) -> None: def test_validate_incomplete_handlebars_tag_2(self) -> None:
my_html = """ my_html = """
{{# foo } {{# foo }
""" """
self._assert_validate_error('Tag missing "}}" at line 2 col 13:"{{# foo }\n"', text=my_html) self._assert_validate_error(
'Tag missing "}}" at line 2 col 13:"{{# foo }\n"',
text=my_html,
template_format="handlebars",
)
def test_validate_incomplete_django_tag_1(self) -> None: def test_validate_incomplete_django_tag_1(self) -> None:
my_html = """ my_html = """
@ -140,13 +148,18 @@ class ParserTest(unittest.TestCase):
'''Tag missing "%}" at line 2 col 13:"{% foo '''Tag missing "%}" at line 2 col 13:"{% foo
"''', "''',
text=my_html, text=my_html,
template_format="django",
) )
def test_validate_incomplete_django_tag_2(self) -> None: def test_validate_incomplete_django_tag_2(self) -> None:
my_html = """ my_html = """
{% foo % {% foo %
""" """
self._assert_validate_error('Tag missing "%}" at line 2 col 13:"{% foo %\n"', text=my_html) self._assert_validate_error(
'Tag missing "%}" at line 2 col 13:"{% foo %\n"',
text=my_html,
template_format="django",
)
def test_validate_incomplete_html_tag_1(self) -> None: def test_validate_incomplete_html_tag_1(self) -> None:
my_html = """ my_html = """
@ -230,7 +243,10 @@ class ParserTest(unittest.TestCase):
this is foo this is foo
{% endif %} {% endif %}
""" """
validate(text=my_html) validate(
text=my_html,
template_format="django",
)
def test_validate_jinja2_whitespace_markers_2(self) -> None: def test_validate_jinja2_whitespace_markers_2(self) -> None:
my_html = """ my_html = """
@ -238,7 +254,10 @@ class ParserTest(unittest.TestCase):
this is foo this is foo
{%- endif %} {%- endif %}
""" """
validate(text=my_html) validate(
text=my_html,
template_format="django",
)
def test_validate_jinja2_whitespace_markers_3(self) -> None: def test_validate_jinja2_whitespace_markers_3(self) -> None:
my_html = """ my_html = """
@ -246,7 +265,10 @@ class ParserTest(unittest.TestCase):
this is foo this is foo
{% endif -%} {% endif -%}
""" """
validate(text=my_html) validate(
text=my_html,
template_format="django",
)
def test_validate_jinja2_whitespace_markers_4(self) -> None: def test_validate_jinja2_whitespace_markers_4(self) -> None:
my_html = """ my_html = """
@ -254,7 +276,10 @@ class ParserTest(unittest.TestCase):
this is foo this is foo
{% endif %} {% endif %}
""" """
validate(text=my_html) validate(
text=my_html,
template_format="django",
)
def test_validate_mismatch_jinja2_whitespace_markers_1(self) -> None: def test_validate_mismatch_jinja2_whitespace_markers_1(self) -> None:
my_html = """ my_html = """
@ -262,7 +287,11 @@ class ParserTest(unittest.TestCase):
this is foo this is foo
{%- if bar %} {%- if bar %}
""" """
self._assert_validate_error("Missing end tag", text=my_html) self._assert_validate_error(
"Missing end tag",
text=my_html,
template_format="django",
)
def test_validate_jinja2_whitespace_type2_markers(self) -> None: def test_validate_jinja2_whitespace_type2_markers(self) -> None:
my_html = """ my_html = """
@ -270,7 +299,10 @@ class ParserTest(unittest.TestCase):
this is foo this is foo
{% endif %} {% endif %}
""" """
validate(text=my_html) validate(
text=my_html,
template_format="django",
)
def test_tokenize(self) -> None: def test_tokenize(self) -> None:
tag = "<!DOCTYPE html>" tag = "<!DOCTYPE html>"
@ -303,41 +335,41 @@ class ParserTest(unittest.TestCase):
self.assertEqual(token.tag, "a") self.assertEqual(token.tag, "a")
tag = "{{#with foo}}bla" tag = "{{#with foo}}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="handlebars")[0]
self.assertEqual(token.kind, "handlebars_start") self.assertEqual(token.kind, "handlebars_start")
self.assertEqual(token.tag, "with") self.assertEqual(token.tag, "with")
tag = "{{/with}}bla" tag = "{{/with}}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="handlebars")[0]
self.assertEqual(token.kind, "handlebars_end") self.assertEqual(token.kind, "handlebars_end")
self.assertEqual(token.tag, "with") self.assertEqual(token.tag, "with")
tag = "{{#> compose_banner }}bla" tag = "{{#> compose_banner }}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="handlebars")[0]
self.assertEqual(token.kind, "handlebars_partial_block") self.assertEqual(token.kind, "handlebars_partial_block")
self.assertEqual(token.tag, "compose_banner") self.assertEqual(token.tag, "compose_banner")
tag = "{% if foo %}bla" tag = "{% if foo %}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="django")[0]
self.assertEqual(token.kind, "django_start") self.assertEqual(token.kind, "django_start")
self.assertEqual(token.tag, "if") self.assertEqual(token.tag, "if")
tag = "{% endif %}bla" tag = "{% endif %}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="django")[0]
self.assertEqual(token.kind, "django_end") self.assertEqual(token.kind, "django_end")
self.assertEqual(token.tag, "if") self.assertEqual(token.tag, "if")
tag = "{% if foo -%}bla" tag = "{% if foo -%}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="django")[0]
self.assertEqual(token.kind, "jinja2_whitespace_stripped_start") self.assertEqual(token.kind, "jinja2_whitespace_stripped_start")
self.assertEqual(token.tag, "if") self.assertEqual(token.tag, "if")
tag = "{%- endif %}bla" tag = "{%- endif %}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="django")[0]
self.assertEqual(token.kind, "jinja2_whitespace_stripped_end") self.assertEqual(token.kind, "jinja2_whitespace_stripped_end")
self.assertEqual(token.tag, "if") self.assertEqual(token.tag, "if")
tag = "{%- if foo -%}bla" tag = "{%- if foo -%}bla"
token = tokenize(tag)[0] token = tokenize(tag, template_format="django")[0]
self.assertEqual(token.kind, "jinja2_whitespace_stripped_type2_start") self.assertEqual(token.kind, "jinja2_whitespace_stripped_type2_start")
self.assertEqual(token.tag, "if") self.assertEqual(token.tag, "if")