mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			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) -> bool:
 | 
						|
    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 + f"Automatically fixing indentation for {fn}" + ENDC)
 | 
						|
            with open(fn, "w") as f:
 | 
						|
                f.write(phtml)
 | 
						|
            # Since we successfully fixed the issues, we return True.
 | 
						|
            return True
 | 
						|
        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 False
 | 
						|
    return True
 |