#!/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 -> `` 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"") 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())