diff --git a/tools/lib/template_parser.py b/tools/lib/template_parser.py index 92810b1c4a..32a1db8b58 100644 --- a/tools/lib/template_parser.py +++ b/tools/lib/template_parser.py @@ -5,8 +5,19 @@ from six.moves import range import re class TemplateParserException(Exception): - # TODO: Have callers pass in line numbers. - pass + def __init__(self, message): + # type: (str) -> None + self.message = message + + def __str__(self): + # type: () -> str + return self.message + +class TokenizationException(Exception): + def __init__(self, message, line_content=None): + # type: (str, str) -> None + self.message = message + self.line_content = line_content class TokenizerState(object): def __init__(self): @@ -72,48 +83,53 @@ def tokenize(text): tokens = [] while state.i < len(text): - if looking_at_comment(): - s = get_html_comment(text, state.i) - tag = s[4:-3] - kind = 'html_comment' - elif looking_at_html_start(): - s = get_html_tag(text, state.i) - tag_parts = s[1:-1].split() + try: + if looking_at_comment(): + s = get_html_comment(text, state.i) + tag = s[4:-3] + kind = 'html_comment' + elif looking_at_html_start(): + s = get_html_tag(text, state.i) + tag_parts = s[1:-1].split() - if not tag_parts: - raise TemplateParserException("Tag name missing") + if not tag_parts: + raise TemplateParserException("Tag name missing") - tag = tag_parts[0] + tag = tag_parts[0] - if is_special_html_tag(s, tag): - kind = 'html_special' - elif s.endswith('/>'): - kind = 'html_singleton' + if is_special_html_tag(s, tag): + kind = 'html_special' + elif s.endswith('/>'): + kind = 'html_singleton' + else: + kind = 'html_start' + elif looking_at_html_end(): + s = get_html_tag(text, state.i) + tag = s[2:-1] + kind = 'html_end' + elif looking_at_handlebars_start(): + s = get_handlebars_tag(text, state.i) + tag = s[3:-2].split()[0] + kind = 'handlebars_start' + elif looking_at_handlebars_end(): + s = get_handlebars_tag(text, state.i) + tag = s[3:-2] + kind = 'handlebars_end' + elif looking_at_django_start(): + s = get_django_tag(text, state.i) + tag = s[3:-2].split()[0] + kind = 'django_start' + elif looking_at_django_end(): + s = get_django_tag(text, state.i) + tag = s[6:-3] + kind = 'django_end' else: - kind = 'html_start' - elif looking_at_html_end(): - s = get_html_tag(text, state.i) - tag = s[2:-1] - kind = 'html_end' - elif looking_at_handlebars_start(): - s = get_handlebars_tag(text, state.i) - tag = s[3:-2].split()[0] - kind = 'handlebars_start' - elif looking_at_handlebars_end(): - s = get_handlebars_tag(text, state.i) - tag = s[3:-2] - kind = 'handlebars_end' - elif looking_at_django_start(): - s = get_django_tag(text, state.i) - tag = s[3:-2].split()[0] - kind = 'django_start' - elif looking_at_django_end(): - s = get_django_tag(text, state.i) - tag = s[6:-3] - kind = 'django_end' - else: - advance(1) - continue + advance(1) + continue + except TokenizationException as e: + raise TemplateParserException('''%s at Line %d Col %d:"%s"''' % + (e.message, state.line, state.col, + e.line_content)) token = Token( kind=kind, @@ -249,7 +265,7 @@ def get_handlebars_tag(text, i): while end < len(text) - 1 and text[end] != '}': end += 1 if text[end] != '}' or text[end+1] != '}': - raise TemplateParserException('Tag missing }}') + raise TokenizationException('Tag missing "}}"', text[i:end+2]) s = text[i:end+2] return s @@ -259,7 +275,7 @@ def get_django_tag(text, i): while end < len(text) - 1 and text[end] != '%': end += 1 if text[end] != '%' or text[end+1] != '}': - raise TemplateParserException('Tag missing %}') + raise TokenizationException('Tag missing "%}"', text[i:end+2]) s = text[i:end+2] return s @@ -267,20 +283,31 @@ def get_html_tag(text, i): # type: (str, int) -> str quote_count = 0 end = i + 1 - while end < len(text) and (text[end] != '>' or quote_count % 2 != 0): + unclosed_end = 0 + while end < len(text) and (text[end] != '>' or quote_count % 2 != 0 and text[end] != '<'): if text[end] == '"': quote_count += 1 + if not unclosed_end and text[end] == '<': + unclosed_end = end end += 1 + if quote_count % 2 != 0: + if unclosed_end: + raise TokenizationException('Unbalanced Quotes', text[i:unclosed_end]) + else: + raise TokenizationException('Unbalanced Quotes', text[i:end+1]) if end == len(text) or text[end] != '>': - raise TemplateParserException('Tag missing >') + raise TokenizationException('Tag missing ">"', text[i:end+1]) s = text[i:end+1] return s def get_html_comment(text, i): # type: (str, int) -> str end = i + 7 + unclosed_end = 0 while end <= len(text): if text[end-3:end] == '-->': return text[i:end] + if not unclosed_end and text[end] == '<': + unclosed_end = end end += 1 - raise TemplateParserException('Unclosed comment') + raise TokenizationException('Unclosed comment', text[i:unclosed_end]) diff --git a/tools/tests/test_template_parser.py b/tools/tests/test_template_parser.py index 7ef3f5a83f..97ba657ed4 100644 --- a/tools/tests/test_template_parser.py +++ b/tools/tests/test_template_parser.py @@ -112,42 +112,51 @@ class ParserTest(unittest.TestCase): my_html = ''' {{# foo ''' - self._assert_validate_error('Tag missing }}', text=my_html) + self._assert_validate_error('''Tag missing "}}" at Line 2 Col 13:"{{# foo + "''', text=my_html) def test_validate_incomplete_handlebars_tag_2(self): # type: () -> None my_html = ''' {{# foo } ''' - self._assert_validate_error('Tag missing }}', text=my_html) + self._assert_validate_error('Tag missing "}}" at Line 2 Col 13:"{{# foo }\n"', text=my_html) def test_validate_incomplete_django_tag_1(self): # type: () -> None my_html = ''' {% foo ''' - self._assert_validate_error('Tag missing %}', text=my_html) + self._assert_validate_error('''Tag missing "%}" at Line 2 Col 13:"{% foo + "''', text=my_html) def test_validate_incomplete_django_tag_2(self): # type: () -> None my_html = ''' {% foo % ''' - self._assert_validate_error('Tag missing %}', text=my_html) + self._assert_validate_error('Tag missing "%}" at Line 2 Col 13:"{% foo %\n"', text=my_html) def test_validate_incomplete_html_tag_1(self): # type: () -> None my_html = ''' ', text=my_html) + self._assert_validate_error('''Tag missing ">" at Line 2 Col 13:" None my_html = ''' " at Line 2 Col 13:" None