@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user