Add TSF conversion and shared Markdown recognition for mixed function, class, and unit declarations. Extend generation, linting, indexing, examples, and tests around the unified declaration model.
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())
|