Generate TSL API Markdown from YAML or JSON into a configurable project scope.\nAdd file and directory lint modes, tags-aware indexing, and keyword search across tags and descriptions.\nBundle the toolkit through the playbook build and sync workflows.
180 lines
5.5 KiB
Python
180 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Query bundled TSL API reference data."""
|
|
import argparse
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ENTRY_RE = re.compile(r"^(#{2,3})(?!#)\s+`(.+?)`\s*$")
|
|
HEADING_RE = re.compile(r"^(#{1,6})(?!#)\s+")
|
|
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_TSV = SKILL_ROOT / "data" / "function_index.tsv"
|
|
DEFAULT_CODEGEN_ROOT = SKILL_ROOT / "references" / "codegen"
|
|
|
|
|
|
def find_tsv(start):
|
|
"""Walk upward from start and find a bundled or legacy function_index.tsv."""
|
|
for directory in [start, *start.parents]:
|
|
candidates = [
|
|
directory / "data" / "function_index.tsv",
|
|
directory / "function_index.tsv",
|
|
directory / "docs" / "tsl" / "codegen" / "function_index.tsv",
|
|
]
|
|
for candidate in candidates:
|
|
if candidate.is_file():
|
|
return candidate
|
|
return None
|
|
|
|
|
|
def load_rows(tsv_path):
|
|
lines = tsv_path.read_text(encoding="utf-8").splitlines()
|
|
if not lines:
|
|
return []
|
|
header = lines[0].split("\t")
|
|
rows = []
|
|
for line in lines[1:]:
|
|
if not line.strip():
|
|
continue
|
|
values = line.split("\t")
|
|
values += [""] * (len(header) - len(values))
|
|
rows.append(dict(zip(header, values)))
|
|
return rows
|
|
|
|
|
|
def search_exact(rows, name):
|
|
key = name.lower()
|
|
return [row for row in rows if row.get("name", "").lower() == key]
|
|
|
|
|
|
def search_keyword(rows, terms):
|
|
lowered_terms = [term.lower() for term in terms]
|
|
matches = []
|
|
for row in rows:
|
|
haystack = " ".join(
|
|
[
|
|
row.get("name", ""),
|
|
row.get("signature", ""),
|
|
row.get("module", ""),
|
|
row.get("tags", ""),
|
|
row.get("summary", ""),
|
|
]
|
|
).lower()
|
|
if all(term in haystack for term in lowered_terms):
|
|
matches.append(row)
|
|
return matches
|
|
|
|
|
|
def slice_entry(codegen_root, page, signature):
|
|
md = codegen_root / page
|
|
if not md.is_file():
|
|
return ""
|
|
lines = md.read_text(encoding="utf-8").splitlines()
|
|
start = None
|
|
for idx, line in enumerate(lines):
|
|
match = ENTRY_RE.match(line)
|
|
if match and match.group(2) == signature:
|
|
start = idx
|
|
start_level = len(match.group(1))
|
|
break
|
|
if start is None:
|
|
return ""
|
|
|
|
end = len(lines)
|
|
for idx in range(start + 1, len(lines)):
|
|
entry_match = ENTRY_RE.match(lines[idx])
|
|
if entry_match:
|
|
end = idx
|
|
break
|
|
heading_match = HEADING_RE.match(lines[idx])
|
|
if heading_match and len(heading_match.group(1)) <= start_level:
|
|
end = idx
|
|
break
|
|
block = lines[start:end]
|
|
while block and not block[-1].strip():
|
|
block.pop()
|
|
return "\n".join(block)
|
|
|
|
|
|
def format_row(row):
|
|
return "\t".join(
|
|
[
|
|
row.get("name", ""),
|
|
row.get("signature", ""),
|
|
f"{row.get('page', '')}#{row.get('anchor', '')}",
|
|
row.get("summary", ""),
|
|
]
|
|
)
|
|
|
|
|
|
def codegen_root_for_tsv(tsv_path):
|
|
if tsv_path.parent.name == "data":
|
|
candidate = tsv_path.parent.parent / "references" / "codegen"
|
|
if candidate.is_dir():
|
|
return candidate
|
|
if tsv_path.parent.name == "codegen":
|
|
return tsv_path.parent
|
|
candidate = tsv_path.parent / "references" / "codegen"
|
|
if candidate.is_dir():
|
|
return candidate
|
|
return DEFAULT_CODEGEN_ROOT
|
|
|
|
|
|
def default_tsv_path():
|
|
if DEFAULT_TSV.is_file():
|
|
return DEFAULT_TSV
|
|
return find_tsv(Path.cwd())
|
|
|
|
|
|
def main(argv=None):
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
parser = argparse.ArgumentParser(description="Query TSL API reference facts.")
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
group.add_argument("--name", help="exact API/function name")
|
|
group.add_argument("--kw", nargs="+", help="keyword terms (AND)")
|
|
parser.add_argument("--tsv", help="explicit path to function_index.tsv")
|
|
parser.add_argument("--limit", type=int, default=50)
|
|
args = parser.parse_args(argv)
|
|
|
|
tsv_path = Path(args.tsv) if args.tsv else default_tsv_path()
|
|
if not tsv_path or not tsv_path.is_file():
|
|
print(
|
|
"ERROR: function_index.tsv not found; reinstall tsl-api-reference "
|
|
"or pass --tsv PATH",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
rows = load_rows(tsv_path)
|
|
codegen_root = codegen_root_for_tsv(tsv_path)
|
|
|
|
if args.name:
|
|
matches = search_exact(rows, args.name)
|
|
if not matches:
|
|
print(f"No TSL API named '{args.name}'. Try: --kw <中文关键词>")
|
|
return 0
|
|
for row in matches:
|
|
body = slice_entry(codegen_root, row["page"], row["signature"])
|
|
if body:
|
|
print(body)
|
|
else:
|
|
print(f"### `{row['signature']}`\n{row.get('summary', '')}")
|
|
print(
|
|
f"\n<!-- {row['scope']}/{row['module']} · "
|
|
f"{row['page']}#{row['anchor']} -->\n"
|
|
)
|
|
return 0
|
|
|
|
matches = search_keyword(rows, args.kw)
|
|
print(f"# {len(matches)} matches for: {' '.join(args.kw)}")
|
|
print("name\tsignature\tpage#anchor\tsummary")
|
|
for row in matches[: args.limit]:
|
|
print(format_row(row))
|
|
if len(matches) > args.limit:
|
|
print(f"... {len(matches) - args.limit} more; refine keywords or raise --limit")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|