#!/usr/bin/env python3 """将全角/易混淆标点规范为半角 ASCII(注释, 文档, 配置模板). 不转换弯引号 “ ” ‘ ’,避免破坏 Python/JS 字符串字面量. """ from __future__ import annotations import argparse from pathlib import Path REPO = Path(__file__).resolve().parents[1] SKIP_DIRS = frozenset({ ".git", ".venv", "node_modules", "__pycache__", ".cursor", "agent-transcripts", }) SCAN_SUFFIXES = frozenset({ ".py", ".js", ".html", ".md", ".sh", ".css", ".json", ".cjs", ".txt", ".yml", ".yaml", ".example", }) AMBIGUOUS_CHARS = frozenset( "\uff08\uff09\uff1a\uff0c\uff1b\uff1f\uff01\u3002\u3001\u00a0" ) TRANSLATION = str.maketrans( { "\uff08": "(", "\uff09": ")", "\uff1a": ":", "\uff0c": ",", "\uff1b": ";", "\uff1f": "?", "\uff01": "!", "\u3002": ".", "\u3001": ",", "\u00a0": " ", } ) def should_scan(path: Path) -> bool: if not path.is_file(): return False if any(part in SKIP_DIRS for part in path.parts): return False if path.name == ".env.example" or path.name.endswith(".env.example"): return True return path.suffix in SCAN_SUFFIXES def normalize_text(text: str) -> tuple[str, int]: count = sum(1 for ch in text if ch in AMBIGUOUS_CHARS) if not count: return text, 0 return text.translate(TRANSLATION), count def read_text_strip_bom(path: Path) -> tuple[str, bool]: raw = path.read_text(encoding="utf-8") if raw.startswith("\ufeff"): return raw.lstrip("\ufeff"), True return raw, False def iter_targets(root: Path) -> list[Path]: return sorted(p for p in root.rglob("*") if should_scan(p)) def main() -> int: parser = argparse.ArgumentParser(description="Normalize ambiguous Unicode punctuation") parser.add_argument("--dry-run", action="store_true") parser.add_argument("--root", default=str(REPO)) args = parser.parse_args() root = Path(args.root) files_changed = 0 chars_changed = 0 for path in iter_targets(root): try: original, had_bom = read_text_strip_bom(path) except (OSError, UnicodeDecodeError): continue normalized, n = normalize_text(original) if not n and not had_bom: continue rel = path.relative_to(root) if args.dry_run: print(f"[dry-run] {rel}: {n} chars") else: # 保持原换行风格, 仅替换标点 path.write_text(normalized, encoding="utf-8", newline="") files_changed += 1 chars_changed += n print(f"done: {files_changed} files, {chars_changed} replacements") return 0 if __name__ == "__main__": raise SystemExit(main())