From fdd63546b2a162639462b26c866145a7bf09196a Mon Sep 17 00:00:00 2001 From: Steve Howell Date: Sat, 20 Nov 2021 12:25:41 +0000 Subject: [PATCH] linters: Rewrite check-templates. I rewrote most of tools/lib/pretty-printer.py, which was fairly easy due to being able to crib some important details from the previous implementation. The main motivation for the rewrite was that we weren't handling else/elif blocks correctly, and it was difficult to modify the previous code. The else/elif shortcomings were somewhat historical in nature--the original parser didn't recognize them (since they weren't in any Zulip templates at the time), and then the pretty printer was mostly able to hack around that due to the "nudge" strategy. Eventually the nudge strategy became too brittle. The "nudge" strategy was that we would mostly trust the existing templates, and we would just nudge over some lines in cases of obviously faulty indentation. Now we are bit more opinionated and rigorous, and we basically set the indentation explicitly for any line that is not in a code/script block. This leads to this diff touching several templates for mostly minor fix-ups. We aren't completely opinionated, as we respect the author's line wrapping decisions in many cases, and we also allow authors not to indent blocks within the template language's block constructs. --- static/templates/message_body.hbs | 8 +- static/templates/message_edit_form.hbs | 6 +- static/templates/navbar.hbs | 4 +- .../stream_settings/stream_settings.hbs | 7 +- static/templates/typeahead_list_item.hbs | 4 +- static/templates/user_profile_modal.hbs | 4 +- templates/404.html | 6 +- templates/corporate/jobs.html | 9 +- templates/corporate/upgrade.html | 2 +- templates/corporate/zephyr.html | 18 +- templates/zerver/app/index.html | 9 +- templates/zerver/config_error.html | 6 +- templates/zerver/development/dev_tools.html | 12 +- templates/zerver/for-companies.html | 16 +- templates/zerver/for-education.html | 19 +- templates/zerver/for-events.html | 32 +- templates/zerver/for-open-source.html | 12 +- templates/zerver/for-research.html | 24 +- templates/zerver/hello.html | 4 +- templates/zerver/history.html | 3 +- templates/zerver/lean-case-study.html | 5 +- templates/zerver/login.html | 12 +- templates/zerver/portico.html | 4 +- templates/zerver/unsubscribe_link_error.html | 6 +- tools/lib/pretty_print.py | 393 ++++++++---------- tools/lib/template_parser.py | 48 ++- tools/tests/test_pretty_print.py | 22 +- 27 files changed, 389 insertions(+), 306 deletions(-) diff --git a/static/templates/message_body.hbs b/static/templates/message_body.hbs index b15ed9f30f..dbba9f2036 100644 --- a/static/templates/message_body.hbs +++ b/static/templates/message_body.hbs @@ -36,7 +36,13 @@ {{#unless status_message}} {{#unless is_hidden}} -
{{#if use_match_properties}}{{rendered_markdown msg/match_content}}{{else}}{{rendered_markdown msg/content}}{{/if}}
+
+ {{#if use_match_properties}} + {{rendered_markdown msg/match_content}} + {{else}} + {{rendered_markdown msg/content}} + {{/if}} +
{{else}} {{> message_hidden_dialog}} {{/unless}} diff --git a/static/templates/message_edit_form.hbs b/static/templates/message_edit_form.hbs index 411b2d8a98..bb7c2d2fb5 100644 --- a/static/templates/message_edit_form.hbs +++ b/static/templates/message_edit_form.hbs @@ -72,8 +72,10 @@ {{#if has_been_editable}}
- + +
{{/if}} diff --git a/static/templates/navbar.hbs b/static/templates/navbar.hbs index 1a7eb12d0c..e9e2bdff85 100644 --- a/static/templates/navbar.hbs +++ b/static/templates/navbar.hbs @@ -23,7 +23,9 @@
- + diff --git a/static/templates/stream_settings/stream_settings.hbs b/static/templates/stream_settings/stream_settings.hbs index 2735e6a169..c06ea3f2ba 100644 --- a/static/templates/stream_settings/stream_settings.hbs +++ b/static/templates/stream_settings/stream_settings.hbs @@ -4,7 +4,12 @@
+ {{#if subscribed }} + {{#tr}}Unsubscribe{{/tr}} + {{else}} + {{#tr}}Subscribe{{/tr}} + {{/if}} +
{{#if is_realm_admin}} diff --git a/static/templates/typeahead_list_item.hbs b/static/templates/typeahead_list_item.hbs index 9d96be5122..fc7f55e39f 100644 --- a/static/templates/typeahead_list_item.hbs +++ b/static/templates/typeahead_list_item.hbs @@ -1,8 +1,8 @@ {{#if is_emoji}} {{#if has_image}} - + {{else}} - + {{/if}}    {{else}} diff --git a/static/templates/user_profile_modal.hbs b/static/templates/user_profile_modal.hbs index 4f6bb25a28..7e3c663e55 100644 --- a/static/templates/user_profile_modal.hbs +++ b/static/templates/user_profile_modal.hbs @@ -48,9 +48,9 @@
- {{else if this.is_link}} + {{else if this.is_link}} {{this.value}} - {{else if this.is_external_account}} + {{else if this.is_external_account}} {{this.value}} {{else}} {{#if this.rendered_value}} diff --git a/templates/404.html b/templates/404.html index b2b022ffb1..64f01bb855 100644 --- a/templates/404.html +++ b/templates/404.html @@ -14,8 +14,10 @@ {% else %}

Page not found (404)

{% endif %} -

If this error is unexpected, you can contact - support.

+

+ If this error is unexpected, you can + contact support. +

diff --git a/templates/corporate/jobs.html b/templates/corporate/jobs.html index 2324e5ec88..9f3534eee4 100644 --- a/templates/corporate/jobs.html +++ b/templates/corporate/jobs.html @@ -157,11 +157,14 @@ suffice.
  • Share your expertise (both newly-acquired and longstanding) with the - rest of the team, through short- and long-form written communication.
  • + rest of the team, through short- and long-form written communication. +
  • Write primarily JavaScript using React Native, with some Java, Swift, - and backend Python, and learn whichever of those are new to you.
  • + and backend Python, and learn whichever of those are new to you. +
  • Work from our office in San Francisco, or from anywhere in the United - States.
  • + States. +

    Extra credit for any of the following:

    diff --git a/templates/corporate/upgrade.html b/templates/corporate/upgrade.html index 4291e394c1..3910afd4d4 100644 --- a/templates/corporate/upgrade.html +++ b/templates/corporate/upgrade.html @@ -200,7 +200,7 @@

    Number of licenses (minimum {{ min_invoiced_licenses }})


    + id="invoiced_licenses" name="licenses" required/>
    diff --git a/templates/corporate/zephyr.html b/templates/corporate/zephyr.html index 46d4089434..aa9d2b5415 100644 --- a/templates/corporate/zephyr.html +++ b/templates/corporate/zephyr.html @@ -35,13 +35,21 @@

    If you want to automatically transfer your existing Zephyr subscriptions

      -
    1. Get your Zulip API key from the Zulip "Settings" panel and put it in a file in your - Athena home directory called ~/Private/.zulip-api-key.

    2. +
    3. +

      + Get your Zulip API key from the Zulip "Settings" panel and put it in a file in your + Athena home directory called ~/Private/.zulip-api-key. +

      +
    4. -
    5. Run the following command to copy over all of your subscriptions:
      - /mit/tabbott/zulip/zephyr_mirror.py --sync-subscriptions

      +
    6. +

      + Run the following command to copy over all of your subscriptions:
      + /mit/tabbott/zulip/zephyr_mirror.py --sync-subscriptions +

      -

      NOTE: Zulip supports several ways to control what messages you want to read +

      + NOTE: Zulip supports several ways to control what messages you want to read right now, but Zulip does not yet have a direct equivalent to BarnOwl filters. If you have more subscriptions than you generally read, we recommend that you use Zulip's "Mute" option to hide those subscriptions from your diff --git a/templates/zerver/app/index.html b/templates/zerver/app/index.html index c452bceb6a..5273174f0b 100644 --- a/templates/zerver/app/index.html +++ b/templates/zerver/app/index.html @@ -98,8 +98,8 @@

      - {% trans %}Unable to connect to - Zulip. Updates may be delayed.{% endtrans %} {{ _('Retrying soon...') }} {{ _('Try now.') }} + {% trans %}Unable to connect to Zulip. + Updates may be delayed.{% endtrans %} {{ _('Retrying soon...') }} {{ _('Try now.') }}
      @@ -115,8 +115,9 @@ Zephyr mirror script yourself in a screen session. - To fix - this, you'll need to use the web interface. + + To fix this, you'll need to use the web interface. +
      diff --git a/templates/zerver/config_error.html b/templates/zerver/config_error.html index 6136d00f66..3dac1bacce 100644 --- a/templates/zerver/config_error.html +++ b/templates/zerver/config_error.html @@ -77,9 +77,9 @@

      {% if development_environment %}

      - See also - the SAML - guide for the development environment. + See also the + SAML guide + for the development environment.

      {% endif %} {% endif %} diff --git a/templates/zerver/development/dev_tools.html b/templates/zerver/development/dev_tools.html index fc7ca766ba..8d5fbeb6df 100644 --- a/templates/zerver/development/dev_tools.html +++ b/templates/zerver/development/dev_tools.html @@ -98,10 +98,14 @@

      Connecting to the local PostgreSQL database

        -
      • ./manage.py dbshell: Connect to - PostgreSQL database via your terminal.
      • -
      • provision creates a ~/.pgpass file, - so psql -U zulip -h localhost works too.
      • +
      • + ./manage.py dbshell: Connect to + PostgreSQL database via your terminal. +
      • +
      • + provision creates a ~/.pgpass file, + so psql -U zulip -h localhost works too. +
      • To connect using a graphical PostgreSQL client diff --git a/templates/zerver/for-companies.html b/templates/zerver/for-companies.html index 99353372b7..22b35a8cbb 100644 --- a/templates/zerver/for-companies.html +++ b/templates/zerver/for-companies.html @@ -292,10 +292,18 @@ Powerful formatting

          -
        • Zulip - code blocks come with syntax highlighting for over 250 languages, and integrated code playgrounds.
        • -
        • Type LaTeX directly into your Zulip message, and see it beautifully rendered.
        • -
        • Enjoy inline image, video and Tweet previews.
        • +
        • +
          + Zulip code blocks + come with syntax highlighting for over 250 languages, and integrated code playgrounds. +
          +
        • +
        • +
          Type LaTeX directly into your Zulip message, and see it beautifully rendered.
          +
        • +
        • +
          Enjoy inline image, video and Tweet previews.
          +
        • If you made a mistake, no worries! You diff --git a/templates/zerver/for-education.html b/templates/zerver/for-education.html index c3b35de000..a5c59b23cd 100644 --- a/templates/zerver/for-education.html +++ b/templates/zerver/for-education.html @@ -218,12 +218,21 @@ With apps for every platform, you can check Zulip at your computer or on your phone.
        • -
        • Zulip alerts you - about timely messages with fully customizable mobile, email and desktop notifications.
        • -
        • Mention users, groups of users or everyone when you need their attention.
          +
        • +
          + Zulip alerts you about timely messages with + fully customizable mobile, email and desktop notifications. +
          +
        • +
        • +
          Mention users, groups of users or everyone when you need their attention.
          +
        • +
        • +
          Use Zulip in your language of choice, with translations into 17 languages.
          +
        • +
        • +
          Zulip works reliably for organizations with thousands of users online at once.
        • -
        • Use Zulip in your language of choice, with translations into 17 languages.
        • -
        • Zulip works reliably for organizations with thousands of users online at once.
      diff --git a/templates/zerver/for-events.html b/templates/zerver/for-events.html index baff95a463..05c56f9f4e 100644 --- a/templates/zerver/for-events.html +++ b/templates/zerver/for-events.html @@ -113,12 +113,24 @@ Your communication hub
      @@ -239,8 +251,12 @@
    7. Zulip alerts participants about timely messages with fully customizable mobile, email and desktop notifications.
    8. Mention users, groups of users or everyone when you need their attention.
    9. -
    10. Zulip works reliably for organizations - with thousands of users online at once.
    11. +
    12. +
      + Zulip works reliably for organizations + with thousands of users online at once. +
      +
    13. diff --git a/templates/zerver/for-open-source.html b/templates/zerver/for-open-source.html index 5432267684..8b40ccd319 100644 --- a/templates/zerver/for-open-source.html +++ b/templates/zerver/for-open-source.html @@ -243,17 +243,17 @@ Zulip is 100% open-source software, with no "open core" catch. We work hard to make it easy to set up, - backup - , and maintain - a self-hosted Zulip installation, where you + backup, + and maintain + a self-hosted Zulip installation, where you have full control of your data.
    14. - Our high quality export - and import - tools ensure that you can always move from + Our high quality export + and import + tools ensure that you can always move from Zulip Cloud hosting to your own servers. There is no lock-in.
      diff --git a/templates/zerver/for-research.html b/templates/zerver/for-research.html index 7d13b3b1c5..699c275a5f 100644 --- a/templates/zerver/for-research.html +++ b/templates/zerver/for-research.html @@ -61,8 +61,12 @@ discussion.
    15. -
    16. Find active conversations, or see what - happened while you were away, with the Recent Topics view.
    17. +
    18. +
      + Find active conversations, or see what happened while you were away, + with the Recent Topics view. +
      +
    19. Keep discussions orderly @@ -154,9 +158,19 @@ Powerful formatting
      diff --git a/templates/zerver/history.html b/templates/zerver/history.html index 97154a3134..edb03ddb5b 100644 --- a/templates/zerver/history.html +++ b/templates/zerver/history.html @@ -117,8 +117,7 @@ As of October 2018, the Zulip server project had merged 6500 pull requests written by over - - 400 developers. + 400 developers.
    20. diff --git a/templates/zerver/lean-case-study.html b/templates/zerver/lean-case-study.html index 41c405a4c7..248bf7fb03 100644 --- a/templates/zerver/lean-case-study.html +++ b/templates/zerver/lean-case-study.html @@ -20,9 +20,8 @@

      Case study:
      Lean theorem prover community

      - Learn more about using Zulip for research
      and open source communities. + Learn more about using Zulip for research
      + and open source communities.
      diff --git a/templates/zerver/login.html b/templates/zerver/login.html index 4a3840fab0..86de0c4a10 100644 --- a/templates/zerver/login.html +++ b/templates/zerver/login.html @@ -48,11 +48,15 @@ page can be easily identified in it's respective JavaScript file. --> {% endif %} {% if no_auth_enabled %}
      -

      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. +

      {% else %} {% if password_auth_enabled %} diff --git a/templates/zerver/portico.html b/templates/zerver/portico.html index 1ddcc8687e..0145459cee 100644 --- a/templates/zerver/portico.html +++ b/templates/zerver/portico.html @@ -3,8 +3,8 @@ {# A base template for stuff like login, register, etc. - Not inside the app itself, but covered by the same structure, - hence the name. +Not inside the app itself, but covered by the same structure, +hence the name. #} {% block content %} diff --git a/templates/zerver/unsubscribe_link_error.html b/templates/zerver/unsubscribe_link_error.html index 5978e41bca..8b2cf02232 100644 --- a/templates/zerver/unsubscribe_link_error.html +++ b/templates/zerver/unsubscribe_link_error.html @@ -4,8 +4,10 @@

      {% 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}}
      {{{test}}}
      - {{/if}} - {{#if foobar2}} - {{> teststuff}} - {{/if}} + {{/if}} + {{#if foobar2}} + {{> teststuff}} + {{/if}} {{/with}} {{/each}} """ @@ -325,9 +325,9 @@ GOOD_HTML13 = """
       :{{this.name}}:
      {{else}} {{#if this.is_realm_emoji}} - + {{else}} -
      +
      {{/if}} {{/if}}
      {{this.count}}
      @@ -354,12 +354,12 @@ GOOD_HTML14 = """ {{#if this.code}}
      Here goes some cool code.
      {{else}} -
      - content of first div
      - content of second div. + content of first div +
      + content of second div. +
      -
      {{/if}}
      """