feat(tsl-codegen): support file and directory inputs

This commit is contained in:
csh
2026-07-30 13:44:50 +08:00
parent aa8a3e73a8
commit 1ae798bcaa
4 changed files with 791 additions and 312 deletions
+346 -105
View File
@@ -26,9 +26,10 @@ 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 \
python tools/tsl-codegen/scripts/generate.py --file entry.yml
python tools/tsl-codegen/scripts/generate.py --file entry.json \
--scope my-project
python tools/tsl-codegen/scripts/generate.py --dir recordings
"""
import argparse
@@ -38,15 +39,42 @@ import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from pathlib import Path, PureWindowsPath
REPO_ROOT = Path(__file__).resolve().parents[3]
PRETTIER_CONFIG = REPO_ROOT / ".prettierrc.json"
FORMAT_SUFFIXES = {
None: (".json", ".yaml", ".yml"),
"json": (".json",),
"yaml": (".yaml", ".yml"),
}
class ChineseArgumentParser(argparse.ArgumentParser):
def error(self, message):
replacements = (
("unrecognized arguments:", "无法识别的参数:"),
("the following arguments are required:", "缺少必需参数:"),
("expected one argument", "需要一个参数值"),
("invalid choice: ", "取值无效:"),
)
if message.startswith("argument "):
message = "参数 " + message[len("argument ") :]
for source, target in replacements:
message = message.replace(source, target)
choice_marker = " (choose from "
if choice_marker in message and message.endswith(")"):
message = message[:-1].replace(choice_marker, "(可选值:", 1) + ""
self.print_usage(sys.stderr)
self.exit(2, f"{self.prog}: 错误:{message}\n")
class GenerationError(Exception):
pass
def die(msg):
print(f"ERROR: {msg}", file=sys.stderr)
raise SystemExit(1)
raise GenerationError(msg)
def scope_name(value):
@@ -65,7 +93,29 @@ 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"无法根据扩展名 {suffix or '(无扩展名)'} 判断录入格式;"
"请使用 --format json 或 --format yaml"
)
def describe_json_error(exc):
translations = (
(
"Expecting property name enclosed in double quotes",
"对象属性名必须使用双引号",
),
("Expecting value", "此处缺少有效值"),
("Expecting ',' delimiter", "此处缺少逗号分隔符"),
("Extra data", "根值之后存在多余内容"),
("Unterminated string", "字符串未结束"),
("Invalid \\escape", "字符串中包含无效转义"),
("Invalid control character", "字符串中包含无效控制字符"),
)
for prefix, message in translations:
if exc.msg.startswith(prefix):
return message
return "语法无效"
def load_entries(path, fmt=None):
@@ -73,26 +123,40 @@ def load_entries(path, fmt=None):
Format is chosen by --format when given, else by file extension.
"""
text = path.read_text(encoding="utf-8")
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
die("文件不是有效的 UTF-8 编码")
except OSError as exc:
die(f"读取文件失败:{exc}")
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}")
die(
f"JSON 格式错误:第 {exc.lineno} 行,第 {exc.colno} 列:"
f"{describe_json_error(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)"
"未安装 pyyaml;请运行 `python -m pip install pyyaml`"
"或改用无需额外依赖的 JSON 录入文件"
)
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")
mark = getattr(exc, "problem_mark", None)
if mark is None:
die("YAML 格式错误:语法无效")
die(
f"YAML 格式错误:第 {mark.line + 1} 行,第 {mark.column + 1} 列:"
"语法无效"
)
die(f"不支持的录入格式:{fmt};只能使用 json 或 yaml")
def escape_cell(text):
@@ -106,16 +170,16 @@ def require(cond, msg):
def require_mapping(value, where):
require(isinstance(value, dict), f"{where}: must be a mapping")
require(isinstance(value, dict), f"{where}:必须是对象")
def reject_unknown(mapping, allowed, where):
unknown = sorted(set(mapping) - set(allowed))
require(not unknown, f"{where}: unknown field(s): {', '.join(unknown)}")
require(not unknown, f"{where}:存在未知字段:{', '.join(unknown)}")
def non_empty_string(value, where):
require(isinstance(value, str) and value.strip(), f"{where}: must be non-empty")
require(isinstance(value, str) and value.strip(), f"{where}:不能为空")
def optional_draft_string(value, where):
@@ -127,7 +191,7 @@ def optional_draft_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")
require(isinstance(tags, list) and tags, f"{where}tags 必须是非空列表")
for index, tag in enumerate(tags):
non_empty_string(tag, f"{where}: tags[{index}]")
@@ -136,57 +200,67 @@ 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")
require(left > 0 and right == len(signature) - 1, f"{where}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")
require(all(names), f"{where}signature 中存在空参数名")
require(
len({item.casefold() for item in names}) == len(names),
f"{where}signature 中存在重复参数名",
)
return name, names
def validate_values(values, where):
require(isinstance(values, list) and values, f"{where}: values must be a non-empty list")
require(isinstance(values, list) and values, f"{where}values 必须是非空列表")
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'")
require("value" in item, f"{item_where}:缺少 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")
require(not params, f"{where}:无参数 signature 不能包含 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")
require(isinstance(params, list), f"{where}params 必须是列表")
require(len(params) == len(expected_names), f"{where}params 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)
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")
require(
isinstance(param["optional"], bool),
f"{param_where}optional 必须是布尔值",
)
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",
f"{where}params 必须与 signature 中的参数顺序一致",
)
def validate_examples(examples, where):
require(isinstance(examples, list) and examples, f"{where}: examples must be a non-empty list")
require(
isinstance(examples, list) and examples, f"{where}examples 必须是非空列表"
)
for index, example in enumerate(examples):
example_where = f"{where}: examples[{index}]"
require_mapping(example, example_where)
@@ -215,10 +289,13 @@ def validate_function(fn, where, *, returns_required, extra_fields=()):
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'")
non_empty_string(fn.get("returns"), f"{where}:缺少 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")
require(
not ("example" in fn and "examples" in fn),
f"{where}example 和 examples 不能同时存在",
)
if "example" in fn:
non_empty_string(fn["example"], f"{where}: example")
if "examples" in fn:
@@ -229,10 +306,16 @@ def validate_function(fn, where, *, returns_required, extra_fields=()):
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}'")
require(
kind in {"method", "property", "field", "constant"},
f"{where}:未知 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")
require(
visibility in {"public", "protected"},
f"{where}visibility 只能是 public 或 protected",
)
non_empty_string(member.get("desc"), f"{where}: desc")
validate_tags(member.get("tags"), where)
@@ -240,25 +323,46 @@ def validate_class_member(member, where):
reject_unknown(
member,
{
"kind", "name", "visibility", "binding", "signature", "desc",
"tags", "params", "returns", "modifiers", "example", "examples",
"kind",
"name",
"visibility",
"binding",
"signature",
"desc",
"tags",
"params",
"returns",
"modifiers",
"example",
"examples",
},
where,
)
require(member.get("binding") in {"instance", "class"}, f"{where}: invalid binding")
require(
member.get("binding") in {"instance", "class"},
f"{where}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")
require(
parsed_name.casefold() == member["name"].casefold(),
f"{where}name 与 signature 中的名称不一致",
)
if "modifiers" in member:
modifiers = member["modifiers"]
require(isinstance(modifiers, list), f"{where}: modifiers must be a list")
require(isinstance(modifiers, list), f"{where}modifiers 必须是列表")
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")
require(
all(item in allowed for item in modifiers),
f"{where}:存在无效 modifier",
)
require(
len(set(modifiers)) == len(modifiers), f"{where}:存在重复 modifier"
)
return
common = {"kind", "name", "visibility", "desc", "tags"}
@@ -266,7 +370,10 @@ def validate_class_member(member, where):
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")
require(
member.get("access") in {"read", "write", "readwrite"},
f"{where}access 无效",
)
params = member.get("params")
if params:
expected = [param.get("name") for param in params]
@@ -276,30 +383,30 @@ def validate_class_member(member, where):
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")
require(isinstance(member["static"], bool), f"{where}static 必须是布尔值")
return
reject_unknown(member, common | {"type", "value", "static"}, where)
require("value" in member and member["value"] is not None, f"{where}: missing 'value'")
require("value" in member and member["value"] is not None, f"{where}:缺少 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")
require(isinstance(member["static"], bool), f"{where}static 必须是布尔值")
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")
require(cls.get("kind") == "class", f"{where}kind 必须是 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")
require(isinstance(cls["bases"], list), f"{where}bases 必须是列表")
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")
require(isinstance(cls.get("members"), list), f"{where}members 必须是列表")
for index, member in enumerate(cls["members"]):
validate_class_member(member, f"{where}: members[{index}]")
@@ -307,7 +414,10 @@ def validate_class(cls, where):
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}'")
require(
kind in {"function", "variable", "constant", "class"},
f"{where}:未知 kind{kind}",
)
if kind == "class":
validate_class(member, where)
return
@@ -321,7 +431,10 @@ def validate_unit_member(member, where):
returns_required=True,
extra_fields={"kind", "name"},
)
require(parsed_name.casefold() == member["name"].casefold(), f"{where}: name and signature differ")
require(
parsed_name.casefold() == member["name"].casefold(),
f"{where}name 与 signature 中的名称不一致",
)
return
common = {"kind", "name", "desc", "tags", "type"}
if kind == "variable":
@@ -329,7 +442,7 @@ def validate_unit_member(member, 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'")
require("value" in member and member["value"] is not None, f"{where}:缺少 value")
if isinstance(member["value"], str):
non_empty_string(member["value"], f"{where}: value")
if "type" in member:
@@ -339,13 +452,13 @@ def validate_unit_member(member, where):
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")
require(unit.get("kind") == "unit", f"{where}kind 必须是 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",
f"{where}members 必须是列表",
)
for index, member in enumerate(unit["members"]):
validate_unit_member(member, f"{where}: members[{index}]")
@@ -354,7 +467,7 @@ def validate_unit(unit, where):
def validate_top_level_function(declaration, where):
require(
declaration.get("kind") == "function",
f"{where}: kind must be function",
f"{where}kind 必须是 function",
)
non_empty_string(declaration.get("name"), f"{where}: name")
parsed_name = validate_function(
@@ -365,7 +478,7 @@ def validate_top_level_function(declaration, where):
)
require(
parsed_name.casefold() == declaration["name"].casefold(),
f"{where}: name and signature differ",
f"{where}name signature 中的名称不一致",
)
@@ -374,7 +487,7 @@ def validate_declaration(declaration, where):
kind = declaration.get("kind")
require(
kind in {"function", "class", "unit"},
f"{where}: unknown kind '{kind}'",
f"{where}:未知 kind{kind}",
)
if kind == "function":
validate_top_level_function(declaration, where)
@@ -395,25 +508,25 @@ def validate_declaration_uniqueness(declarations):
key = declaration["signature"].casefold()
require(
key not in function_signatures,
f"{where}: duplicate function signature",
f"{where}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")
require(key not in names, f"{where}{kind} 名称重复")
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")
require_mapping(data, "录入根")
reject_unknown(data, {"module", "path", "declarations"}, "录入根")
non_empty_string(data.get("module"), "录入根:module")
non_empty_string(data.get("path"), "录入根:path")
declarations = data.get("declarations")
require(
isinstance(declarations, list) and declarations,
"input root: declarations must be a non-empty list",
"录入根:declarations 必须是非空列表",
)
for index, declaration in enumerate(declarations):
validate_declaration(declaration, f"declarations[{index}]")
@@ -424,7 +537,7 @@ def validate_page(data):
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")
require(desc, f"{where}:参数 {param.get('name', '?')} 缺少 desc")
if param.get("optional") and not desc.startswith("可选。"):
return "可选。" + desc
return desc
@@ -436,8 +549,8 @@ def render_param_table(params, where):
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'")
require(name, f"{where}:存在缺少 name 的参数")
require(ptype, f"{where}:参数 {name} 缺少 type")
desc = param_desc(param, where)
lines.append(
f"| `{escape_cell(name)}` | {escape_cell(ptype)} | {escape_cell(desc)} |"
@@ -479,7 +592,9 @@ def render_examples(fn, heading_level):
lines.append(f"// 输出:{output_lines[0]}")
else:
lines.append("// 输出:")
lines.extend(f"// {line}" if line else "//" for line in output_lines)
lines.extend(
f"// {line}" if line else "//" for line in output_lines
)
lines.append("```")
lines.append("")
while lines and not lines[-1]:
@@ -520,9 +635,7 @@ def render_callable(
returns_required=returns_required,
extra_fields={"kind", "name", "visibility", "binding", "modifiers"},
)
lines = render_intro(
f"{'#' * level} `{signature}`", declaration, fn
)
lines = render_intro(f"{'#' * level} `{signature}`", declaration, fn)
if show_visibility:
lines.extend([f"可见性:`{fn['visibility']}`", ""])
if fn.get("modifiers"):
@@ -563,11 +676,7 @@ def render_top_level_function(fn, index):
def render_class_member(member, level, where):
kind = member["kind"]
if kind == "method":
declaration = (
"class function"
if member["binding"] == "class"
else "function"
)
declaration = "class function" if member["binding"] == "class" else "function"
return render_callable(
member,
member["signature"],
@@ -586,14 +695,14 @@ def render_class_member(member, level, where):
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 = 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"]]
access = {"read": "read", "write": "write", "readwrite": "read / write"}[
member["access"]
]
lines.append(f"访问:{access}")
params = member.get("params") or []
if params:
@@ -609,13 +718,13 @@ def render_class_member(member, level, where):
def render_class(cls, level, where, *, page_root):
lines = render_intro(
f"{'#' * level} `{cls['name']}`", "class", cls
)
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.extend(
render_class_member(member, level + 1, f"{where}: members[{index}]")
)
lines.append("")
while lines and not lines[-1]:
lines.pop()
@@ -638,9 +747,7 @@ def render_unit_member(member, index, unit_where):
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
)
lines = render_intro(f"### `{member['name']}`", declaration, member)
if kind == "variable":
lines.append(f"类型:{member['type']}")
else:
@@ -718,13 +825,34 @@ def format_markdown(text):
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")
require(relative, "录入数据缺少 path")
require(isinstance(relative, str), "path 必须是字符串")
recorded_path = PureWindowsPath(relative)
require(
not recorded_path.drive.startswith("\\\\"),
"path 必须是相对路径,不允许 UNC 路径",
)
require(
not recorded_path.drive,
"path 必须是相对路径,不允许 Windows 盘符路径",
)
require(
not recorded_path.root,
"path 必须是相对路径,不能以斜杠或反斜杠开头",
)
require(
bool(recorded_path.parts),
"path 必须指向具体文档,不能只表示当前目录",
)
require(
".." not in recorded_path.parts,
"path 不能包含 '..',不允许跳转到父目录",
)
require(
recorded_path.suffix == "",
"path 不能包含 .md 等文件扩展名",
)
relative_path = Path(*recorded_path.parts)
return (
Path("skills/tsl-api-reference/references/codegen")
/ scope
@@ -750,11 +878,75 @@ def atomic_write(path, text):
temporary_path.unlink()
def gather_directory_inputs(directory, fmt):
try:
if not directory.is_dir():
die(f"输入目录不存在或不是目录:{directory}")
suffixes = FORMAT_SUFFIXES[fmt]
inputs = sorted(
(
path
for path in directory.iterdir()
if path.is_file() and path.suffix.lower() in suffixes
),
key=lambda path: (path.name.casefold(), path.name),
)
except OSError as exc:
die(f"读取输入目录失败:{directory}{exc}")
if not inputs:
displayed_suffixes = "".join(suffixes)
die(
f"目录 {directory} 中未找到符合条件的直属录入文件"
f"{displayed_suffixes}"
)
return inputs
def prepare_input(in_path, input_format, scope):
data = load_entries(in_path, input_format)
try:
rendered = render_page(data)
except GenerationError as exc:
raise GenerationError(f"录入数据校验失败:{exc}") from exc
try:
out_path = output_path(data, scope)
except GenerationError as exc:
raise GenerationError(f"输出路径无效:{exc}") from exc
try:
formatted = format_markdown(rendered)
except OSError as exc:
raise GenerationError(f"运行 Prettier 失败:{exc}") from exc
return in_path, out_path, formatted
def report_input_error(in_path, error):
print(f"错误:{in_path}{error}", file=sys.stderr)
def find_output_collisions(prepared):
owners = {}
collisions = []
for in_path, out_path, _ in prepared:
resolved_output = out_path.resolve()
first_input = owners.get(resolved_output)
if first_input is None:
owners[resolved_output] = in_path
continue
collisions.append(
f"输出目标冲突:{first_input}{in_path} 都会写入 {resolved_output}"
)
return collisions
def main(argv=None):
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
parser = ChineseArgumentParser(
description="从 YAML/JSON 录入文件生成 TSL API 文档",
usage=(
"%(prog)s [--help] [--scope SCOPE] [--format {json,yaml}] "
"(--file INPUT_FILE | --dir INPUT_DIR | INPUT_FILE)"
),
add_help=False,
allow_abbrev=False,
)
@@ -764,9 +956,22 @@ def main(argv=None):
help="显示本帮助并退出(不提供 -h 短选项)",
)
parser.add_argument(
"input",
"legacy_input",
nargs="?",
metavar="INPUT_FILE",
help="YAML/JSON 录入文件路径,例如 tmp/my-functions.yaml",
help="已废弃,请使用 --file;暂时兼容 YAML/JSON 录入文件路径",
)
parser.add_argument(
"--file",
dest="input_file",
metavar="INPUT_FILE",
help="要生成的单个 YAML/JSON 录入文件",
)
parser.add_argument(
"--dir",
dest="input_dir",
metavar="INPUT_DIR",
help="批量生成目录中的直属 YAML/JSON 录入文件",
)
parser.add_argument(
"--scope",
@@ -781,18 +986,54 @@ def main(argv=None):
)
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)
input_modes = (args.legacy_input, args.input_file, args.input_dir)
if sum(value is not None for value in input_modes) != 1:
parser.error("必须且只能指定一种输入方式:INPUT_FILE、--file 或 --dir")
is_batch = args.input_dir is not None
try:
if is_batch:
input_paths = gather_directory_inputs(Path(args.input_dir), args.format)
input_format = None
else:
in_path = Path(args.input_file or args.legacy_input)
if not in_path.is_file():
die(f"输入文件不存在或不是普通文件:{in_path}")
input_paths = [in_path]
input_format = args.format
except GenerationError as exc:
print(f"错误:{exc}", file=sys.stderr)
return 1
text = format_markdown(render_page(data))
out_path = output_path(data, args.scope)
atomic_write(out_path, text)
print(
f"wrote {out_path}",
file=sys.stderr,
)
prepared = []
input_errors = []
for in_path in input_paths:
try:
prepared.append(prepare_input(in_path, input_format, args.scope))
except GenerationError as exc:
input_errors.append((in_path, exc))
collisions = find_output_collisions(prepared)
if input_errors or collisions:
for in_path, error in input_errors:
report_input_error(in_path, error)
for collision in collisions:
print(f"错误:{collision}", file=sys.stderr)
if is_batch:
issue_count = len(input_errors) + len(collisions)
print(
f"错误:批量生成已中止:发现 {issue_count} 个问题,"
f"共检查 {len(input_paths)} 个文件;未写入任何 Markdown 文件",
file=sys.stderr,
)
return 1
for in_path, out_path, text in prepared:
try:
atomic_write(out_path, text)
except OSError as exc:
report_input_error(in_path, f"写入 Markdown 失败:{out_path}{exc}")
return 1
print(f"已生成:{in_path} -> {out_path}", file=sys.stderr)
return 0