✨ feat(tsl-codegen): support unified API declarations
Add TSF conversion and shared Markdown recognition for mixed function, class, and unit declarations. Extend generation, linting, indexing, examples, and tests around the unified declaration model.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user