📦 deps(tsl): sync tsl-playbook from 13be5ea0

Source-Commit: 13be5ea0aa
This commit is contained in:
ci[bot]
2026-07-29 07:58:05 +00:00
parent 9d2b696f58
commit 80357f4bc5
280 changed files with 48658 additions and 13929 deletions
+605 -51
View File
@@ -2,17 +2,20 @@
"""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.
an ordered `declarations` list containing function, class, or unit entries. 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.
Rendered Markdown is passed through the repository-pinned Prettier before it is
written, keeping generated pages consistent with the existing codegen tree.
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):
Function declaration fields:
kind required `function`
name required must match the signature name case-insensitively
signature required verbatim, underscores/case untouched
desc required description; may contain multiple lines
tags optional list of Chinese keywords -> `<!-- tags: ... -->`
@@ -27,11 +30,20 @@ Usage (run from repo root):
python tools/tsl-codegen/scripts/generate.py entry.json \
--scope my-project
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
PRETTIER_CONFIG = REPO_ROOT / ".prettierrc.json"
def die(msg):
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(1)
@@ -53,10 +65,7 @@ def resolve_format(path, fmt):
return "json"
if suffix in (".yml", ".yaml"):
return "yaml"
die(
f"cannot infer format from extension '{suffix}'; "
f"pass --format json|yaml"
)
die(f"cannot infer format from extension '{suffix}'; " f"pass --format json|yaml")
def load_entries(path, fmt=None):
@@ -96,6 +105,322 @@ def require(cond, msg):
die(msg)
def require_mapping(value, where):
require(isinstance(value, dict), f"{where}: must be a mapping")
def reject_unknown(mapping, allowed, where):
unknown = sorted(set(mapping) - set(allowed))
require(not unknown, f"{where}: unknown field(s): {', '.join(unknown)}")
def non_empty_string(value, where):
require(isinstance(value, str) and value.strip(), f"{where}: must be non-empty")
def optional_draft_string(value, where):
if value == "":
return
non_empty_string(value, where)
def validate_tags(tags, where):
if tags is None:
return
require(isinstance(tags, list) and tags, f"{where}: tags must be a non-empty list")
for index, tag in enumerate(tags):
non_empty_string(tag, f"{where}: tags[{index}]")
def signature_names(signature, where):
non_empty_string(signature, f"{where}: signature")
left = signature.find("(")
right = signature.rfind(")")
require(left > 0 and right == len(signature) - 1, f"{where}: invalid signature")
name = signature[:left]
non_empty_string(name, f"{where}: signature name")
raw = signature[left + 1 : right].strip()
if not raw:
return name, []
names = [item.strip() for item in raw.split(",")]
require(all(names), f"{where}: signature contains an empty parameter")
require(len({item.casefold() for item in names}) == len(names), f"{where}: duplicate parameter name")
return name, names
def validate_values(values, where):
require(isinstance(values, list) and values, f"{where}: values must be a non-empty list")
for index, item in enumerate(values):
item_where = f"{where}[{index}]"
require_mapping(item, item_where)
reject_unknown(item, {"value", "desc"}, item_where)
require("value" in item, f"{item_where}: missing 'value'")
non_empty_string(item.get("desc"), f"{item_where}: desc")
def validate_params(params, expected_names, where):
if not expected_names:
require(not params, f"{where}: nullary signature must not have params")
return
require(isinstance(params, list), f"{where}: params must be a list")
require(len(params) == len(expected_names), f"{where}: params do not match signature")
actual_names = []
for index, param in enumerate(params):
param_where = f"{where}: params[{index}]"
require_mapping(param, param_where)
reject_unknown(param, {"name", "type", "desc", "optional", "values"}, param_where)
name = param.get("name")
non_empty_string(name, f"{param_where}: name")
non_empty_string(param.get("type"), f"{param_where}: type")
non_empty_string(param.get("desc"), f"{param_where}: desc")
if "optional" in param:
require(isinstance(param["optional"], bool), f"{param_where}: optional must be boolean")
if "values" in param:
validate_values(param["values"], f"{param_where}: values")
actual_names.append(name)
require(
[name.casefold() for name in actual_names]
== [name.casefold() for name in expected_names],
f"{where}: params must follow signature order",
)
def validate_examples(examples, where):
require(isinstance(examples, list) and examples, f"{where}: examples must be a non-empty list")
for index, example in enumerate(examples):
example_where = f"{where}: examples[{index}]"
require_mapping(example, example_where)
reject_unknown(example, {"desc", "code", "output"}, example_where)
non_empty_string(example.get("desc"), f"{example_where}: desc")
non_empty_string(example.get("code"), f"{example_where}: code")
if "output" in example:
non_empty_string(example["output"], f"{example_where}: output")
def validate_function(fn, where, *, returns_required, extra_fields=()):
require_mapping(fn, where)
allowed = {
"signature",
"desc",
"tags",
"params",
"returns",
"example",
"examples",
*extra_fields,
}
reject_unknown(fn, allowed, where)
name, names = signature_names(fn.get("signature"), where)
non_empty_string(fn.get("desc"), f"{where}: desc")
validate_tags(fn.get("tags"), where)
validate_params(fn.get("params"), names, where)
if returns_required:
non_empty_string(fn.get("returns"), f"{where}: missing 'returns'")
elif "returns" in fn:
optional_draft_string(fn["returns"], f"{where}: returns")
require(not ("example" in fn and "examples" in fn), f"{where}: use example or examples, not both")
if "example" in fn:
non_empty_string(fn["example"], f"{where}: example")
if "examples" in fn:
validate_examples(fn["examples"], where)
return name
def validate_class_member(member, where):
require_mapping(member, where)
kind = member.get("kind")
require(kind in {"method", "property", "field", "constant"}, f"{where}: unknown kind '{kind}'")
non_empty_string(member.get("name"), f"{where}: name")
visibility = member.get("visibility")
require(visibility in {"public", "protected"}, f"{where}: visibility must be public or protected")
non_empty_string(member.get("desc"), f"{where}: desc")
validate_tags(member.get("tags"), where)
if kind == "method":
reject_unknown(
member,
{
"kind", "name", "visibility", "binding", "signature", "desc",
"tags", "params", "returns", "modifiers", "example", "examples",
},
where,
)
require(member.get("binding") in {"instance", "class"}, f"{where}: invalid binding")
parsed_name = validate_function(
member,
where,
returns_required=False,
extra_fields={"kind", "name", "visibility", "binding", "modifiers"},
)
require(parsed_name.casefold() == member["name"].casefold(), f"{where}: name and signature differ")
if "modifiers" in member:
modifiers = member["modifiers"]
require(isinstance(modifiers, list), f"{where}: modifiers must be a list")
allowed = {"overload", "virtual", "override"}
require(all(item in allowed for item in modifiers), f"{where}: invalid modifier")
require(len(set(modifiers)) == len(modifiers), f"{where}: duplicate modifier")
return
common = {"kind", "name", "visibility", "desc", "tags"}
if kind == "property":
reject_unknown(member, common | {"type", "params", "access"}, where)
if "type" in member:
optional_draft_string(member["type"], f"{where}: type")
require(member.get("access") in {"read", "write", "readwrite"}, f"{where}: invalid access")
params = member.get("params")
if params:
expected = [param.get("name") for param in params]
validate_params(params, expected, where)
return
if kind == "field":
reject_unknown(member, common | {"type", "static"}, where)
non_empty_string(member.get("type"), f"{where}: type")
if "static" in member:
require(isinstance(member["static"], bool), f"{where}: static must be boolean")
return
reject_unknown(member, common | {"type", "value", "static"}, where)
require("value" in member and member["value"] is not None, f"{where}: missing 'value'")
if isinstance(member["value"], str):
non_empty_string(member["value"], f"{where}: value")
if "type" in member:
optional_draft_string(member["type"], f"{where}: type")
if "static" in member:
require(isinstance(member["static"], bool), f"{where}: static must be boolean")
def validate_class(cls, where):
require_mapping(cls, where)
reject_unknown(cls, {"kind", "name", "desc", "tags", "bases", "members"}, where)
require(cls.get("kind") == "class", f"{where}: kind must be class")
non_empty_string(cls.get("name"), f"{where}: name")
non_empty_string(cls.get("desc"), f"{where}: desc")
validate_tags(cls.get("tags"), where)
if "bases" in cls:
require(isinstance(cls["bases"], list), f"{where}: bases must be a list")
for index, base in enumerate(cls["bases"]):
non_empty_string(base, f"{where}: bases[{index}]")
require(isinstance(cls.get("members"), list), f"{where}: members must be a list")
for index, member in enumerate(cls["members"]):
validate_class_member(member, f"{where}: members[{index}]")
def validate_unit_member(member, where):
require_mapping(member, where)
kind = member.get("kind")
require(kind in {"function", "variable", "constant", "class"}, f"{where}: unknown kind '{kind}'")
if kind == "class":
validate_class(member, where)
return
non_empty_string(member.get("name"), f"{where}: name")
non_empty_string(member.get("desc"), f"{where}: desc")
validate_tags(member.get("tags"), where)
if kind == "function":
parsed_name = validate_function(
member,
where,
returns_required=True,
extra_fields={"kind", "name"},
)
require(parsed_name.casefold() == member["name"].casefold(), f"{where}: name and signature differ")
return
common = {"kind", "name", "desc", "tags", "type"}
if kind == "variable":
reject_unknown(member, common, where)
non_empty_string(member.get("type"), f"{where}: type")
return
reject_unknown(member, common | {"value"}, where)
require("value" in member and member["value"] is not None, f"{where}: missing 'value'")
if isinstance(member["value"], str):
non_empty_string(member["value"], f"{where}: value")
if "type" in member:
optional_draft_string(member["type"], f"{where}: type")
def validate_unit(unit, where):
require_mapping(unit, where)
reject_unknown(unit, {"kind", "name", "desc", "tags", "members"}, where)
require(unit.get("kind") == "unit", f"{where}: kind must be unit")
non_empty_string(unit.get("name"), f"{where}: name")
non_empty_string(unit.get("desc"), f"{where}: desc")
validate_tags(unit.get("tags"), where)
require(
isinstance(unit.get("members"), list),
f"{where}: members must be a list",
)
for index, member in enumerate(unit["members"]):
validate_unit_member(member, f"{where}: members[{index}]")
def validate_top_level_function(declaration, where):
require(
declaration.get("kind") == "function",
f"{where}: kind must be function",
)
non_empty_string(declaration.get("name"), f"{where}: name")
parsed_name = validate_function(
declaration,
where,
returns_required=True,
extra_fields={"kind", "name"},
)
require(
parsed_name.casefold() == declaration["name"].casefold(),
f"{where}: name and signature differ",
)
def validate_declaration(declaration, where):
require_mapping(declaration, where)
kind = declaration.get("kind")
require(
kind in {"function", "class", "unit"},
f"{where}: unknown kind '{kind}'",
)
if kind == "function":
validate_top_level_function(declaration, where)
elif kind == "class":
validate_class(declaration, where)
else:
validate_unit(declaration, where)
def validate_declaration_uniqueness(declarations):
function_signatures = set()
class_names = set()
unit_names = set()
for index, declaration in enumerate(declarations):
where = f"declarations[{index}]"
kind = declaration["kind"]
if kind == "function":
key = declaration["signature"].casefold()
require(
key not in function_signatures,
f"{where}: duplicate function signature",
)
function_signatures.add(key)
continue
names = class_names if kind == "class" else unit_names
key = declaration["name"].casefold()
require(key not in names, f"{where}: duplicate {kind} name")
names.add(key)
def validate_page(data):
require_mapping(data, "input root")
reject_unknown(data, {"module", "path", "declarations"}, "input root")
non_empty_string(data.get("module"), "input root: module")
non_empty_string(data.get("path"), "input root: path")
declarations = data.get("declarations")
require(
isinstance(declarations, list) and declarations,
"input root: declarations must be a non-empty list",
)
for index, declaration in enumerate(declarations):
validate_declaration(declaration, f"declarations[{index}]")
validate_declaration_uniqueness(declarations)
return declarations
def param_desc(param, where):
"""Description column text: prepend `可选。` for optional params."""
desc = param.get("desc")
@@ -120,14 +445,17 @@ def render_param_table(params, where):
return lines
def render_enum_sections(params):
"""`**name 取值**` sections for every param carrying a `values` list."""
def render_enum_sections(params, heading_level=None):
"""Render value sections, preserving legacy function-page headings."""
lines = []
for param in params:
values = param.get("values")
if not values:
continue
lines.append(f"**{param['name']} 取值**")
if heading_level is None:
lines.append(f"**{param['name']} 取值**")
else:
lines.append(f"{'#' * heading_level} `{param['name']}` 取值")
lines.append("")
for item in values:
lines.append(f"- `{item['value']}` — {item['desc']}")
@@ -135,23 +463,89 @@ def render_enum_sections(params):
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'")
def render_examples(fn, heading_level):
lines = []
if "examples" in fn:
lines.extend([f"{'#' * heading_level} 示例", ""])
for index, example in enumerate(fn["examples"], start=1):
lines.append(f"范例{index:02d}{example['desc']}")
lines.append("")
lines.append("```tsl")
lines.extend(example["code"].rstrip("\n").split("\n"))
output = example.get("output")
if output is not None:
output_lines = output.split("\n")
if len(output_lines) == 1:
lines.append(f"// 输出:{output_lines[0]}")
else:
lines.append("// 输出:")
lines.extend(f"// {line}" if line else "//" for line in output_lines)
lines.append("```")
lines.append("")
while lines and not lines[-1]:
lines.pop()
return lines
if fn.get("example"):
return [
f"{'#' * heading_level} 示例",
"",
"```tsl",
*fn["example"].rstrip("\n").split("\n"),
"```",
]
return []
lines = [f"## `{sig}`"]
tags = fn.get("tags")
def render_intro(heading, declaration, item):
lines = [heading, "", f"声明:{declaration}", "", item["desc"], ""]
tags = item.get("tags")
if tags:
lines.append(f"<!-- tags: {' '.join(str(t) for t in tags)} -->")
lines.append("")
lines.append(desc)
lines.append("")
lines.extend([f"<!-- tags: {' '.join(tags)} -->", ""])
return lines
def render_callable(
fn,
signature,
declaration,
where,
*,
level,
returns_required,
show_visibility,
):
validate_function(
fn,
where,
returns_required=returns_required,
extra_fields={"kind", "name", "visibility", "binding", "modifiers"},
)
lines = render_intro(
f"{'#' * level} `{signature}`", declaration, fn
)
if show_visibility:
lines.extend([f"可见性:`{fn['visibility']}`", ""])
if fn.get("modifiers"):
rendered = "".join(f"`{item}`" for item in fn["modifiers"])
lines.extend([f"修饰符:{rendered}", ""])
params = fn.get("params") or []
if params:
lines.extend(render_param_table(params, where))
lines.append("")
lines.extend(render_enum_sections(params, level + 1))
if fn.get("returns"):
lines.append(f"返回:{fn['returns']}")
example_lines = render_examples(fn, level + 1)
if example_lines:
lines.extend(["", *example_lines])
return lines
def render_top_level_function(fn, index):
"""Render one function entry to a list of lines (no trailing blank)."""
where = f"declarations[{index}]"
sig = fn["signature"]
lines = render_intro(f"## `{sig}`", "function", fn)
params = fn.get("params") or []
if params:
@@ -159,34 +553,168 @@ def render_function(fn, index):
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("```")
lines.append(f"返回:{fn['returns']}")
examples = render_examples(fn, 3)
if examples:
lines.extend(["", *examples])
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'")
def render_class_member(member, level, where):
kind = member["kind"]
if kind == "method":
declaration = (
"class function"
if member["binding"] == "class"
else "function"
)
return render_callable(
member,
member["signature"],
declaration,
where,
level=level,
returns_required=False,
show_visibility=True,
)
static_prefix = "static " if member.get("static") else ""
declaration = {
"property": "property",
"field": f"{static_prefix}field",
"constant": f"{static_prefix}const",
}[kind]
signature = member["name"]
if kind == "property" and member.get("params"):
signature += "(" + ", ".join(param["name"] for param in member["params"]) + ")"
lines = render_intro(
f"{'#' * level} `{signature}`", declaration, member
)
lines.extend([f"可见性:`{member['visibility']}`", ""])
if kind == "property":
if member.get("type"):
lines.extend([f"类型:{member['type']}", ""])
access = {"read": "read", "write": "write", "readwrite": "read / write"}[member["access"]]
lines.append(f"访问:{access}")
params = member.get("params") or []
if params:
lines.extend(["", *render_param_table(params, where), ""])
lines.extend(render_enum_sections(params, level + 1))
elif kind == "field":
lines.append(f"类型:{member['type']}")
else:
if member.get("type"):
lines.extend([f"类型:{member['type']}", ""])
lines.append(f"值:`{render_scalar(member['value'])}`")
return lines
out = [f"# {module}", ""]
for index, fn in enumerate(functions):
out.extend(render_function(fn, index))
def render_class(cls, level, where, *, page_root):
lines = render_intro(
f"{'#' * level} `{cls['name']}`", "class", cls
)
if cls.get("bases"):
lines.extend(["父类:" + "".join(f"`{base}`" for base in cls["bases"]), ""])
for index, member in enumerate(cls["members"]):
lines.extend(render_class_member(member, level + 1, f"{where}: members[{index}]"))
lines.append("")
while lines and not lines[-1]:
lines.pop()
return lines
def render_unit_member(member, index, unit_where):
where = f"{unit_where}: members[{index}]"
kind = member["kind"]
if kind == "function":
return render_callable(
member,
member["signature"],
"function",
where,
level=3,
returns_required=True,
show_visibility=False,
)
if kind == "class":
return render_class(member, 3, where, page_root=False)
declaration = "var" if kind == "variable" else "const"
lines = render_intro(
f"### `{member['name']}`", declaration, member
)
if kind == "variable":
lines.append(f"类型:{member['type']}")
else:
if member.get("type"):
lines.extend([f"类型:{member['type']}", ""])
lines.append(f"值:`{render_scalar(member['value'])}`")
return lines
def render_unit(unit, where):
lines = render_intro(f"## `{unit['name']}`", "unit", unit)
for index, member in enumerate(unit["members"]):
lines.extend(render_unit_member(member, index, where))
lines.append("")
while lines and not lines[-1]:
lines.pop()
return lines
def render_scalar(value):
if isinstance(value, str):
return value
return json.dumps(value, ensure_ascii=False)
def render_declaration(declaration, index):
kind = declaration["kind"]
where = f"declarations[{index}]"
if kind == "function":
return render_top_level_function(declaration, index)
if kind == "class":
return render_class(declaration, 2, where, page_root=True)
return render_unit(declaration, where)
def render_page(data):
"""Validate and render a whole leaf page."""
declarations = validate_page(data)
out = [f"# {data['module']}", ""]
for index, declaration in enumerate(declarations):
out.extend(render_declaration(declaration, index))
out.append("")
return "\n".join(out).rstrip("\n") + "\n"
def format_markdown(text):
"""Format generated Markdown with the repository-pinned Prettier."""
npx = shutil.which("npx")
if not npx:
die("未找到 Prettier;请先在仓库根目录运行 `npm install`")
result = subprocess.run(
[
npx,
"--no-install",
"prettier",
"--config",
str(PRETTIER_CONFIG),
"--parser",
"markdown",
],
input=text,
capture_output=True,
text=True,
encoding="utf-8",
cwd=REPO_ROOT,
check=False,
)
if result.returncode != 0:
detail = result.stderr.strip() or "未知错误"
die(f"Prettier 格式化失败:{detail}")
return result.stdout
def output_path(data, scope):
"""Build the leaf-page destination from the recording file's relative path."""
relative = data.get("path")
@@ -204,10 +732,37 @@ def output_path(data, scope):
)
def atomic_write(path, text):
"""Atomically replace path and remove the temporary file on failure."""
path.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(
prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
)
temporary_path = Path(temporary_name)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary_path, path)
finally:
if temporary_path.exists():
temporary_path.unlink()
def main(argv=None):
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="从 YAML/JSON 录入文件生成 TSL 函数文档")
parser = argparse.ArgumentParser(
description="从 YAML/JSON 录入文件生成 TSL API 文档",
add_help=False,
allow_abbrev=False,
)
parser.add_argument(
"--help",
action="help",
help="显示本帮助并退出(不提供 -h 短选项)",
)
parser.add_argument(
"input",
metavar="INPUT_FILE",
@@ -231,10 +786,9 @@ def main(argv=None):
die(f"input not found: {in_path}")
data = load_entries(in_path, args.format)
text = render_page(data)
text = format_markdown(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")
atomic_write(out_path, text)
print(
f"wrote {out_path}",
file=sys.stderr,