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 status_message}}
{{#unless is_hidden}} {{#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}} {{else}}
{{> message_hidden_dialog}} {{> message_hidden_dialog}}
{{/unless}} {{/unless}}

View File

@ -72,8 +72,10 @@
{{#if has_been_editable}} {{#if has_been_editable}}
<div class="message-edit-timer-control-group"> <div class="message-edit-timer-control-group">
<span class="message_edit_countdown_timer"></span> <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" <span>
{{#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> <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> </span>
</div> </div>
{{/if}} {{/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' }}" <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' }} (/)"> autocomplete="off" aria-label="{{t 'Search' }}" title="{{t 'Search' }} (/)">
</div> </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> </div>
</form> </form>
</div> </div>

View File

@ -4,7 +4,12 @@
<div class="button-group"> <div class="button-group">
<div class="sub_unsub_button_wrapper inline-block"> <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}}> <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> </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> <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}} {{#if is_realm_admin}}

View File

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

View File

@ -157,11 +157,14 @@
suffice. suffice.
</li> </li>
<li>Share your expertise (both newly-acquired and longstanding) with the <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, <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 <li>Work from our office in San Francisco, or from anywhere in the United
States.</li> States.
</li>
</ul> </ul>
<h3>Extra credit for any of the following:</h3> <h3>Extra credit for any of the following:</h3>

View File

@ -35,13 +35,21 @@
<h2>If you want to automatically transfer your existing Zephyr subscriptions</h2> <h2>If you want to automatically transfer your existing Zephyr subscriptions</h2>
<ol> <ol>
<li><p>Get your Zulip API key from the Zulip "Settings" panel and put it in a file in your <li>
Athena home directory called <code>~/Private/.zulip-api-key</code>.</p></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 /> <li>
<code>/mit/tabbott/zulip/zephyr_mirror.py --sync-subscriptions</code></p> <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. 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 If you have more subscriptions than you generally read, we recommend that you use
Zulip's "Mute" option to hide those subscriptions from your Zulip's "Mute" option to hide those subscriptions from your

View File

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

View File

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

View File

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

View File

@ -292,10 +292,18 @@
Powerful formatting Powerful formatting
</h1> </h1>
<ul> <ul>
<li><div class="list-content"><a href="/help/code-blocks">Zulip <li>
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> <div class="list-content">
<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> <a href="/help/code-blocks">Zulip code blocks</a>
<li><div class="list-content">Enjoy inline image, video and Tweet previews.</div></li> 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> <li>
<div class="list-content"> <div class="list-content">
If you made a mistake, no worries! You 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. With <a href="/apps">apps for every platform</a>, you can check Zulip at your computer or on your phone.
</div> </div>
</li> </li>
<li><div class="list-content">Zulip alerts you <li>
about timely messages with <a href="/help/stream-notifications">fully customizable</a> mobile, email and desktop notifications.</div></li> <div class="list-content">
<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> 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>
<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> </ul>
</div> </div>
</div> </div>

View File

@ -113,12 +113,24 @@
Your communication hub Your communication hub
</h1> </h1>
<ul> <ul>
<li><div class="list-content">Share the agenda and presentations with <li>
<a href="/help/share-and-upload-files">drag-and-drop file uploads</a>.</div></li> <div class="list-content">
<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> Share the agenda and presentations with
<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> <a href="/help/share-and-upload-files">drag-and-drop file uploads</a>.
<li><div class="list-content">Make a <a href="/help/start-a-call">video call</a> with the click of a button.</div></li> </div>
<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>
<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> </ul>
</div> </div>
<div class="quote"> <div class="quote">
@ -239,8 +251,12 @@
</li> </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">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">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 <li>
with thousands of users online at once.</div></li> <div class="list-content">
Zulip works reliably for organizations
with thousands of users online at once.
</div>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

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

View File

@ -61,8 +61,12 @@
discussion. discussion.
</div> </div>
</li> </li>
<li><div class="list-content">Find active conversations, or see what <li>
happened while you were away, with the <a href="/help/reading-strategies#recent-topics">Recent Topics</a> view.</div></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> <li>
<div class="list-content"> <div class="list-content">
Keep discussions orderly Keep discussions orderly
@ -154,9 +158,19 @@
Powerful formatting Powerful formatting
</h1> </h1>
<ul> <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>
<li><div class="list-content"><a href="/help/code-blocks">Zulip <div class="list-content">
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> <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">Structure your points with bulleted and numbered <a href="/help/format-your-message-using-markdown#lists">lists</a>.</div></li>
<li> <li>
<div class="list-content"> <div class="list-content">

View File

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

View File

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

View File

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

View File

@ -48,11 +48,15 @@ page can be easily identified in it's respective JavaScript file. -->
{% endif %} {% endif %}
{% if no_auth_enabled %} {% if no_auth_enabled %}
<div class="alert"> <div class="alert">
<p>No authentication backends are enabled on this <p>
server yet, so it is impossible to log in!</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 <p>
authentication documentation</a> to learn how to configure authentication backends.</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> </div>
{% else %} {% else %}
{% if password_auth_enabled %} {% if password_auth_enabled %}

View File

@ -4,8 +4,10 @@
<h1>{% trans %}Unknown email unsubscribe request{% endtrans %}</h1> <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 <p>
recognize the URL.{% endtrans %}</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> <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 import subprocess
from typing import Any, Dict, List from typing import List, Optional, Set
from zulint.printer import ENDC, GREEN 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: def requires_indent(line: str) -> bool:
# We use 1-based indexing for both rows and columns. line = line.lstrip()
tokens = tokenize(html) return line.startswith("<")
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 def open_token(token: Token) -> bool:
# easier. if token.kind in (
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_start",
"handlebars_singleton", "html_start",
"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 return True
# 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 in (
if (
(
token.kind == "handlebars_start"
and stack[-1]["token_kind"] == "handlebars_start"
)
or (
token.kind
in {
"django_start", "django_start",
"jinja2_whitespace_stripped_type2_start",
"jinja2_whitespace_stripped_start", "jinja2_whitespace_stripped_start",
}
and stack[-1]["token_kind"]
in {
"django_start",
"jinja2_whitespace_stripped_type2_start", "jinja2_whitespace_stripped_type2_start",
"jinja2_whitespace_stripped_start", ):
} return is_django_block_tag(token.tag)
)
) and not stack[-1]["indenting"]: return False
info = stack.pop()
info["depth"] = info["depth"] + 1
info["indenting"] = True def close_token(token: Token) -> bool:
info["adjust_offset_until"] = token.line return token.kind in (
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", "django_end",
"handlebars_singleton_end", "handlebars_end",
"html_end",
"jinja2_whitespace_stripped_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. def else_token(token: Token) -> bool:
formatted_lines = [] return token.kind in (
for i, line in enumerate(html.split("\n")): "django_else",
row = i + 1 "handlebars_else",
offset = offsets.get(row, 0) )
pretty_line = line
if line.strip() == "":
pretty_line = "" 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: else:
if offset > 0: print(i, opens, closes)
pretty_line = (" " * offset) + pretty_line raise Exception(f"too many tokens on row {row}")
elif offset < 0:
pretty_line = pretty_line[-1 * offset :]
assert line.strip() == pretty_line.strip() def indent_level(s: str) -> int:
formatted_lines.append(pretty_line) 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")
open_offsets: List[str] = []
formatted_lines = []
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() == "":
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) return "\n".join(formatted_lines)

View File

@ -70,11 +70,17 @@ def tokenize(text: str) -> List[Token]:
def looking_at_handlebars_start() -> bool: def looking_at_handlebars_start() -> bool:
return looking_at("{{#") or looking_at("{{^") return looking_at("{{#") or looking_at("{{^")
def looking_at_handlebars_else() -> bool:
return looking_at("{{else")
def looking_at_handlebars_end() -> bool: def looking_at_handlebars_end() -> bool:
return looking_at("{{/") return looking_at("{{/")
def looking_at_django_start() -> bool: 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: def looking_at_django_end() -> bool:
return looking_at("{% end") return looking_at("{% end")
@ -130,6 +136,10 @@ def tokenize(text: str) -> List[Token]:
s = get_html_tag(text, state.i) s = get_html_tag(text, state.i)
tag = s[2:-1] tag = s[2:-1]
kind = "html_end" 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(): elif looking_at_handlebars_start():
s = get_handlebars_tag(text, state.i) s = get_handlebars_tag(text, state.i)
tag = s[3:-2].split()[0] tag = s[3:-2].split()[0]
@ -140,17 +150,22 @@ def tokenize(text: str) -> List[Token]:
s = get_handlebars_tag(text, state.i) s = get_handlebars_tag(text, state.i)
tag = s[3:-2] tag = s[3:-2]
kind = "handlebars_end" 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(): elif looking_at_django_start():
# must check this after end/else
s = get_django_tag(text, state.i) s = get_django_tag(text, state.i)
tag = s[3:-2].split()[0] tag = s[3:-2].split()[0]
kind = "django_start" kind = "django_start"
if s[-3] == "-": if s[-3] == "-":
kind = "jinja2_whitespace_stripped_start" 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(): elif looking_at_jinja2_end_whitespace_stripped():
s = get_django_tag(text, state.i) s = get_django_tag(text, state.i)
tag = s[7:-3] tag = s[7:-3]
@ -284,6 +299,7 @@ def validate(
state.foreign = True state.foreign = True
def f(end_token: Token) -> None: def f(end_token: Token) -> None:
is_else_tag = end_token.tag == "else"
end_tag = end_token.tag.strip("~") end_tag = end_token.tag.strip("~")
end_line = end_token.line end_line = end_token.line
@ -297,9 +313,12 @@ def validate(
problem = None problem = None
if (start_tag == "code") and (end_line == start_line + 1): if (start_tag == "code") and (end_line == start_line + 1):
problem = "Code tag is split across two lines." 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." 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: if end_col != start_col:
problem = "Bad indentation." problem = "Bad indentation."
@ -324,6 +343,8 @@ def validate(
line {end_line}, col {end_col} line {end_line}, col {end_col}
""" """
) )
if not is_else_tag:
state.matcher = old_matcher state.matcher = old_matcher
state.foreign = old_foreign state.foreign = old_foreign
state.depth -= 1 state.depth -= 1
@ -350,17 +371,20 @@ def validate(
elif kind == "handlebars_start": elif kind == "handlebars_start":
start_tag_matcher(token) start_tag_matcher(token)
elif kind == "handlebars_else":
state.matcher(token)
elif kind == "handlebars_end": elif kind == "handlebars_end":
state.matcher(token) state.matcher(token)
elif kind in { elif kind in {
"django_start", "django_start",
"django_else",
"jinja2_whitespace_stripped_start", "jinja2_whitespace_stripped_start",
"jinja2_whitespace_stripped_type2_start", "jinja2_whitespace_stripped_type2_start",
}: }:
if is_django_block_tag(tag): if is_django_block_tag(tag):
start_tag_matcher(token) 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) state.matcher(token)
if state.depth != 0: if state.depth != 0: