[docs][refactor]: 将旧 01~06 中文教程合并整理为 modules/frameworks 文件夹结构
- 删除 01~05 旧中文教程目录 - 将 03 核心功能教程拆分为各 framework 模块的子文档 - 06 工具脚本移入 modules/frameworks/编辑器工具/UI工具/ - 非框架内容合并进 rules/20-开发规则.md 与 rules/40-工作流清单.md - 更新所有交叉链接与文档关联表 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
218
hooks/sync_doc_associations.py
Normal file
218
hooks/sync_doc_associations.py
Normal file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
# hooks/sync_doc_associations.py
|
||||
#
|
||||
# 当 md 文件变更时,自动在关联文档的标准位置追加变更提醒:
|
||||
# - 有 ## 变更记录 表格的模块文档:追加一行变更记录
|
||||
# - 有 ## 下一步 清单的 progress/tasks 文档:追加一个待检查项
|
||||
# - 其他文档:追加 ## 最近关联变更 章节
|
||||
#
|
||||
# 本脚本只追加、不覆盖正文;执行后请通过 git diff 人工确认。
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
EXCLUDED_PREFIXES = (
|
||||
'.claude/', '.codex/', '.git/',
|
||||
'Library/', 'Temp/', 'Logs/', 'UserSettings/',
|
||||
'obj/', 'bin/', 'HybridCLRData/',
|
||||
'Assets/ThirdPlugins/',
|
||||
'Assets/.WX-WASM-SDK-V2/',
|
||||
'Packages/',
|
||||
)
|
||||
|
||||
|
||||
def run_git(args):
|
||||
result = subprocess.run(
|
||||
["git"] + args,
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return result.stdout.strip(), result.returncode
|
||||
|
||||
|
||||
def get_changed_md_files():
|
||||
"""获取 tracked 改动 + untracked 新 md 文件。"""
|
||||
changed, code = run_git(["diff", "--name-only"])
|
||||
if code != 0 or not changed:
|
||||
changed = ""
|
||||
|
||||
untracked, code = run_git(["ls-files", "--others", "--exclude-standard"])
|
||||
if code == 0 and untracked:
|
||||
changed = (changed + "\n" + untracked).strip()
|
||||
|
||||
if not changed:
|
||||
return []
|
||||
|
||||
files = [line.strip() for line in changed.splitlines() if line.strip().endswith(".md")]
|
||||
files = [f for f in files if not f.startswith(EXCLUDED_PREFIXES)]
|
||||
return sorted(set(files))
|
||||
|
||||
|
||||
def resolve_link(source_file: Path, link: str):
|
||||
"""解析相对链接为项目相对路径。"""
|
||||
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", link):
|
||||
return None
|
||||
if link.startswith("#"):
|
||||
return None
|
||||
link = link.split("#")[0]
|
||||
if not link:
|
||||
return None
|
||||
target = (source_file.parent / link).resolve()
|
||||
try:
|
||||
target.relative_to(PROJECT_ROOT)
|
||||
except ValueError:
|
||||
return None
|
||||
rel = target.relative_to(PROJECT_ROOT)
|
||||
return str(rel).replace("\\", "/")
|
||||
|
||||
|
||||
def extract_associations(md_path: Path):
|
||||
"""读取 md 的 ## 文档关联 章节,返回关联文件相对路径列表。"""
|
||||
content = md_path.read_text(encoding="utf-8")
|
||||
in_section = False
|
||||
targets = []
|
||||
for line in content.splitlines():
|
||||
if re.match(r"^##\s+文档关联\s*$", line):
|
||||
in_section = True
|
||||
continue
|
||||
if in_section and re.match(r"^##\s+", line):
|
||||
break
|
||||
if in_section:
|
||||
for _, link in re.findall(r"\[([^\]]*)\]\(([^\)]+)\)", line):
|
||||
resolved = resolve_link(md_path, link)
|
||||
if resolved:
|
||||
targets.append(resolved)
|
||||
return targets
|
||||
|
||||
|
||||
def find_section_range(lines, title_re):
|
||||
"""返回 [start, end) 行号,end 为下一个 ## 标题或文件末尾。"""
|
||||
start = None
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(title_re, line):
|
||||
start = i
|
||||
continue
|
||||
if start is not None and re.match(r"^##\s+", line):
|
||||
return start, i
|
||||
if start is not None:
|
||||
return start, len(lines)
|
||||
return None, None
|
||||
|
||||
|
||||
def append_changelog(target_path: Path, source_rel: str, date_str: str) -> bool:
|
||||
"""在 ## 变更记录 表格追加一行;成功返回 True。"""
|
||||
content = target_path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
start, end = find_section_range(lines, r"^##\s+变更记录\s*$")
|
||||
if start is None:
|
||||
return False
|
||||
|
||||
# 找到表格最后一行(跳过标题和分隔线)
|
||||
insert_pos = end
|
||||
for i in range(start + 1, end):
|
||||
if lines[i].strip().startswith("|") and not re.match(r"^\|[-:\s|]+\|$", lines[i].strip()):
|
||||
insert_pos = i + 1
|
||||
|
||||
new_line = f"| {date_str} | [{source_rel}](../../{source_rel}) 发生变更 | 待补充影响范围 | 待确认 |"
|
||||
lines.insert(insert_pos, new_line)
|
||||
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def append_next_step(target_path: Path, source_rel: str) -> bool:
|
||||
"""在 ## 下一步 清单追加一个待检查项;成功返回 True。"""
|
||||
content = target_path.read_text(encoding="utf-8")
|
||||
lines = content.splitlines()
|
||||
start, end = find_section_range(lines, r"^##\s+下一步\s*$")
|
||||
if start is None:
|
||||
return False
|
||||
|
||||
new_line = f"- [ ] 检查 [{source_rel}](../../{source_rel}) 的变更是否影响本文档"
|
||||
# 避免重复
|
||||
for line in lines[start:end]:
|
||||
if source_rel in line and "变更是否影响本文档" in line:
|
||||
return True # 已存在,视为成功但不写入
|
||||
|
||||
lines.insert(end, new_line)
|
||||
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def append_recent_changes_section(target_path: Path, source_rel: str, date_str: str) -> bool:
|
||||
"""在没有 变更记录/下一步 的文档末尾追加 ## 最近关联变更。"""
|
||||
content = target_path.read_text(encoding="utf-8")
|
||||
# 生成从 target 到 source 的相对链接
|
||||
try:
|
||||
link = os.path.relpath(PROJECT_ROOT / source_rel, target_path.parent).replace("\\", "/")
|
||||
except ValueError:
|
||||
link = source_rel
|
||||
|
||||
if "## 最近关联变更" in content:
|
||||
lines = content.splitlines()
|
||||
start, end = find_section_range(lines, r"^##\s+最近关联变更\s*$")
|
||||
if start is not None:
|
||||
new_line = f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。"
|
||||
lines.insert(end, new_line)
|
||||
target_path.write_text("\n".join(lines) + ("\n" if content.endswith("\n") else ""), encoding="utf-8")
|
||||
return True
|
||||
|
||||
section = (
|
||||
"\n---\n\n"
|
||||
"## 最近关联变更\n\n"
|
||||
f"- {date_str}:关联文档 [{source_rel}]({link}) 发生变更,需同步检查。\n"
|
||||
)
|
||||
target_path.write_text(content.rstrip() + section, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def sync_associations():
|
||||
changed_files = get_changed_md_files()
|
||||
if not changed_files:
|
||||
print("[sync-doc-assoc] No .md changes detected.")
|
||||
return 0
|
||||
|
||||
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
modified = []
|
||||
|
||||
for source_rel in changed_files:
|
||||
source_path = PROJECT_ROOT / source_rel
|
||||
if not source_path.exists():
|
||||
continue
|
||||
targets = extract_associations(source_path)
|
||||
for target_rel in targets:
|
||||
target_path = PROJECT_ROOT / target_rel
|
||||
if not target_path.is_file():
|
||||
# Skip directories, globs, or missing targets
|
||||
continue
|
||||
if target_rel == source_rel:
|
||||
continue
|
||||
|
||||
done = (
|
||||
append_changelog(target_path, source_rel, date_str)
|
||||
or append_next_step(target_path, source_rel)
|
||||
or append_recent_changes_section(target_path, source_rel, date_str)
|
||||
)
|
||||
if done:
|
||||
modified.append((source_rel, target_rel))
|
||||
|
||||
if not modified:
|
||||
print("[sync-doc-assoc] No associated docs need updating.")
|
||||
return 0
|
||||
|
||||
print("[sync-doc-assoc] Updated associated docs:")
|
||||
for source, target in modified:
|
||||
print(f" - {source} -> {target}")
|
||||
print("\n[sync-doc-assoc] Please review the changes with git diff before committing.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(sync_associations())
|
||||
Reference in New Issue
Block a user