"""Shared recognition of TSL codegen API headings and declaration lines.""" from bisect import bisect_right from dataclasses import dataclass import re TOP_LEVEL_RE = re.compile(r"^##(?!#)\s+`(.+?)`\s*$") PLAIN_H2_RE = re.compile(r"^##(?!#)\s+") BARE_API_RE = re.compile(r"^(#{3,5})(?!#)\s+`(.+?)`\s*$") LEGACY_TYPED_RE = re.compile( r"^(#{3,5})(?!#)\s+" r"(class function|static function|static field|static const|" r"function|property|field|const|var|class)\s+`(.+?)`\s*$" ) DECLARATION_LINE_RE = re.compile(r"^声明:(.*?)\s*$") DECLARATION_PREFIX_RE = re.compile(r"^声明[::]", re.IGNORECASE) LOOSE_DECLARATION_RE = re.compile(r"^声明[::]\s*(.*?)\s*$", re.IGNORECASE) TOP_LEVEL_TYPES = {"function", "class", "unit"} CLASS_DECLARATIONS = { "function": ("method", "instance"), "class function": ("method", "class"), "property": ("property", "instance"), "field": ("field", "instance"), "static field": ("field", "static"), "const": ("constant", "instance"), "static const": ("constant", "static"), } UNIT_DECLARATIONS = { "function": ("function", "unit"), "var": ("variable", "unit"), "const": ("constant", "unit"), "class": ("class", "unit"), } HEADING_RE = re.compile(r"^(#{1,6})(?!#)\s+") FENCE_RE = re.compile(r"^\s*(```|~~~)") @dataclass(frozen=True) class ApiHeading: level: int kind: str binding: str name: str signature: str visible_title: str root_kind: str valid: bool = True error: str = "" @dataclass(frozen=True) class ApiEntry: heading: ApiHeading start: int end: int def simple_name(signature): return signature.split("(", 1)[0].strip() def fence_flags(lines): flags = [] in_fence = False marker = "" for line in lines: stripped = line.lstrip() if not in_fence and FENCE_RE.match(line): marker = stripped[:3] flags.append(True) in_fence = True continue flags.append(in_fence) if in_fence and stripped.startswith(marker): in_fence = False marker = "" return flags def heading_body_end(lines, start, flags=None): if flags is None: flags = fence_flags(lines) for index in range(start + 1, len(lines)): if flags[index]: continue if HEADING_RE.match(lines[index]): return index return len(lines) def top_level_header_end(lines, start, flags=None): return heading_body_end(lines, start, flags) def _declaration(lines, start, end, flags): content = [] declaration_lines = [] for index in range(start + 1, end): if flags[index]: continue stripped = lines[index].strip() if not stripped: continue content.append((index, stripped)) if DECLARATION_PREFIX_RE.match(stripped): declaration_lines.append((index, stripped)) if not content: return "", "API 标题后的第一条非空正文必须是 `声明:...`" first_index, first = content[0] match = DECLARATION_LINE_RE.fullmatch(first) if not match: loose = LOOSE_DECLARATION_RE.fullmatch(first) if loose and loose.group(1).strip(): expected = f"声明:{loose.group(1).strip().casefold()}" return "", f"声明行必须精确写为 `{expected}`" return ( "", "API 标题后的第一条非空正文必须是规范的 `声明:...` 行", ) value = match.group(1).strip().casefold() expected = f"声明:{value}" if first != expected: return "", f"声明行必须精确写为 `{expected}`" if len(declaration_lines) != 1 or declaration_lines[0][0] != first_index: return "", "每个 API 必须且只能包含一行声明" return value, "" def top_level_type(lines, start, end): return _declaration(lines, start, end, fence_flags(lines)) def invalid_heading(level, name, signature, visible_title, root_kind, message): return ApiHeading( level, "invalid", "", name, signature, visible_title, root_kind, False, message, ) def _valid_heading(level, kind, binding, signature, root_kind): return ApiHeading( level, kind, binding, simple_name(signature), signature, signature, root_kind, ) def top_level_heading(signature, declaration, error): if error: return invalid_heading( 2, simple_name(signature), signature, signature, "", error, ) if declaration not in TOP_LEVEL_TYPES: return invalid_heading( 2, simple_name(signature), signature, signature, "", f"顶级 API 声明必须是 function、class 或 unit,实为 {declaration}", ) return _valid_heading(2, declaration, "", signature, declaration) def member_heading( level, declaration, signature, root_kind, unit_class_open, error, ): name = simple_name(signature) if error: return invalid_heading( level, name, signature, signature, root_kind, error ) if declaration == "static function": return invalid_heading( level, name, signature, signature, root_kind, "不存在 static function,请使用 class function", ) if root_kind == "class": if level != 3 or declaration not in CLASS_DECLARATIONS: return invalid_heading( level, name, signature, signature, root_kind, "class API 成员必须使用约定的 H3 声明", ) kind, binding = CLASS_DECLARATIONS[declaration] return _valid_heading(level, kind, binding, signature, root_kind) if root_kind == "unit": if level == 3: if declaration not in UNIT_DECLARATIONS: return invalid_heading( level, name, signature, signature, root_kind, "unit interface 成员必须使用约定的 H3 声明", ) kind, binding = UNIT_DECLARATIONS[declaration] return _valid_heading(level, kind, binding, signature, root_kind) if ( level == 4 and unit_class_open and declaration in CLASS_DECLARATIONS ): kind, binding = CLASS_DECLARATIONS[declaration] return _valid_heading(level, kind, binding, signature, root_kind) return invalid_heading( level, name, signature, signature, root_kind, "unit class 成员必须位于所属 class 的 H4", ) return invalid_heading( level, name, signature, signature, root_kind, "function 顶级 API 不能包含 API 成员", ) def collect_headings_and_boundaries(lines): flags = fence_flags(lines) headings = [] h2_boundaries = [] root_kind = "" unit_class_open = False for index, line in enumerate(lines): if flags[index]: continue if PLAIN_H2_RE.match(line): h2_boundaries.append(index) root_kind = "" unit_class_open = False top_level = TOP_LEVEL_RE.match(line) if not top_level: continue signature = top_level.group(1) end = heading_body_end(lines, index, flags) declaration, error = _declaration( lines, index, end, flags ) heading = top_level_heading(signature, declaration, error) if heading.valid: root_kind = heading.kind headings.append((index, heading)) continue if not root_kind: continue heading_match = HEADING_RE.match(line) if root_kind == "unit" and heading_match: if len(heading_match.group(1)) == 3: unit_class_open = False legacy = LEGACY_TYPED_RE.match(line) if legacy: level = len(legacy.group(1)) signature = legacy.group(3) headings.append( ( index, invalid_heading( level, simple_name(signature), signature, line.lstrip("# "), root_kind, "API 标题只写名称或调用签名,声明种类写在声明行", ), ) ) continue bare = BARE_API_RE.match(line) if not bare: continue level = len(bare.group(1)) signature = bare.group(2) end = heading_body_end(lines, index, flags) declaration, error = _declaration(lines, index, end, flags) heading = member_heading( level, declaration, signature, root_kind, unit_class_open, error, ) if root_kind == "unit" and level == 3: unit_class_open = heading.valid and heading.kind == "class" headings.append((index, heading)) return headings, h2_boundaries def collect_headings(lines): headings, _ = collect_headings_and_boundaries(lines) return headings def iter_api_entries(lines): headings, h2_boundaries = collect_headings_and_boundaries(lines) for index, (start, heading) in enumerate(headings): next_heading = ( headings[index + 1][0] if index + 1 < len(headings) else len(lines) ) boundary_index = bisect_right(h2_boundaries, start) next_h2 = ( h2_boundaries[boundary_index] if boundary_index < len(h2_boundaries) else len(lines) ) yield ApiEntry(heading, start, min(next_heading, next_h2)) def slug(text): return re.sub(r"[^a-z0-9_]", "", text.casefold())