No authentication backends are enabled on this - server yet, so it is impossible to log in!
++ No authentication backends are enabled on this + server yet, so it is impossible to log in! +
-See the Zulip - authentication documentation to learn how to configure authentication backends.
++ See the + Zulip authentication documentation to learn how to configure authentication backends. +
{% trans %}Unknown email unsubscribe request{% endtrans %}
-{% trans %}Hi there! It looks like you tried to unsubscribe from something, but we don't -recognize the URL.{% endtrans %}
++ {% trans %}Hi there! It looks like you tried to unsubscribe from something, + but we don't recognize the URL.{% endtrans %} +
{% trans %}Please double-check that you have the full URL and try again, or email us and we'll get this squared away!{% endtrans %}
diff --git a/tools/lib/pretty_print.py b/tools/lib/pretty_print.py index d1495acf81..ec4fb7c77c 100644 --- a/tools/lib/pretty_print.py +++ b/tools/lib/pretty_print.py @@ -1,223 +1,198 @@ import subprocess -from typing import Any, Dict, List +from typing import List, Optional, Set from zulint.printer import ENDC, GREEN -from .template_parser import is_django_block_tag, tokenize +from .template_parser import Token, is_django_block_tag, tokenize -def pretty_print_html(html: str, num_spaces: int = 4) -> str: - # We use 1-based indexing for both rows and columns. +def requires_indent(line: str) -> bool: + line = line.lstrip() + return line.startswith("<") + + +def open_token(token: Token) -> bool: + if token.kind in ( + "handlebars_start", + "html_start", + ): + return True + + if token.kind in ( + "django_start", + "jinja2_whitespace_stripped_start", + "jinja2_whitespace_stripped_type2_start", + ): + return is_django_block_tag(token.tag) + + return False + + +def close_token(token: Token) -> bool: + return token.kind in ( + "django_end", + "handlebars_end", + "html_end", + "jinja2_whitespace_stripped_end", + ) + + +def else_token(token: Token) -> bool: + return token.kind in ( + "django_else", + "handlebars_else", + ) + + +def pop_unused_tokens(tokens: List[Token], row: int) -> bool: + while tokens and tokens[-1].line <= row: + token = tokens.pop() + if close_token(token): + return True + return False + + +def indent_pref(row: int, tokens: List[Token], line: str) -> str: + opens = 0 + closes = 0 + is_else = False + + while tokens and tokens[-1].line == row: + token = tokens.pop() + if open_token(token): + opens += 1 + elif close_token(token): + closes += 1 + elif else_token(token): + is_else = True + + if is_else: + if opens and closes: + return "neutral" + return "else" + + i = opens - closes + if i == 0: + return "neutral" + elif i == 1: + return "open" + elif i == -1: + return "close" + else: + print(i, opens, closes) + raise Exception(f"too many tokens on row {row}") + + +def indent_level(s: str) -> int: + return len(s) - len(s.lstrip()) + + +def same_indent(s1: str, s2: str) -> bool: + return indent_level(s1) == indent_level(s2) + + +def next_non_blank_line(lines: List[str], i: int) -> str: + next_line = "" + for j in range(i + 1, len(lines)): + next_line = lines[j] + if next_line.strip() != "": + break + return next_line + + +def get_exempted_lines(tokens: List[Token]) -> Set[int]: + exempted = set() + for code_tag in ("code", "pre", "script"): + for token in tokens: + if token.kind == "html_start" and token.tag == code_tag: + start: Optional[int] = token.line + + if token.kind == "html_end" and token.tag == code_tag: + # The pretty printer expects well-formed HTML, even + # if it's strangely formatted, so we expect start + # to be None. + assert start is not None + + # We leave code blocks completely alone, including + # the start and end tags. + for i in range(start, token.line + 1): + exempted.add(i) + start = None + return exempted + + +def pretty_print_html(html: str) -> str: tokens = tokenize(html) + + exempted_lines = get_exempted_lines(tokens) + + tokens.reverse() lines = html.split("\n") - # We will keep a stack of "start" tags so that we know - # when HTML ranges end. Note that some start tags won't - # be blocks from an indentation standpoint. - stack: List[Dict[str, Any]] = [] - - # Seed our stack with a pseudo entry to make depth calculations - # easier. - info: Dict[str, Any] = dict( - block=False, - depth=-1, - line=-1, - token_kind="html_start", - tag="html", - extra_indent=0, - ignore_lines=[], - ) - stack.append(info) - - # Our main job is to figure out offsets that we use to nudge lines - # over by. - offsets: Dict[int, int] = {} - - # Loop through our start/end tokens, and calculate offsets. As - # we proceed, we will push/pop info dictionaries on/off a stack. - for token in tokens: - - if ( - token.kind - in ( - "html_start", - "handlebars_start", - "handlebars_singleton", - "html_singleton", - "django_start", - "jinja2_whitespace_stripped_type2_start", - "jinja2_whitespace_stripped_start", - ) - and stack[-1]["tag"] != "pre" - ): - # An HTML start tag should only cause a new indent if we - # are on a new line. - if token.tag not in ("extends", "include", "else", "elif") and ( - is_django_block_tag(token.tag) or token.kind != "django_start" - ): - is_block = token.line > stack[-1]["line"] - - if is_block: - if ( - ( - token.kind == "handlebars_start" - and stack[-1]["token_kind"] == "handlebars_start" - ) - or ( - token.kind - in { - "django_start", - "jinja2_whitespace_stripped_type2_start", - "jinja2_whitespace_stripped_start", - } - and stack[-1]["token_kind"] - in { - "django_start", - "jinja2_whitespace_stripped_type2_start", - "jinja2_whitespace_stripped_start", - } - ) - ) and not stack[-1]["indenting"]: - info = stack.pop() - info["depth"] = info["depth"] + 1 - info["indenting"] = True - info["adjust_offset_until"] = token.line - stack.append(info) - new_depth = stack[-1]["depth"] + 1 - extra_indent = stack[-1]["extra_indent"] - line = lines[token.line - 1] - adjustment = len(line) - len(line.lstrip()) + 1 - offset = (1 + extra_indent + new_depth * num_spaces) - adjustment - info = dict( - block=True, - depth=new_depth, - actual_depth=new_depth, - line=token.line, - tag=token.tag, - token_kind=token.kind, - line_span=token.line_span, - offset=offset, - extra_indent=token.col - adjustment + extra_indent, - extra_indent_prev=extra_indent, - adjustment=adjustment, - indenting=True, - adjust_offset_until=token.line, - ignore_lines=[], - ) - if token.kind in ("handlebars_start", "django_start"): - info.update(dict(depth=new_depth - 1, indenting=False)) - else: - info = dict( - block=False, - depth=stack[-1]["depth"], - actual_depth=stack[-1]["depth"], - line=token.line, - tag=token.tag, - token_kind=token.kind, - extra_indent=stack[-1]["extra_indent"], - ignore_lines=[], - ) - stack.append(info) - elif ( - token.kind - in ( - "html_end", - "handlebars_end", - "html_singleton_end", - "django_end", - "handlebars_singleton_end", - "jinja2_whitespace_stripped_end", - ) - and (stack[-1]["tag"] != "pre" or token.tag == "pre") - ): - info = stack.pop() - if info["block"]: - # We are at the end of an indentation block. We - # assume the whole block was formatted ok before, just - # possibly at an indentation that we don't like, so we - # nudge over all lines in the block by the same offset. - start_line = info["line"] - end_line = token.line - if token.tag == "pre": - offsets[start_line] = 0 - offsets[end_line] = 0 - stack[-1]["ignore_lines"].append(start_line) - stack[-1]["ignore_lines"].append(end_line) - else: - offsets[start_line] = info["offset"] - line = lines[token.line - 1] - adjustment = len(line) - len(line.lstrip()) + 1 - if adjustment == token.col and token.kind != "html_singleton_end": - offsets[end_line] = ( - info["offset"] - + info["adjustment"] - - adjustment - + info["extra_indent"] - - info["extra_indent_prev"] - ) - elif start_line + info["line_span"] - 1 == end_line and info["line_span"] > 1: - offsets[end_line] = ( - 1 + info["extra_indent"] + (info["depth"] + 1) * num_spaces - ) - adjustment - # We would like singleton tags and tags which spread over - # multiple lines to have 2 space indentation. - offsets[end_line] -= 2 - elif token.line != info["line"]: - offsets[end_line] = info["offset"] - if token.tag != "pre" and token.tag != "script": - for line_num in range(start_line + 1, end_line): - # Be careful not to override offsets that happened - # deeper in the HTML within our block. - if line_num not in offsets: - line = lines[line_num - 1] - new_depth = info["depth"] + 1 - if ( - line.lstrip().startswith("{{else}}") - or line.lstrip().startswith("{% else %}") - or line.lstrip().startswith("{% elif") - ): - new_depth = info["actual_depth"] - extra_indent = info["extra_indent"] - adjustment = len(line) - len(line.lstrip()) + 1 - offset = (1 + extra_indent + new_depth * num_spaces) - adjustment - if line_num <= start_line + info["line_span"] - 1: - # We would like singleton tags and tags which spread over - # multiple lines to have 2 space indentation. - offset -= 2 - offsets[line_num] = offset - elif ( - token.kind in ("handlebars_end", "django_end") - and info["indenting"] - and line_num < info["adjust_offset_until"] - and line_num not in info["ignore_lines"] - ): - offsets[line_num] += num_spaces - elif token.tag != "pre": - for line_num in range(start_line + 1, end_line): - if line_num not in offsets: - offsets[line_num] = info["offset"] - else: - for line_num in range(start_line + 1, end_line): - if line_num not in offsets: - offsets[line_num] = 0 - stack[-1]["ignore_lines"].append(line_num) - - # Now that we have all of our offsets calculated, we can just - # join all our lines together, fixing up offsets as needed. + open_offsets: List[str] = [] formatted_lines = [] - for i, line in enumerate(html.split("\n")): - row = i + 1 - offset = offsets.get(row, 0) - pretty_line = line + next_offset: str = "" + tag_end_row: Optional[int] = None + tag_continuation_offset = "" + + def line_offset(row: int, line: str, next_line: str) -> Optional[str]: + nonlocal next_offset + nonlocal tag_end_row + nonlocal tag_continuation_offset + + if tag_end_row and row < tag_end_row: + was_closed = pop_unused_tokens(tokens, row) + if was_closed: + next_offset = open_offsets.pop() + return tag_continuation_offset + + offset = next_offset + if tokens: + token = tokens[-1] + if token.line == row and token.line_span > 1: + if token.kind in ("django_comment", "handlebar_comment", "html_comment"): + tag_continuation_offset = offset + else: + tag_continuation_offset = offset + " " + tag_end_row = row + token.line_span + + pref = indent_pref(row, tokens, line) + if pref == "open": + if same_indent(line, next_line) and not requires_indent(line): + next_offset = offset + else: + next_offset = offset + " " * 4 + open_offsets.append(offset) + elif pref == "else": + offset = open_offsets[-1] + if same_indent(line, next_line): + next_offset = offset + else: + next_offset = offset + " " * 4 + elif pref == "close": + offset = open_offsets.pop() + next_offset = offset + return offset + + def adjusted_line(row: int, line: str, next_line: str) -> str: if line.strip() == "": - pretty_line = "" - else: - if offset > 0: - pretty_line = (" " * offset) + pretty_line - elif offset < 0: - pretty_line = pretty_line[-1 * offset :] - assert line.strip() == pretty_line.strip() - formatted_lines.append(pretty_line) + return "" + + offset = line_offset(row, line, next_line) + + if row in exempted_lines: + return line.rstrip() + + if offset is None: + return line.rstrip() + + return offset + line.strip() + + for i, line in enumerate(lines): + # We use 1-based indexing for both rows and columns. + next_line = next_non_blank_line(lines, i) + row = i + 1 + formatted_lines.append(adjusted_line(row, line, next_line)) return "\n".join(formatted_lines) diff --git a/tools/lib/template_parser.py b/tools/lib/template_parser.py index b433a99df8..c829f3ceb4 100644 --- a/tools/lib/template_parser.py +++ b/tools/lib/template_parser.py @@ -70,11 +70,17 @@ def tokenize(text: str) -> List[Token]: def looking_at_handlebars_start() -> bool: return looking_at("{{#") or looking_at("{{^") + def looking_at_handlebars_else() -> bool: + return looking_at("{{else") + def looking_at_handlebars_end() -> bool: return looking_at("{{/") def looking_at_django_start() -> bool: - return looking_at("{% ") and not looking_at("{% end") + return looking_at("{% ") + + def looking_at_django_else() -> bool: + return looking_at("{% else") or looking_at("{% elif") def looking_at_django_end() -> bool: return looking_at("{% end") @@ -130,6 +136,10 @@ def tokenize(text: str) -> List[Token]: s = get_html_tag(text, state.i) tag = s[2:-1] kind = "html_end" + elif looking_at_handlebars_else(): + s = get_handlebars_tag(text, state.i) + tag = "else" + kind = "handlebars_else" elif looking_at_handlebars_start(): s = get_handlebars_tag(text, state.i) tag = s[3:-2].split()[0] @@ -140,17 +150,22 @@ def tokenize(text: str) -> List[Token]: s = get_handlebars_tag(text, state.i) tag = s[3:-2] kind = "handlebars_end" + elif looking_at_django_else(): + s = get_django_tag(text, state.i) + tag = "else" + kind = "django_else" + elif looking_at_django_end(): + s = get_django_tag(text, state.i) + tag = s[6:-3] + kind = "django_end" elif looking_at_django_start(): + # must check this after end/else s = get_django_tag(text, state.i) tag = s[3:-2].split()[0] kind = "django_start" if s[-3] == "-": kind = "jinja2_whitespace_stripped_start" - elif looking_at_django_end(): - s = get_django_tag(text, state.i) - tag = s[6:-3] - kind = "django_end" elif looking_at_jinja2_end_whitespace_stripped(): s = get_django_tag(text, state.i) tag = s[7:-3] @@ -284,6 +299,7 @@ def validate( state.foreign = True def f(end_token: Token) -> None: + is_else_tag = end_token.tag == "else" end_tag = end_token.tag.strip("~") end_line = end_token.line @@ -297,9 +313,12 @@ def validate( problem = None if (start_tag == "code") and (end_line == start_line + 1): problem = "Code tag is split across two lines." - if start_tag != end_tag: + if is_else_tag: + pass + elif start_tag != end_tag: problem = "Mismatched tag." - elif check_indent and (end_line > start_line + max_lines): + + if not problem and check_indent and (end_line > start_line + max_lines): if end_col != start_col: problem = "Bad indentation." @@ -315,7 +334,7 @@ def validate( raise TemplateParserException( f""" fn: {fn} - {problem} + {problem} start: {start_token.s} line {start_line}, col {start_col} @@ -324,9 +343,11 @@ def validate( line {end_line}, col {end_col} """ ) - state.matcher = old_matcher - state.foreign = old_foreign - state.depth -= 1 + + if not is_else_tag: + state.matcher = old_matcher + state.foreign = old_foreign + state.depth -= 1 state.matcher = f @@ -350,17 +371,20 @@ def validate( elif kind == "handlebars_start": start_tag_matcher(token) + elif kind == "handlebars_else": + state.matcher(token) elif kind == "handlebars_end": state.matcher(token) elif kind in { "django_start", + "django_else", "jinja2_whitespace_stripped_start", "jinja2_whitespace_stripped_type2_start", }: if is_django_block_tag(tag): start_tag_matcher(token) - elif kind in {"django_end", "jinja2_whitespace_stripped_end"}: + elif kind in {"django_else", "django_end", "jinja2_whitespace_stripped_end"}: state.matcher(token) if state.depth != 0: diff --git a/tools/tests/test_pretty_print.py b/tools/tests/test_pretty_print.py index b1a4e13b54..419ccaa36e 100644 --- a/tools/tests/test_pretty_print.py +++ b/tools/tests/test_pretty_print.py @@ -192,12 +192,12 @@ BAD_HTML8 = """ GOOD_HTML8 = """ {{#each test}} {{#with this}} - {{#if foobar}} + {{#if foobar}}+
{{/if}} {{/if}}
Here goes some cool code.{{else}} -