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.
247 lines
8.0 KiB
Python
247 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate compliant TSL codegen markdown from a YAML/JSON entry file.
|
|
|
|
The recording format is one leaf page: a `module` title, a relative `path`, and
|
|
a `functions` list. This script renders it to the markdown the codegen tree
|
|
stores, matching tools/tsl-codegen/STANDARD.md.
|
|
|
|
Tables are emitted as valid Markdown with single-space padding. Prettier may be
|
|
used optionally to align columns.
|
|
|
|
Input dispatch is by extension: .json parses with the stdlib (keeping the
|
|
toolchain dependency-free); .yml/.yaml needs pyyaml. If pyyaml is missing the
|
|
script says so and points at the JSON path.
|
|
|
|
Entry schema (per function):
|
|
signature required verbatim, underscores/case untouched
|
|
desc required description; may contain multiple lines
|
|
tags optional list of Chinese keywords -> `<!-- tags: ... -->`
|
|
params required when the signature takes args; omit for nullary
|
|
returns required return type
|
|
example optional fenced tsl block, pasted verbatim
|
|
Each param: name/type/desc required; optional (bool) -> `可选。` prefix;
|
|
values (list of {value, desc}) -> a `**name 取值**` enum section.
|
|
|
|
Usage (run from repo root):
|
|
python tools/tsl-codegen/scripts/generate.py entry.yml
|
|
python tools/tsl-codegen/scripts/generate.py entry.json \
|
|
--scope my-project
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
def die(msg):
|
|
print(f"ERROR: {msg}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def scope_name(value):
|
|
"""Validate a user-defined single directory name."""
|
|
if not value or value in {".", ".."} or "/" in value or "\\" in value:
|
|
raise argparse.ArgumentTypeError("scope 必须是单级目录名")
|
|
return value
|
|
|
|
|
|
def resolve_format(path, fmt):
|
|
"""Pick the parser: explicit --format wins, else derive from extension."""
|
|
if fmt:
|
|
return fmt
|
|
suffix = path.suffix.lower()
|
|
if suffix == ".json":
|
|
return "json"
|
|
if suffix in (".yml", ".yaml"):
|
|
return "yaml"
|
|
die(
|
|
f"cannot infer format from extension '{suffix}'; "
|
|
f"pass --format json|yaml"
|
|
)
|
|
|
|
|
|
def load_entries(path, fmt=None):
|
|
"""Parse a recording file as JSON (stdlib) or YAML (pyyaml).
|
|
|
|
Format is chosen by --format when given, else by file extension.
|
|
"""
|
|
text = path.read_text(encoding="utf-8")
|
|
fmt = resolve_format(path, fmt)
|
|
if fmt == "json":
|
|
try:
|
|
return json.loads(text)
|
|
except json.JSONDecodeError as exc:
|
|
die(f"invalid JSON in {path}: {exc}")
|
|
if fmt == "yaml":
|
|
try:
|
|
import yaml
|
|
except ImportError:
|
|
die(
|
|
"pyyaml is not installed; either `pip install pyyaml` or "
|
|
"convert the input to .json (json parses with the stdlib)"
|
|
)
|
|
try:
|
|
return yaml.safe_load(text)
|
|
except yaml.YAMLError as exc:
|
|
die(f"invalid YAML in {path}: {exc}")
|
|
die(f"unknown format '{fmt}'; use json or yaml")
|
|
|
|
|
|
def escape_cell(text):
|
|
"""Escape `|` so a value stays inside one markdown table cell."""
|
|
return str(text).replace("|", "\\|")
|
|
|
|
|
|
def require(cond, msg):
|
|
if not cond:
|
|
die(msg)
|
|
|
|
|
|
def param_desc(param, where):
|
|
"""Description column text: prepend `可选。` for optional params."""
|
|
desc = param.get("desc")
|
|
require(desc, f"{where}: param '{param.get('name', '?')}' missing desc")
|
|
if param.get("optional") and not desc.startswith("可选。"):
|
|
return "可选。" + desc
|
|
return desc
|
|
|
|
|
|
def render_param_table(params, where):
|
|
"""Three-column 参数/类型/说明 table with single-space padding."""
|
|
lines = ["| 参数 | 类型 | 说明 |", "| --- | --- | --- |"]
|
|
for param in params:
|
|
name = param.get("name")
|
|
ptype = param.get("type")
|
|
require(name, f"{where}: a param is missing 'name'")
|
|
require(ptype, f"{where}: param '{name}' missing 'type'")
|
|
desc = param_desc(param, where)
|
|
lines.append(
|
|
f"| `{escape_cell(name)}` | {escape_cell(ptype)} | {escape_cell(desc)} |"
|
|
)
|
|
return lines
|
|
|
|
|
|
def render_enum_sections(params):
|
|
"""`**name 取值**` sections for every param carrying a `values` list."""
|
|
lines = []
|
|
for param in params:
|
|
values = param.get("values")
|
|
if not values:
|
|
continue
|
|
lines.append(f"**{param['name']} 取值**")
|
|
lines.append("")
|
|
for item in values:
|
|
lines.append(f"- `{item['value']}` — {item['desc']}")
|
|
lines.append("")
|
|
return lines
|
|
|
|
|
|
def render_function(fn, index):
|
|
"""Render one function entry to a list of lines (no trailing blank)."""
|
|
where = f"functions[{index}]"
|
|
sig = fn.get("signature")
|
|
require(sig, f"{where}: missing 'signature'")
|
|
desc = fn.get("desc")
|
|
require(desc, f"{where} ({sig}): missing 'desc'")
|
|
returns = fn.get("returns")
|
|
require(returns, f"{where} ({sig}): missing 'returns'")
|
|
|
|
lines = [f"## `{sig}`"]
|
|
tags = fn.get("tags")
|
|
if tags:
|
|
lines.append(f"<!-- tags: {' '.join(str(t) for t in tags)} -->")
|
|
lines.append("")
|
|
lines.append(desc)
|
|
lines.append("")
|
|
|
|
params = fn.get("params") or []
|
|
if params:
|
|
lines.extend(render_param_table(params, f"{where} ({sig})"))
|
|
lines.append("")
|
|
lines.extend(render_enum_sections(params))
|
|
|
|
lines.append(f"返回:{returns}")
|
|
|
|
example = fn.get("example")
|
|
if example:
|
|
lines.append("")
|
|
lines.append("### 示例")
|
|
lines.append("")
|
|
lines.append("```tsl")
|
|
lines.extend(example.rstrip("\n").split("\n"))
|
|
lines.append("```")
|
|
return lines
|
|
|
|
|
|
def render_page(data):
|
|
"""Render a whole leaf page: H1 + every function entry."""
|
|
require(isinstance(data, dict), "input root must be a mapping")
|
|
module = data.get("module")
|
|
require(module, "input missing 'module'")
|
|
functions = data.get("functions")
|
|
require(functions, "input missing non-empty 'functions'")
|
|
|
|
out = [f"# {module}", ""]
|
|
for index, fn in enumerate(functions):
|
|
out.extend(render_function(fn, index))
|
|
out.append("")
|
|
return "\n".join(out).rstrip("\n") + "\n"
|
|
|
|
|
|
def output_path(data, scope):
|
|
"""Build the leaf-page destination from the recording file's relative path."""
|
|
relative = data.get("path")
|
|
require(relative, "input missing 'path'")
|
|
require(isinstance(relative, str), "'path' must be a string")
|
|
require("\\" not in relative, "'path' must use '/' as the separator")
|
|
relative_path = Path(relative)
|
|
require(not relative_path.is_absolute(), "'path' must be relative")
|
|
require(".." not in relative_path.parts, "'path' must not contain '..'")
|
|
require(relative_path.suffix == "", "'path' must not include a file extension")
|
|
return (
|
|
Path("skills/tsl-api-reference/references/codegen")
|
|
/ scope
|
|
/ relative_path.with_suffix(".md")
|
|
)
|
|
|
|
|
|
def main(argv=None):
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
parser = argparse.ArgumentParser(description="从 YAML/JSON 录入文件生成 TSL 函数文档")
|
|
parser.add_argument(
|
|
"input",
|
|
metavar="INPUT_FILE",
|
|
help="YAML/JSON 录入文件路径,例如 tmp/my-functions.yaml",
|
|
)
|
|
parser.add_argument(
|
|
"--scope",
|
|
type=scope_name,
|
|
default="project",
|
|
help="codegen 下的一级目录,可自定义(默认:project)",
|
|
)
|
|
parser.add_argument(
|
|
"--format",
|
|
choices=["json", "yaml"],
|
|
help="录入文件格式;默认根据文件扩展名判断",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
in_path = Path(args.input)
|
|
if not in_path.is_file():
|
|
die(f"input not found: {in_path}")
|
|
data = load_entries(in_path, args.format)
|
|
|
|
text = render_page(data)
|
|
out_path = output_path(data, args.scope)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(text, encoding="utf-8", newline="\n")
|
|
print(
|
|
f"wrote {out_path}",
|
|
file=sys.stderr,
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|