check-templates: Rewrite pretty_print (again).

It now does everything based on the tokens, rather
than walking the lines and trying to match up tokens
to lines.
This commit is contained in:
Steve Howell 2021-12-02 16:10:42 +00:00 committed by Tim Abbott
parent 7e7b628054
commit fb574431cb
9 changed files with 135 additions and 197 deletions

View File

@ -19,7 +19,7 @@ page can be easily identified in it's respective JavaScript file -->
{% include 'zerver/dev_env_email_access_details.html' %} {% include 'zerver/dev_env_email_access_details.html' %}
<p>{% trans %}Still no email? We can <a href="#" id="resend_email_link">resend it</a>.{% endtrans %} <p>{% trans %}Still no email? We can <a href="#" id="resend_email_link">resend it</a>.{% endtrans %}
<i class="grey">({{ _("Just in case, take a look at your Spam folder.") }})</i></p> <i class="grey">({{ _("Just in case, take a look at your Spam folder.") }})</i></p>
{% if realm_creation %} {% if realm_creation %}
<form class="resend_confirm" action="/new/" method="post" style="position: absolute;"> <form class="resend_confirm" action="/new/" method="post" style="position: absolute;">
{{ csrf_input }} {{ csrf_input }}

View File

@ -31,14 +31,13 @@
<p> <p>
You may also want to test your email configuration, You may also want to test your email configuration,
as described in the as described in the
<a href="https://zulip.readthedocs.io/en/latest/production/email.html"> <a href="https://zulip.readthedocs.io/en/latest/production/email.html">Production installation docs</a>.
Production installation docs</a>.
</p> </p>
{% else %} {% else %}
<p> <p>
Please have a look at our Please have a look at our
<a target="_blank" rel="noopener noreferrer" href="https://zulip.readthedocs.io/en/latest/subsystems/email.html#development-and-testing"> <a target="_blank" rel="noopener noreferrer" href="https://zulip.readthedocs.io/en/latest/subsystems/email.html#development-and-testing"> setup guide</a>
setup guide</a> for forwarding emails sent in development for forwarding emails sent in development
environment to an email account. environment to an email account.
</p> </p>
{% endif %} {% endif %}
@ -51,8 +50,8 @@
{% if has_markdown_file %} {% if has_markdown_file %}
{% if development_environment %} {% if development_environment %}
{{ render_markdown_path('zerver/'+social_backend_name+'-error.md', {{ render_markdown_path('zerver/'+social_backend_name+'-error.md',
{"root_domain_uri": root_domain_uri, "settings_path": secrets_path, "secrets_path": secrets_path, {"root_domain_uri": root_domain_uri, "settings_path": secrets_path, "secrets_path": secrets_path,
"client_id_key_name": "social_auth_" + social_backend_name + "_key"}) }} "client_id_key_name": "social_auth_" + social_backend_name + "_key"}) }}
<p> <p>
For more information, have a look at For more information, have a look at
the <a href="https://zulip.readthedocs.io/en/latest/development/authentication.html#{{ social_backend_name }}">authentication the <a href="https://zulip.readthedocs.io/en/latest/development/authentication.html#{{ social_backend_name }}">authentication
@ -60,8 +59,8 @@
</p> </p>
{% else %} {% else %}
{{ render_markdown_path('zerver/'+social_backend_name+'-error.md', {{ render_markdown_path('zerver/'+social_backend_name+'-error.md',
{"root_domain_uri": root_domain_uri, "settings_path": settings_path, "secrets_path": secrets_path, {"root_domain_uri": root_domain_uri, "settings_path": settings_path, "secrets_path": secrets_path,
"client_id_key_name": "SOCIAL_AUTH_" + social_backend_name.upper() + "_KEY"}) }} "client_id_key_name": "SOCIAL_AUTH_" + social_backend_name.upper() + "_KEY"}) }}
<p> <p>
For more information, have a look at For more information, have a look at
our <a href="https://zulip.readthedocs.io/en/latest/production/authentication-methods.html">authentication our <a href="https://zulip.readthedocs.io/en/latest/production/authentication-methods.html">authentication

View File

@ -49,8 +49,8 @@
<br /> <br />
<div class="alert alert-info"> <div class="alert alert-info">
You must set up SMTP as described You must set up SMTP as described
<a target="_blank" rel="noopener noreferrer" href="https://zulip.readthedocs.io/en/latest/subsystems/email.html#development-and-testing"> <a target="_blank" rel="noopener noreferrer" href="https://zulip.readthedocs.io/en/latest/subsystems/email.html#development-and-testing"> here</a>
here</a> first before enabling this. first before enabling this.
</div> </div>
</form> </form>
</div> </div>

View File

@ -8,7 +8,7 @@
<p>{% trans %}Hi,{% endtrans %}</p> <p>{% trans %}Hi,{% endtrans %}</p>
<p>{% trans realm_uri=macros.link_tag(realm_uri), old_email=macros.email_tag(old_email), new_email=macros.email_tag(new_email) %}We received a request to change the email address for the Zulip account on {{ realm_uri }} from {{ old_email }} to {{ new_email }}. To confirm this change, please click below:{% endtrans %} <p>{% trans realm_uri=macros.link_tag(realm_uri), old_email=macros.email_tag(old_email), new_email=macros.email_tag(new_email) %}We received a request to change the email address for the Zulip account on {{ realm_uri }} from {{ old_email }} to {{ new_email }}. To confirm this change, please click below:{% endtrans %}
<a class="button" href="{{ activate_url }}">{{_('Confirm email change') }}</a></p> <a class="button" href="{{ activate_url }}">{{_('Confirm email change') }}</a></p>
<p>{% trans support_email=macros.email_tag(support_email) %}If you did not request this change, please contact us immediately at {{ support_email }}.{% endtrans %}</p> <p>{% trans support_email=macros.email_tag(support_email) %}If you did not request this change, please contact us immediately at {{ support_email }}.{% endtrans %}</p>
{% endblock %} {% endblock %}

View File

@ -105,7 +105,7 @@
text editor. Anything you can do with a mouse, you text editor. Anything you can do with a mouse, you
can do even faster from the keyboard. can do even faster from the keyboard.
<a class="cta" href="/help/keyboard-shortcuts" target="_blank" rel="noopener noreferrer"> <a class="cta" href="/help/keyboard-shortcuts" target="_blank" rel="noopener noreferrer">
Learn more about keyboard shortcuts.</a> Learn more about keyboard shortcuts.</a>
</p> </p>
</div> </div>
<img class="image" src="/static/images/landing-page/love-keyboard-shortcuts.svg" alt="" /> <img class="image" src="/static/images/landing-page/love-keyboard-shortcuts.svg" alt="" />

View File

@ -124,7 +124,7 @@
<li><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html#synchronizing-data">LDAP/Active Directory sync</a></li> <li><a href="https://zulip.readthedocs.io/en/stable/production/authentication-methods.html#synchronizing-data">LDAP/Active Directory sync</a></li>
<li>Advanced <a href="/help/roles-and-permissions">roles</a> and <a href="/help/stream-permissions">permissions</a></li> <li>Advanced <a href="/help/roles-and-permissions">roles</a> and <a href="/help/stream-permissions">permissions</a></li>
<li>Easy <a href="https://zulip.readthedocs.io/en/stable/production/install.html">installation</a> <li>Easy <a href="https://zulip.readthedocs.io/en/stable/production/install.html">installation</a>
and <a href="https://zulip.readthedocs.io/en/stable/production/upgrade-or-modify.html">maintenance</a></li> and <a href="https://zulip.readthedocs.io/en/stable/production/upgrade-or-modify.html">maintenance</a></li>
</ul> </ul>
</div> </div>
<div class="bottom"> <div class="bottom">

View File

@ -1,208 +1,138 @@
import subprocess import subprocess
from typing import List, Optional, Set from typing import List, Optional
from zulint.printer import BOLDRED, CYAN, ENDC, GREEN from zulint.printer import BOLDRED, CYAN, ENDC, GREEN
from .template_parser import Token, is_django_block_tag from .template_parser import Token
def requires_indent(line: str) -> bool: def shift_indents_to_the_next_tokens(tokens: List[Token]) -> None:
line = line.lstrip() """
return line.startswith("<") During the parsing/validation phase, it's useful to have separate
tokens for "indent" chunks, but during pretty printing, we like
to attach an `.indent` field to the substantive node, whether
it's an HTML tag or template directive or whatever.
"""
tokens[0].indent = ""
for i, token in enumerate(tokens[:-1]):
next_token = tokens[i + 1]
if token.kind == "indent":
next_token.indent = token.s
token.new_s = ""
if token.kind == "newline" and next_token.kind != "indent":
next_token.indent = ""
def open_token(token: Token) -> bool: def token_allows_children_to_skip_indents(token: Token) -> bool:
if token.kind in ( # For legacy reasons we don't always indent blocks.
"handlebars_start", return token.kind in ("django_start", "handlebars_start") or token.tag == "a"
"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: def adjust_block_indentation(tokens: List[Token], fn: str) -> None:
return token.kind in ( start_token: Optional[Token] = None
"django_end",
"handlebars_end",
"html_end",
"jinja2_whitespace_stripped_end",
)
for token in tokens:
if token.kind in ("indent", "whitespace", "newline"):
continue
def else_token(token: Token) -> bool: if token.tag in ("code", "pre"):
return token.kind in ( continue
"django_else",
"handlebars_else",
)
# print(token.line, repr(start_token.indent) if start_token else "?", repr(token.indent), token.s, token.end_token and "start", token.start_token and "end")
def pop_unused_tokens(tokens: List[Token], row: int) -> bool: if token.tag == "else":
was_closed = False assert token.start_token
while tokens and tokens[-1].line <= row: if token.indent is not None:
token = tokens.pop() token.indent = token.start_token.indent
if close_token(token): continue
was_closed = True
return was_closed
if start_token and token.indent is not None:
if not start_token.indent_is_final and token.indent == start_token.orig_indent:
if token_allows_children_to_skip_indents(start_token):
start_token.child_indent = start_token.indent
start_token.indent_is_final = True
def indent_pref(row: int, tokens: List[Token], line: str) -> str: # Detect start token by its having a end token
opens = 0 if token.end_token:
closes = 0 if token.indent is not None:
is_else = False token.orig_indent = token.indent
if start_token:
while tokens and tokens[-1].line == row: assert start_token.child_indent is not None
token = tokens.pop() token.indent = start_token.child_indent
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, tokens: List[Token]) -> str:
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
while tokens and tokens[-1].line < row:
token = tokens.pop()
offset = next_offset
if tokens:
token = tokens[-1]
if token.kind == "indent":
token = tokens[-2]
if (
token.line == row
and token.line_span > 1
and token.kind not in ("template_var", "text")
):
if token.kind in ("django_comment", "handlebar_comment", "html_comment"):
tag_continuation_offset = offset
else: else:
tag_continuation_offset = offset + " " token.indent = ""
tag_end_row = row + token.line_span token.child_indent = token.indent + " "
token.parent_token = start_token
start_token = token
continue
pref = indent_pref(row, tokens, line) # Detect end token by its having a start token
if pref == "open": if token.start_token:
if same_indent(line, next_line) and not requires_indent(line): if start_token != token.start_token:
next_offset = offset raise AssertionError(
else: f"""
next_offset = offset + " " * 4 {token.kind} was unexpected in {token.s}
open_offsets.append(offset) in row {token.line} of {fn}
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 token.indent is not None:
if line.strip() == "": token.indent = start_token.indent
return "" start_token = start_token.parent_token
continue
offset = line_offset(row, line, next_line) if token.indent is None:
continue
if row in exempted_lines: if start_token is None:
return line.rstrip() token.indent = ""
continue
if offset is None: if start_token.child_indent is not None:
return line.rstrip() token.indent = start_token.child_indent
return offset + line.strip()
for i, line in enumerate(lines): def fix_indents_for_multi_line_tags(tokens: List[Token]) -> None:
# We use 1-based indexing for both rows and columns. for token in tokens:
next_line = next_non_blank_line(lines, i) if token.kind == "code":
row = i + 1 continue
formatted_lines.append(adjusted_line(row, line, next_line))
return "\n".join(formatted_lines) if token.line_span == 1 or token.indent is None:
continue
if token.kind in ("django_comment", "handlebar_comment", "html_comment", "text"):
continue_indent = token.indent
else:
continue_indent = token.indent + " "
frags = token.new_s.split("\n")
def fix(frag: str) -> str:
frag = frag.strip()
return continue_indent + frag if frag else ""
token.new_s = frags[0] + "\n" + "\n".join(fix(frag) for frag in frags[1:])
def apply_token_indents(tokens: List[Token]) -> None:
for token in tokens:
if token.indent:
token.new_s = token.indent + token.new_s
def pretty_print_html(tokens: List[Token], fn: str) -> str:
for token in tokens:
token.new_s = token.s
shift_indents_to_the_next_tokens(tokens)
adjust_block_indentation(tokens, fn)
fix_indents_for_multi_line_tags(tokens)
apply_token_indents(tokens)
return "".join(token.new_s for token in tokens)
def numbered_lines(s: str) -> str: def numbered_lines(s: str) -> str:
@ -212,7 +142,7 @@ def numbered_lines(s: str) -> str:
def validate_indent_html(fn: str, tokens: List[Token], fix: bool) -> bool: def validate_indent_html(fn: str, tokens: List[Token], fix: bool) -> bool:
with open(fn) as f: with open(fn) as f:
html = f.read() html = f.read()
phtml = pretty_print_html(html, tokens) phtml = pretty_print_html(tokens, fn)
if not html.split("\n") == phtml.split("\n"): if not html.split("\n") == phtml.split("\n"):
if fix: if fix:
print(GREEN + f"Automatically fixing indentation for {fn}" + ENDC) print(GREEN + f"Automatically fixing indentation for {fn}" + ENDC)

View File

@ -39,6 +39,14 @@ class Token:
self.start_token: Optional[Token] = None self.start_token: Optional[Token] = None
self.end_token: Optional[Token] = None self.end_token: Optional[Token] = None
# These get set during the pretty-print phase.
self.new_s = ""
self.indent: Optional[str] = None
self.orig_indent: Optional[str] = None
self.child_indent: Optional[str] = None
self.indent_is_final = False
self.parent_token: Optional[Token] = None
def tokenize(text: str) -> List[Token]: def tokenize(text: str) -> List[Token]:
in_code_block = False in_code_block = False

View File

@ -281,8 +281,9 @@ GOOD_HTML11 = """
def pretty_print(html: str) -> str: def pretty_print(html: str) -> str:
tokens = validate(fn=None, text=html) fn = "<test str>"
return pretty_print_html(html, tokens) tokens = validate(fn=fn, text=html)
return pretty_print_html(tokens, fn=fn)
class TestPrettyPrinter(unittest.TestCase): class TestPrettyPrinter(unittest.TestCase):