360 lines
10 KiB
Python
360 lines
10 KiB
Python
"""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())
|