mirror of https://github.com/zulip/zulip.git
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:
parent
7e7b628054
commit
fb574431cb
|
@ -31,14 +31,13 @@
|
|||
<p>
|
||||
You may also want to test your email configuration,
|
||||
as described in the
|
||||
<a href="https://zulip.readthedocs.io/en/latest/production/email.html">
|
||||
Production installation docs</a>.
|
||||
<a href="https://zulip.readthedocs.io/en/latest/production/email.html">Production installation docs</a>.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
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">
|
||||
setup guide</a> for forwarding emails sent in development
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://zulip.readthedocs.io/en/latest/subsystems/email.html#development-and-testing"> setup guide</a>
|
||||
for forwarding emails sent in development
|
||||
environment to an email account.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -49,8 +49,8 @@
|
|||
<br />
|
||||
<div class="alert alert-info">
|
||||
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">
|
||||
here</a> first before enabling this.
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://zulip.readthedocs.io/en/latest/subsystems/email.html#development-and-testing"> here</a>
|
||||
first before enabling this.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<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 %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,208 +1,138 @@
|
|||
import subprocess
|
||||
from typing import List, Optional, Set
|
||||
from typing import List, Optional
|
||||
|
||||
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:
|
||||
line = line.lstrip()
|
||||
return line.startswith("<")
|
||||
def shift_indents_to_the_next_tokens(tokens: List[Token]) -> None:
|
||||
"""
|
||||
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]
|
||||
|
||||
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:
|
||||
was_closed = False
|
||||
while tokens and tokens[-1].line <= row:
|
||||
token = tokens.pop()
|
||||
if close_token(token):
|
||||
was_closed = True
|
||||
return was_closed
|
||||
|
||||
|
||||
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, 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
|
||||
next_token.indent = token.s
|
||||
token.new_s = ""
|
||||
|
||||
if token.kind == "newline" and next_token.kind != "indent":
|
||||
next_token.indent = ""
|
||||
|
||||
|
||||
def token_allows_children_to_skip_indents(token: Token) -> bool:
|
||||
# For legacy reasons we don't always indent blocks.
|
||||
return token.kind in ("django_start", "handlebars_start") or token.tag == "a"
|
||||
|
||||
|
||||
def adjust_block_indentation(tokens: List[Token], fn: str) -> None:
|
||||
start_token: Optional[Token] = None
|
||||
|
||||
for token in tokens:
|
||||
if token.kind in ("indent", "whitespace", "newline"):
|
||||
continue
|
||||
|
||||
if token.tag in ("code", "pre"):
|
||||
continue
|
||||
|
||||
# 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")
|
||||
|
||||
if token.tag == "else":
|
||||
assert token.start_token
|
||||
if token.indent is not None:
|
||||
token.indent = token.start_token.indent
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
# Detect start token by its having a end token
|
||||
if token.end_token:
|
||||
if token.indent is not None:
|
||||
token.orig_indent = token.indent
|
||||
if start_token:
|
||||
assert start_token.child_indent is not None
|
||||
token.indent = start_token.child_indent
|
||||
else:
|
||||
tag_continuation_offset = offset + " "
|
||||
tag_end_row = row + token.line_span
|
||||
token.indent = ""
|
||||
token.child_indent = token.indent + " "
|
||||
token.parent_token = start_token
|
||||
start_token = token
|
||||
continue
|
||||
|
||||
pref = indent_pref(row, tokens, line)
|
||||
if pref == "open":
|
||||
if same_indent(line, next_line) and not requires_indent(line):
|
||||
next_offset = offset
|
||||
# Detect end token by its having a start token
|
||||
if token.start_token:
|
||||
if start_token != token.start_token:
|
||||
raise AssertionError(
|
||||
f"""
|
||||
{token.kind} was unexpected in {token.s}
|
||||
in row {token.line} of {fn}
|
||||
"""
|
||||
)
|
||||
|
||||
if token.indent is not None:
|
||||
token.indent = start_token.indent
|
||||
start_token = start_token.parent_token
|
||||
continue
|
||||
|
||||
if token.indent is None:
|
||||
continue
|
||||
|
||||
if start_token is None:
|
||||
token.indent = ""
|
||||
continue
|
||||
|
||||
if start_token.child_indent is not None:
|
||||
token.indent = start_token.child_indent
|
||||
|
||||
|
||||
def fix_indents_for_multi_line_tags(tokens: List[Token]) -> None:
|
||||
for token in tokens:
|
||||
if token.kind == "code":
|
||||
continue
|
||||
|
||||
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:
|
||||
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
|
||||
continue_indent = token.indent + " "
|
||||
|
||||
def adjusted_line(row: int, line: str, next_line: str) -> str:
|
||||
if line.strip() == "":
|
||||
return ""
|
||||
frags = token.new_s.split("\n")
|
||||
|
||||
offset = line_offset(row, line, next_line)
|
||||
def fix(frag: str) -> str:
|
||||
frag = frag.strip()
|
||||
return continue_indent + frag if frag else ""
|
||||
|
||||
if row in exempted_lines:
|
||||
return line.rstrip()
|
||||
token.new_s = frags[0] + "\n" + "\n".join(fix(frag) for frag in frags[1:])
|
||||
|
||||
if offset is None:
|
||||
return line.rstrip()
|
||||
|
||||
return offset + line.strip()
|
||||
def apply_token_indents(tokens: List[Token]) -> None:
|
||||
for token in tokens:
|
||||
if token.indent:
|
||||
token.new_s = token.indent + token.new_s
|
||||
|
||||
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)
|
||||
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:
|
||||
|
@ -212,7 +142,7 @@ def numbered_lines(s: str) -> str:
|
|||
def validate_indent_html(fn: str, tokens: List[Token], fix: bool) -> bool:
|
||||
with open(fn) as f:
|
||||
html = f.read()
|
||||
phtml = pretty_print_html(html, tokens)
|
||||
phtml = pretty_print_html(tokens, fn)
|
||||
if not html.split("\n") == phtml.split("\n"):
|
||||
if fix:
|
||||
print(GREEN + f"Automatically fixing indentation for {fn}" + ENDC)
|
||||
|
|
|
@ -39,6 +39,14 @@ class Token:
|
|||
self.start_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]:
|
||||
in_code_block = False
|
||||
|
|
|
@ -281,8 +281,9 @@ GOOD_HTML11 = """
|
|||
|
||||
|
||||
def pretty_print(html: str) -> str:
|
||||
tokens = validate(fn=None, text=html)
|
||||
return pretty_print_html(html, tokens)
|
||||
fn = "<test str>"
|
||||
tokens = validate(fn=fn, text=html)
|
||||
return pretty_print_html(tokens, fn=fn)
|
||||
|
||||
|
||||
class TestPrettyPrinter(unittest.TestCase):
|
||||
|
|
Loading…
Reference in New Issue