mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	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.
		
			
				
	
	
		
			221 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			221 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import subprocess
 | 
						|
from typing import List, Optional, Set
 | 
						|
 | 
						|
from zulint.printer import ENDC, GREEN
 | 
						|
 | 
						|
from .template_parser import Token, is_django_block_tag, tokenize
 | 
						|
 | 
						|
 | 
						|
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")
 | 
						|
 | 
						|
    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)
 | 
						|
 | 
						|
 | 
						|
def validate_indent_html(fn: str, fix: bool) -> int:
 | 
						|
    with open(fn) as f:
 | 
						|
        html = f.read()
 | 
						|
    phtml = pretty_print_html(html)
 | 
						|
    if not html.split("\n") == phtml.split("\n"):
 | 
						|
        if fix:
 | 
						|
            print(GREEN + "Automatically fixing problems..." + ENDC)
 | 
						|
            with open(fn, "w") as f:
 | 
						|
                f.write(phtml)
 | 
						|
            # Since we successfully fixed the issues, we exit with status 0
 | 
						|
            return 0
 | 
						|
        print(
 | 
						|
            "Invalid indentation detected in file: "
 | 
						|
            f"{fn}\nDiff for the file against expected indented file:",
 | 
						|
            flush=True,
 | 
						|
        )
 | 
						|
        subprocess.run(["diff", fn, "-"], input=phtml, universal_newlines=True)
 | 
						|
        print()
 | 
						|
        print("This problem can be fixed with the `--fix` option.")
 | 
						|
        return 0
 | 
						|
    return 1
 |