From ea00d43bb8f5733660241aa94af150405e1f3324 Mon Sep 17 00:00:00 2001 From: csh Date: Fri, 23 Jan 2026 16:54:53 +0800 Subject: [PATCH] :bug: fix(playbook): support toml without tomllib --- .gitattributes | 3 +- scripts/playbook.py | 124 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 0610245..a0be8e6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -39,10 +39,9 @@ *.cppm text eol=lf *.mpp text eol=lf *.cmake text eol=lf +*.clangd text eol=lf CMakeLists.txt text eol=lf CMakePresets.json text eol=lf -*.clangd text eol=lf -.clangd text eol=lf # Binary files (no line-ending conversion). *.png binary diff --git a/scripts/playbook.py b/scripts/playbook.py index bc04219..a85cfbe 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -5,7 +5,10 @@ from pathlib import Path from shutil import copy2, copytree, which import subprocess -import tomllib +try: + import tomllib +except ModuleNotFoundError: # Python < 3.11 + tomllib = None ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"] SCRIPT_DIR = Path(__file__).resolve().parent @@ -16,8 +19,125 @@ def usage() -> str: return "Usage:\n python scripts/playbook.py -config \n python scripts/playbook.py -h" +def strip_inline_comment(value: str) -> str: + in_single = False + in_double = False + escape = False + for idx, ch in enumerate(value): + if escape: + escape = False + continue + if in_double and ch == "\\": + escape = True + continue + if ch == "'" and not in_double: + in_single = not in_single + continue + if ch == '"' and not in_single: + in_double = not in_double + continue + if ch == "#" and not in_single and not in_double: + return value[:idx].rstrip() + return value + + +def split_list_items(raw: str) -> list[str]: + items: list[str] = [] + buf: list[str] = [] + in_single = False + in_double = False + escape = False + for ch in raw: + if escape: + buf.append(ch) + escape = False + continue + if in_double and ch == "\\": + buf.append(ch) + escape = True + continue + if ch == "'" and not in_double: + in_single = not in_single + buf.append(ch) + continue + if ch == '"' and not in_single: + in_double = not in_double + buf.append(ch) + continue + if ch == "," and not in_single and not in_double: + items.append("".join(buf).strip()) + buf = [] + continue + buf.append(ch) + tail = "".join(buf).strip() + if tail: + items.append(tail) + return items + + +def parse_toml_value(raw: str) -> object: + value = raw.strip() + if not value: + return "" + if value.startswith("[") and value.endswith("]"): + inner = value[1:-1].strip() + if not inner: + return [] + return [parse_toml_value(item) for item in split_list_items(inner)] + lowered = value.lower() + if lowered == "true": + return True + if lowered == "false": + return False + if value[0] in ("'", '"') and value[-1] == value[0]: + if value[0] == "'": + return value[1:-1] + import ast + + try: + return ast.literal_eval(value) + except (ValueError, SyntaxError): + return value[1:-1] + try: + if "." in value: + return float(value) + return int(value) + except ValueError: + return value + + +def loads_toml_minimal(raw: str) -> dict: + data: dict[str, dict] = {} + current = None + for line in raw.splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + if stripped.startswith("[") and stripped.endswith("]"): + section = stripped[1:-1].strip() + if not section: + raise ValueError("empty section header") + current = data.setdefault(section, {}) + if not isinstance(current, dict): + raise ValueError(f"invalid section: {section}") + continue + if "=" not in stripped: + raise ValueError(f"invalid line: {line}") + key, value = stripped.split("=", 1) + key = key.strip() + if not key: + raise ValueError("missing key") + value = strip_inline_comment(value.strip()) + target = current if current is not None else data + target[key] = parse_toml_value(value) + return data + + def load_config(path: Path) -> dict: - return tomllib.loads(path.read_text(encoding="utf-8")) + raw = path.read_text(encoding="utf-8") + if tomllib is not None: + return tomllib.loads(raw) + return loads_toml_minimal(raw) def log(message: str) -> None: