feat(tsl-codegen): support unified API declarations

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.
This commit is contained in:
csh
2026-07-29 15:45:50 +08:00
parent bd25835d2d
commit 9edc8fc868
13 changed files with 8438 additions and 310 deletions
+139 -34
View File
@@ -1,23 +1,28 @@
#!/usr/bin/env python3
"""Rebuild the bundled TSL API function index from the codegen markdown tree.
"""Rebuild the bundled TSL API index from the codegen markdown tree.
The markdown tree is the source of truth: each `## `sig`` / `### `sig`` heading
is one function entry. The TSV is a derived product; regenerate it whenever the
leaf Markdown changes rather than editing it by hand.
The markdown tree is the source of truth. The TSV is a derived product;
regenerate it whenever the leaf Markdown changes rather than editing it by
hand.
Columns (tab-separated, LF line endings, UTF-8):
name scope module signature page anchor tags summary
kind binding visibility owner qualified_name
- name: signature text up to the first '('
- scope: first path segment under the codegen root (for example project)
- module: second path segment for nested pages, else the flat file stem
- signature: verbatim from the heading, backticks stripped
- page: POSIX path relative to the codegen root
- anchor: GitHub-style slug of the name (lowercased, chars outside
[a-z0-9_] removed); per-page duplicate slugs get -1/-2 suffixes
in document order, matching the rendered heading anchors.
- anchor: top-level declarations use the historic name slug; typed members
slug the complete visible API title. Per-page duplicate slugs get
-1/-2 suffixes in document order.
- tags: space-separated keywords from `<!-- tags: ... -->`
- summary: first prose line under the entry heading, empty for table,
heading, or standalone return-type lines
- summary: first prose line under the entry heading
- kind: function/class/method/property/field/constant/unit/variable
- binding: instance/class/static/unit, or empty when not applicable
- visibility: public/protected for class members; public for unit interface
- owner: dot-separated containing API path, excluding the entry name
- qualified_name: dot-separated stable API identity
Usage (run from repo root; --skill-dir is required):
SKILL=skills/tsl-api-reference
@@ -31,9 +36,22 @@ import sys
from pathlib import Path
import re
ENTRY_RE = re.compile(r"^#{2,3}(?!#)\s+`(.+?)`\s*$")
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from api_markdown import DECLARATION_LINE_RE, iter_api_entries, slug
RETURN_RE = re.compile(r"^返回[:]")
DECLARATION_TYPE_RE = re.compile(
r"^类型[:]\s*(function|class|unit)\s*$", re.IGNORECASE
)
TAGS_RE = re.compile(r"^<!--\s*tags:\s*(.*?)\s*-->$")
VISIBILITY_RE = re.compile(
r"^可见性[:]\s*`?(public|protected|private)`?\s*$",
re.IGNORECASE,
)
HEADER = [
"name",
"scope",
@@ -43,29 +61,56 @@ HEADER = [
"anchor",
"tags",
"summary",
"kind",
"binding",
"visibility",
"owner",
"qualified_name",
]
def slug(name):
"""GitHub-style anchor slug: lowercase, keep [a-z0-9_], drop the rest."""
return re.sub(r"[^a-z0-9_]", "", name.lower())
def extract_metadata(lines, heading_idx):
"""Return tags and the first prose line under a function entry heading."""
def extract_metadata(lines, heading_idx, end_idx=None):
"""Return tags and the first prose line under one API heading."""
tags = ""
for line in lines[heading_idx + 1:]:
summary = ""
summary_open = True
for line in lines[heading_idx + 1:end_idx]:
text = line.strip()
if not text:
continue
if DECLARATION_LINE_RE.fullmatch(text):
continue
tag_match = TAGS_RE.match(text)
if tag_match:
tags = " ".join(tag_match.group(1).split()).replace("\t", " ")
continue
if text.startswith("|") or text.startswith("#") or RETURN_RE.match(text):
return tags, ""
return tags, text.replace("\t", " ")
return tags, ""
if summary:
continue
if (
text.startswith("|")
or text.startswith("#")
or RETURN_RE.match(text)
or DECLARATION_TYPE_RE.match(text)
):
summary_open = False
continue
if summary_open:
summary = text.replace("\t", " ")
return tags, summary
def extract_visibility(lines, start, end):
for line in lines[start + 1:end]:
match = VISIBILITY_RE.match(line.strip())
if match:
return match.group(1).casefold()
return ""
def next_anchor(base, seen):
count = seen.get(base, 0)
seen[base] = count + 1
return base if count == 0 else f"{base}-{count}"
def parse_page(codegen_root, md):
@@ -75,18 +120,69 @@ def parse_page(codegen_root, md):
seen = {}
rows = []
lines = md.read_text(encoding="utf-8").splitlines()
for idx, line in enumerate(lines):
m = ENTRY_RE.match(line)
if not m:
entries = list(iter_api_entries(lines))
root_kind = ""
root_name = ""
unit_class_owner = ""
for entry in entries:
heading = entry.heading
if not heading.valid:
continue
sig = m.group(1)
name = sig.split("(", 1)[0]
base = slug(name)
n = seen.get(base, 0)
seen[base] = n + 1
anchor = base if n == 0 else f"{base}-{n}"
tags, summary = extract_metadata(lines, idx)
rows.append([name, scope, module, sig, page, anchor, tags, summary])
if heading.level == 2:
root_kind = heading.kind
root_name = heading.name
unit_class_owner = ""
owner = ""
anchor_base = slug(heading.name)
elif root_kind == "class":
owner = root_name
anchor_base = slug(heading.visible_title)
elif root_kind == "unit" and heading.level == 3:
anchor_base = slug(heading.visible_title)
owner = root_name
unit_class_owner = (
f"{root_name}.{heading.name}"
if heading.kind == "class"
else ""
)
elif root_kind == "unit" and heading.level == 4:
owner = unit_class_owner
anchor_base = slug(heading.visible_title)
else:
continue
qualified_name = (
f"{owner}.{heading.name}" if owner else heading.name
)
if root_kind == "unit" and heading.level == 3:
visibility = "public"
elif (
root_kind == "class" and heading.level == 3
) or (root_kind == "unit" and heading.level == 4):
visibility = extract_visibility(lines, entry.start, entry.end)
else:
visibility = ""
tags, summary = extract_metadata(lines, entry.start, entry.end)
rows.append(
[
heading.name,
scope,
module,
heading.signature,
page,
next_anchor(anchor_base, seen),
tags,
summary,
heading.kind,
heading.binding,
visibility,
owner,
qualified_name,
]
)
return rows
@@ -120,7 +216,16 @@ def read_tsv(tsv_path):
def main(argv=None):
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser = argparse.ArgumentParser(
description=__doc__.splitlines()[0],
add_help=False,
allow_abbrev=False,
)
parser.add_argument(
"--help",
action="help",
help="show this help message and exit (no -h short option)",
)
parser.add_argument(
"--skill-dir",
required=True,