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.
This commit is contained in:
Steve Howell 2021-11-20 12:25:41 +00:00 committed by Tim Abbott
parent 4792af5682
commit fdd63546b2
27 changed files with 389 additions and 306 deletions

View File

@ -36,7 +36,13 @@
{{#unless status_message}}
{{#unless is_hidden}}
<div class="message_content rendered_markdown">{{#if use_match_properties}}{{rendered_markdown msg/match_content}}{{else}}{{rendered_markdown msg/content}}{{/if}}</div>
<div class="message_content rendered_markdown">
{{#if use_match_properties}}
{{rendered_markdown msg/match_content}}
{{else}}
{{rendered_markdown msg/content}}
{{/if}}
</div>
{{else}}
{{> message_hidden_dialog}}
{{/unless}}

View File

@ -72,8 +72,10 @@
{{#if has_been_editable}}
<div class="message-edit-timer-control-group">
<span class="message_edit_countdown_timer"></span>
<span><i id="message_edit_tooltip" class="tippy-zulip-tooltip message_edit_tooltip fa fa-question-circle" aria-hidden="true"
{{#if is_widget_message}} data-tippy-content="{{#tr}}Widgets cannot be edited.{{/tr}}" {{else}} data-tippy-content="{{#tr}}This organization is configured to restrict editing of message content to {minutes_to_edit} minutes after it is sent.{{/tr}}" {{/if}}></i>
<span>
<i id="message_edit_tooltip" class="tippy-zulip-tooltip message_edit_tooltip fa fa-question-circle" aria-hidden="true"
{{#if is_widget_message}} data-tippy-content="{{#tr}}Widgets cannot be edited.{{/tr}}" {{else}} data-tippy-content="{{#tr}}This organization is configured to restrict editing of message content to {minutes_to_edit} minutes after it is sent.{{/tr}}" {{/if}}>
</i>
</span>
</div>
{{/if}}

View File

@ -23,7 +23,9 @@
<div contenteditable="true" class="input search-query input-block-level" id="search_query" type="text" placeholder="{{t 'Search' }}"
autocomplete="off" aria-label="{{t 'Search' }}" title="{{t 'Search' }} (/)">
</div>
<button class="btn search_button" type="button" id="search_exit" aria-label="{{t 'Exit search' }}"><i class="fa fa-remove" aria-hidden="true"></i></button>
<button class="btn search_button" type="button" id="search_exit" aria-label="{{t 'Exit search' }}">
<i class="fa fa-remove" aria-hidden="true"></i>
</button>
</div>
</form>
</div>

View File

@ -4,7 +4,12 @@
<div class="button-group">
<div class="sub_unsub_button_wrapper inline-block">
<button class="button small rounded subscribe-button sub_unsub_button {{#unless subscribed }}unsubscribed{{/unless}}" type="button" name="button" {{#if should_display_subscription_button}}title="{{t 'Toggle subscription'}} (S)" {{else}}disabled="disabled"{{/if}}>
{{#if subscribed }}{{#tr}}Unsubscribe{{/tr}}{{else}}{{#tr}}Subscribe{{/tr}}{{/if}}</button>
{{#if subscribed }}
{{#tr}}Unsubscribe{{/tr}}
{{else}}
{{#tr}}Subscribe{{/tr}}
{{/if}}
</button>
</div>
<a href="{{preview_url}}" class="button small rounded" id="preview-stream-button" role="button" title="{{t 'View stream'}} (V)" {{#unless should_display_preview_button }}style="display: none"{{/unless}}><i class="fa fa-eye"></i></a>
{{#if is_realm_admin}}

View File

@ -1,8 +1,8 @@
{{#if is_emoji}}
{{#if has_image}}
<img class="emoji" src="{{ img_src }}" />
<img class="emoji" src="{{ img_src }}" />
{{else}}
<span class='emoji emoji-{{ emoji_code }}'></span>
<span class='emoji emoji-{{ emoji_code }}'></span>
{{/if}}
&nbsp;&nbsp;
{{else}}

View File

@ -48,9 +48,9 @@
<div class="pill-container not-editable" data-field-id="{{this.id}}">
<div class="input" contenteditable="false" style="display: none;"></div>
</div>
{{else if this.is_link}}
{{else if this.is_link}}
<a href="{{this.value}}" target="_blank" rel="noopener noreferrer" class="value">{{this.value}}</a>
{{else if this.is_external_account}}
{{else if this.is_external_account}}
<a href="{{this.link}}" target="_blank" rel="noopener noreferrer" class="value">{{this.value}}</a>
{{else}}
{{#if this.rendered_value}}

View File

@ -14,8 +14,10 @@
{% else %}
<h1 class="lead">Page not found (404)</h1>
{% endif %}
<p>If this error is unexpected, you can <a href="mailto:{{ support_email }}">contact
support</a>.</p>
<p>
If this error is unexpected, you can
<a href="mailto:{{ support_email }}">contact support</a>.
</p>
</div>
</div>
</div>

View File

@ -157,11 +157,14 @@
suffice.
</li>
<li>Share your expertise (both newly-acquired and longstanding) with the
rest of the team, through short- and long-form written communication.</li>
rest of the team, through short- and long-form written communication.
</li>
<li>Write primarily JavaScript using React Native, with some Java, Swift,
and backend Python, and learn whichever of those are new to you.</li>
and backend Python, and learn whichever of those are new to you.
</li>
<li>Work from our office in San Francisco, or from anywhere in the United
States.</li>
States.
</li>
</ul>
<h3>Extra credit for any of the following:</h3>

View File

@ -200,7 +200,7 @@
</p>
<h4>Number of licenses (minimum {{ min_invoiced_licenses }})</h4>
<input type="number" min="{{ min_invoiced_licenses }}" autocomplete="off"
id="invoiced_licenses" name="licenses" required/><br />
id="invoiced_licenses" name="licenses" required/><br />
<button type="submit" id="invoice-button" class="stripe-button-el invoice-button">Buy Standard</button>
</form>
</div>

View File

@ -35,13 +35,21 @@
<h2>If you want to automatically transfer your existing Zephyr subscriptions</h2>
<ol>
<li><p>Get your Zulip API key from the Zulip "Settings" panel and put it in a file in your
Athena home directory called <code>~/Private/.zulip-api-key</code>.</p></li>
<li>
<p>
Get your Zulip API key from the Zulip "Settings" panel and put it in a file in your
Athena home directory called <code>~/Private/.zulip-api-key</code>.
</p>
</li>
<li><p>Run the following command to copy over all of your subscriptions:<br />
<code>/mit/tabbott/zulip/zephyr_mirror.py --sync-subscriptions</code></p>
<li>
<p>
Run the following command to copy over all of your subscriptions:<br />
<code>/mit/tabbott/zulip/zephyr_mirror.py --sync-subscriptions</code>
</p>
<p> <strong>NOTE</strong>: Zulip supports several ways to control what messages you want to read
<p>
<strong>NOTE</strong>: 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

View File

@ -98,8 +98,8 @@
<div class="alert-box">
<div class="alert alert_sidebar alert-error home-error-bar" id="connection-error">
<div class="exit"></div>
{% trans %}<strong class="message">Unable to connect to
Zulip.</strong> Updates may be delayed.{% endtrans %} {{ _('Retrying soon...') }} <a class="restart_get_events_button">{{ _('Try now.') }}</a>
{% trans %}<strong class="message">Unable to connect to Zulip.</strong>
Updates may be delayed.{% endtrans %} {{ _('Retrying soon...') }} <a class="restart_get_events_button">{{ _('Try now.') }}</a>
</div>
<div class="alert alert_sidebar alert-error home-error-bar" id="zephyr-mirror-error">
<div class="exit"></div>
@ -115,8 +115,9 @@
Zephyr mirror script yourself</a> in a screen
session.
</span>
<span id="desktop-zephyr-mirror-error-text" class="notdisplayed">To fix
this, you'll need to use the web interface.</span>
<span id="desktop-zephyr-mirror-error-text" class="notdisplayed">
To fix this, you'll need to use the web interface.
</span>
</div>
<div class="alert alert_sidebar alert-error home-error-bar" id="home-error"></div>
<div class="alert alert_sidebar alert-error home-error-bar" id="reloading-application"></div>

View File

@ -77,9 +77,9 @@
</p>
{% if development_environment %}
<p>
See also
the <a href="https://zulip.readthedocs.io/en/latest/development/authentication.html#saml">SAML
guide</a> for the development environment.
See also the
<a href="https://zulip.readthedocs.io/en/latest/development/authentication.html#saml">SAML guide</a>
for the development environment.
</p>
{% endif %}
{% endif %}

View File

@ -98,10 +98,14 @@
</p>
<h2>Connecting to the local PostgreSQL database</h2>
<ul>
<li><code>./manage.py dbshell</code>: Connect to
PostgreSQL database via your terminal.</li>
<li><code>provision</code> creates a <code>~/.pgpass</code> file,
so <code>psql -U zulip -h localhost</code> works too.</li>
<li>
<code>./manage.py dbshell</code>: Connect to
PostgreSQL database via your terminal.
</li>
<li>
<code>provision</code> creates a <code>~/.pgpass</code> file,
so <code>psql -U zulip -h localhost</code> works too.
</li>
<li>
<p>
To connect using a graphical PostgreSQL client

View File

@ -292,10 +292,18 @@
Powerful formatting
</h1>
<ul>
<li><div class="list-content"><a href="/help/code-blocks">Zulip
code blocks</a> come with syntax highlighting for over 250 languages, and integrated <a href="/help/code-blocks#code-playgrounds">code playgrounds.</a></div></li>
<li><div class="list-content"><a href="/help/format-your-message-using-markdown#latex">Type LaTeX</a> directly into your Zulip message, and see it beautifully rendered.</div></li>
<li><div class="list-content">Enjoy inline image, video and Tweet previews.</div></li>
<li>
<div class="list-content">
<a href="/help/code-blocks">Zulip code blocks</a>
come with syntax highlighting for over 250 languages, and integrated <a href="/help/code-blocks#code-playgrounds">code playgrounds.</a>
</div>
</li>
<li>
<div class="list-content"><a href="/help/format-your-message-using-markdown#latex">Type LaTeX</a> directly into your Zulip message, and see it beautifully rendered.</div>
</li>
<li>
<div class="list-content">Enjoy inline image, video and Tweet previews.</div>
</li>
<li>
<div class="list-content">
If you made a mistake, no worries! You

View File

@ -218,12 +218,21 @@
With <a href="/apps">apps for every platform</a>, you can check Zulip at your computer or on your phone.
</div>
</li>
<li><div class="list-content">Zulip alerts you
about timely messages with <a href="/help/stream-notifications">fully customizable</a> mobile, email and desktop notifications.</div></li>
<li><div class="list-content">Mention <a href="/help/mention-a-user-or-group">users</a>, <a href="/help/mention-a-user-or-group#mention-a-user-or-group">groups of users</a> or <a href="/help/pm-mention-alert-notifications#wildcard-mentions">everyone</a> when you need their attention.</div>
<li>
<div class="list-content">
Zulip alerts you about timely messages with
<a href="/help/stream-notifications">fully customizable</a> mobile, email and desktop notifications.
</div>
</li>
<li>
<div class="list-content">Mention <a href="/help/mention-a-user-or-group">users</a>, <a href="/help/mention-a-user-or-group#mention-a-user-or-group">groups of users</a> or <a href="/help/pm-mention-alert-notifications#wildcard-mentions">everyone</a> when you need their attention.</div>
</li>
<li>
<div class="list-content">Use Zulip in your language of choice, with translations into <a href="https://www.transifex.com/zulip/zulip/">17 languages</a>.</div>
</li>
<li>
<div class="list-content">Zulip works reliably for organizations with thousands of users online at once.</div>
</li>
<li><div class="list-content">Use Zulip in your language of choice, with translations into <a href="https://www.transifex.com/zulip/zulip/">17 languages</a>.</div></li>
<li><div class="list-content">Zulip works reliably for organizations with thousands of users online at once.</div></li>
</ul>
</div>
</div>

View File

@ -113,12 +113,24 @@
Your communication hub
</h1>
<ul>
<li><div class="list-content">Share the agenda and presentations with
<a href="/help/share-and-upload-files">drag-and-drop file uploads</a>.</div></li>
<li><div class="list-content">Use <a href="/help/emoji-reactions">emoji reactions</a> for lightweight interactions. Have fun with <a href="/help/custom-emoji">custom emoji</a>, or get feedback with a <a href="/help/create-a-poll">poll</a>.</div></li>
<li><div class="list-content">Announce the schedule without worrying about time zones using <a href="/help/format-your-message-using-markdown#global-times">global times</a>.</div></li>
<li><div class="list-content">Make a <a href="/help/start-a-call">video call</a> with the click of a button.</div></li>
<li><div class="list-content">Use Zulip in your language of choice, with translations into <a href="https://www.transifex.com/zulip/zulip/">17 languages</a>.</div></li>
<li>
<div class="list-content">
Share the agenda and presentations with
<a href="/help/share-and-upload-files">drag-and-drop file uploads</a>.
</div>
</li>
<li>
<div class="list-content">Use <a href="/help/emoji-reactions">emoji reactions</a> for lightweight interactions. Have fun with <a href="/help/custom-emoji">custom emoji</a>, or get feedback with a <a href="/help/create-a-poll">poll</a>.</div>
</li>
<li>
<div class="list-content">Announce the schedule without worrying about time zones using <a href="/help/format-your-message-using-markdown#global-times">global times</a>.</div>
</li>
<li>
<div class="list-content">Make a <a href="/help/start-a-call">video call</a> with the click of a button.</div>
</li>
<li>
<div class="list-content">Use Zulip in your language of choice, with translations into <a href="https://www.transifex.com/zulip/zulip/">17 languages</a>.</div>
</li>
</ul>
</div>
<div class="quote">
@ -239,8 +251,12 @@
</li>
<li><div class="list-content">Zulip alerts participants about timely messages with <a href="/help/stream-notifications">fully customizable</a> mobile, email and desktop notifications.</div></li>
<li><div class="list-content">Mention <a href="/help/mention-a-user-or-group">users</a>, <a href="/help/mention-a-user-or-group#mention-a-user-or-group">groups of users</a> or <a href="/help/pm-mention-alert-notifications#wildcard-mentions">everyone</a> when you need their attention.</div></li>
<li><div class="list-content">Zulip works reliably for organizations
with thousands of users online at once.</div></li>
<li>
<div class="list-content">
Zulip works reliably for organizations
with thousands of users online at once.
</div>
</li>
</ul>
</div>
</div>

View File

@ -243,17 +243,17 @@
Zulip is <a href="https://github.com/zulip">100%
open-source software</a>, with no "open core"
catch. We work hard to make it <a href="https://zulip.readthedocs.io/en/latest/production/install.html">easy to set up</a>,
<a href="https://zulip.readthedocs.io/en/stable/production/export-and-import.html#backups">backup
</a>, and <a href="https://zulip.readthedocs.io/en/stable/production/upgrade-or-modify.html">maintain
</a> a self-hosted Zulip installation, where you
<a href="https://zulip.readthedocs.io/en/stable/production/export-and-import.html#backups">backup</a>,
and <a href="https://zulip.readthedocs.io/en/stable/production/upgrade-or-modify.html">maintain</a>
a self-hosted Zulip installation, where you
have full control of your data.
</div>
</li>
<li>
<div class="list-content">
Our high quality <a href="/help/export-your-organization">export
</a> and <a href="https://zulip.readthedocs.io/en/latest/production/export-and-import.html">import
</a> tools ensure that you can always move from
Our high quality <a href="/help/export-your-organization">export</a>
and <a href="https://zulip.readthedocs.io/en/latest/production/export-and-import.html">import</a>
tools ensure that you can always move from
<a href="/plans/">Zulip Cloud</a> hosting to your
own servers. There is no lock-in.
</div>

View File

@ -61,8 +61,12 @@
discussion.
</div>
</li>
<li><div class="list-content">Find active conversations, or see what
happened while you were away, with the <a href="/help/reading-strategies#recent-topics">Recent Topics</a> view.</div></li>
<li>
<div class="list-content">
Find active conversations, or see what happened while you were away,
with the <a href="/help/reading-strategies#recent-topics">Recent Topics</a> view.
</div>
</li>
<li>
<div class="list-content">
Keep discussions orderly
@ -154,9 +158,19 @@
Powerful formatting
</h1>
<ul>
<li><div class="list-content"><a href="/help/format-your-message-using-markdown#latex">Type LaTeX</a> directly into your Zulip message, and see it beautifully rendered.</div></li>
<li><div class="list-content"><a href="/help/code-blocks">Zulip
code blocks</a> come with syntax highlighting for over 250 languages, and integrated <a href="/help/code-blocks#code-playgrounds">code playgrounds.</a></div></li>
<li>
<div class="list-content">
<a href="/help/format-your-message-using-markdown#latex">Type LaTeX</a>
directly into your Zulip message, and see it beautifully rendered.
</div>
</li>
<li>
<div class="list-content">
<a href="/help/code-blocks">Zulip code blocks</a>
come with syntax highlighting for over 250 languages, and integrated
<a href="/help/code-blocks#code-playgrounds">code playgrounds.</a>
</div>
</li>
<li><div class="list-content">Structure your points with bulleted and numbered <a href="/help/format-your-message-using-markdown#lists">lists</a>.</div></li>
<li>
<div class="list-content">

View File

@ -371,8 +371,8 @@
and <a href="https://github.com/matrix-org/synapse/graphs/contributors">matrix.org</a>.
</p>
<p>
<a href="https://zulip.readthedocs.io/en/stable/production/install.html"
class="button">Install Zulip {{ latest_release_version }}</a> or <a href="{{ latest_release_announcement }}">read the Zulip {{ latest_major_version }} release announcement</a>.
<a href="https://zulip.readthedocs.io/en/stable/production/install.html" class="button">Install Zulip {{ latest_release_version }}</a>
or <a href="{{ latest_release_announcement }}">read the Zulip {{ latest_major_version }} release announcement</a>.
</p>
</div>
</div>

View File

@ -117,8 +117,7 @@
As of October 2018, the Zulip server project had
merged <a href="https://github.com/zulip/zulip/pulls">
6500 pull requests</a> written by over
<a href="https://github.com/zulip/zulip/graphs/contributors">
400 developers</a>.
<a href="https://github.com/zulip/zulip/graphs/contributors">400 developers</a>.
</li>
</ul>

View File

@ -20,9 +20,8 @@
<h1 class="center">Case study: <br/>Lean theorem prover community</h1>
</div>
<div class="hero-text">
Learn more about using Zulip for <a
href="/for/research">research</a><br/> and <a
href="/for/open-source">open source</a> communities.
Learn more about using Zulip for <a href="/for/research">research</a><br/>
and <a href="/for/open-source">open source</a> communities.
</div>
</div>
<div class="main">

View File

@ -48,11 +48,15 @@ page can be easily identified in it's respective JavaScript file. -->
{% endif %}
{% if no_auth_enabled %}
<div class="alert">
<p>No authentication backends are enabled on this
server yet, so it is impossible to log in!</p>
<p>
No authentication backends are enabled on this
server yet, so it is impossible to log in!
</p>
<p>See the <a href="https://zulip.readthedocs.io/en/latest/production/install.html#step-3-configure-zulip">Zulip
authentication documentation</a> to learn how to configure authentication backends.</p>
<p>
See the <a href="https://zulip.readthedocs.io/en/latest/production/install.html#step-3-configure-zulip">
Zulip authentication documentation</a> to learn how to configure authentication backends.
</p>
</div>
{% else %}
{% if password_auth_enabled %}

View File

@ -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 %}

View File

@ -4,8 +4,10 @@
<h1>{% trans %}Unknown email unsubscribe request{% endtrans %}</h1>
<p>{% trans %}Hi there! It looks like you tried to unsubscribe from something, but we don't
recognize the URL.{% endtrans %}</p>
<p>
{% trans %}Hi there! It looks like you tried to unsubscribe from something,
but we don't recognize the URL.{% endtrans %}
</p>
<p>{% trans %}Please double-check that you have the full URL and try again, or <a href="mailto:{{ support_email }}?Subject=Unsubscribe%20me%2C%20please!&Body=Hi%20there!%0A%0AI%20clicked%20this%20unsubscribe%20link%20in%20a%20Zulip%20e-mail%2C%20but%20it%20took%20me%20to%20an%20error%20page%3A%0A%0A_____________%0A%0APlease%20unsubscribe%20me.%0A%0AThanks%2C%0A_____________%0A">email us</a> and we'll get this squared away!{% endtrans %}</p>

View File

@ -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)

View File

@ -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:

View File

@ -192,12 +192,12 @@ BAD_HTML8 = """
GOOD_HTML8 = """
{{#each test}}
{{#with this}}
{{#if foobar}}
{{#if foobar}}
<div class="anything">{{{test}}}</div>
{{/if}}
{{#if foobar2}}
{{> teststuff}}
{{/if}}
{{/if}}
{{#if foobar2}}
{{> teststuff}}
{{/if}}
{{/with}}
{{/each}}
"""
@ -325,9 +325,9 @@ GOOD_HTML13 = """
<div>&nbsp:{{this.name}}:</div>
{{else}}
{{#if this.is_realm_emoji}}
<img src="{{this.url}}" class="emoji" />
<img src="{{this.url}}" class="emoji" />
{{else}}
<br />
<br />
{{/if}}
{{/if}}
<div>{{this.count}}</div>
@ -354,12 +354,12 @@ GOOD_HTML14 = """
{{#if this.code}}
<pre>Here goes some cool code.</pre>
{{else}}
<div>
content of first div
<div>
content of second div.
content of first div
<div>
content of second div.
</div>
</div>
</div>
{{/if}}
</div>
"""