"""把既有文集渲染到单一阅读视图;原文与历史选择器仍以JSON载荷为准。""" from html import escape import importlib.util import os from pathlib import Path import posixpath import re from urllib.parse import quote, unquote, urlsplit from markdown_it import MarkdownIt import source_archive # 复用现有链接检查器的GitHub标题语义,不维护另一种slug算法。 _spec = importlib.util.spec_from_file_location('book_link_rules', Path(__file__).resolve().parent / 'check-local-links.py') _links = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(_links) slug = _links.github_slug def chapter_id(path): return 'doc-' + Path(path).stem def relocated(path, fragment=''): """把历史导航转成当前阅读地址;冻结原文字节不改写。""" name = posixpath.basename(path) if posixpath.dirname(path) in {'.github/scripts', '.github/extensions'} and ( (name.startswith(('check-', 'test-')) and name.endswith('.py')) or name in {'requirements-gongfa.txt', 'requirements-faqi.txt'}): return '.github/' + name, fragment if posixpath.dirname(path) in {'.github/lib', '.github/extensions/lib', '.github/scripts/lib'}: if name in {'README.md', 'AGENTS.md'}: return '.github/AGENTS.md', '维护脚本与共享库' if name == '__init__.py': return '.github/source_boundaries.py', fragment aliases = { '.github/learning-map.md': '.github/god.md', '.github/context/getting-started/learning-map.md': '.github/god.md', '.github/docs/getting-started/learning-map.md': '.github/god.md', '.github/context/tool-explanation-template-v0.md': '.github/tool-explanation-template-v0.md', '.github/context/tool-explanation-template-v1.md': '.github/tool-explanation-template-v1.md', '.github/metadata/faqi/README.md': '.github/context/faqi/README.md', '.github/metadata/faqi/AGENTS.md': '.github/context/AGENTS.md', '.github/metadata/gongfa/AGENTS.md': '.github/context/AGENTS.md', '.github/skills/claude-official-skills/README.md': '.github/skills/README.md', '.github/skills/claude-official-skills/AGENTS.md': '.github/skills/AGENTS.md', } if path in aliases: target = aliases[path] return target, '' if target == '.github/god.md' else fragment flat_groups = { '.github/context/ai-citation': 'ai-citation', '.github/context/metadata': 'metadata', '.github/context/prompts': 'prompts', '.github/context/getting-started': 'getting-started', } for old, group in flat_groups.items(): if path == old or path.startswith(old + '/'): suffix = path[len(old):].lstrip('/') if not suffix or suffix == 'README.md': filename = f'{group}-README.md' elif suffix == 'AGENTS.md': filename = f'{group}-AGENTS.md' else: filename = suffix.replace('/', '-') return '.github/' + filename, fragment if path.rstrip('/') == '.github/docs/gongfa': return '.github/context/gongfa/README.md', fragment or '原文文集' if path.startswith('.github/docs/gongfa/'): kind = 'faqi' if Path(path).name == 'faqi-system.md' else 'gongfa' target = f'.github/context/{kind}/README.md' if Path(path).name in {'README.md', 'AGENTS.md'}: return target, '数据与边界' if fragment else '' return target, chapter_id(path) + ('-' + fragment if fragment else '') if path.startswith('.github/metadata/gongfa/'): if Path(path).name in {'catalog.md', 'README.md', 'AGENTS.md'}: return '.github/context/gongfa/README.md', ('品级词表' if '品级' in fragment or 'grade' in fragment else '数据与边界') if fragment else '' return '.github/context/gongfa/data.json', '' if path in {'.github/metadata/faqi.json', '.github/metadata/faqi.schema.json'}: return '.github/context/faqi/data.json', '' if path == '.github/tools/faqi-catalog.md': return '.github/context/faqi/README.md', fragment or '法器总表' # auto-skill脚本路径是固定历史来源身份;入口已迁到Extensions,不是当前Skill目录。 aliases = { '.github/skills/auto-skill/scripts/Skill_Seekers-development': '.github/extensions/Skill_Seekers-development', '.github/skills/auto-skill/scripts/skill-seekers-src': '.github/extensions/Skill_Seekers-development', '.github/skills/auto-skill/scripts/skill-seekers-configs': '.github/extensions/Skill_Seekers-development/api/configs_repo', '.github/skills/claude-official-skills': '.github/extensions/claude-official-skills', '.github/extensions/fetch-research-raw.py': '.github/fetch-research-raw.py', '.github/extensions/sync-doc-toc.py': '.github/sync-doc-toc.py', '.github/extensions/sync-faqi-catalog.py': '.github/sync-faqi-catalog.py', '.github/extensions/sync-gongfa-catalog.py': '.github/sync-gongfa-catalog.py', } prefixes = { '.github/docs/getting-started': '.github/context/getting-started', '.github/docs': '.github/context', '.github/assets/external-resources': '.github/context/external-resources', '.github/assets/ai-citation': '.github/context/ai-citation', '.github/assets': '.github/context/assets', '.github/metadata': '.github/context/metadata', '.github/research': '.github/context/research', '.github/prompts': '.github/context/prompts', '.github/scripts/lib': '.github', '.github/extensions/lib': '.github', '.github/lib': '.github', '.github/scripts': '.github', '.github/skills/auto-skill/scripts': '.github/extensions', '.github/tools/config': '.github/extensions/codex-config', '.github/tools/prompts-library': '.github/extensions/prompts-library', '.github/tools/chat-vault': '.github/extensions/chat-vault', '.github/tools/external/.tmux': '.github/extensions/oh-my-tmux', '.github/tools/external/html-tools-main/README.md': '.github/extensions/html-tools-main.md', '.github/tools/external/html-tools-main': '.github/extensions', '.github/tools/external': '.github/extensions', '.github/tools': '.github/extensions', **aliases, } for old, new in sorted(prefixes.items(), key=lambda item: len(item[0]), reverse=True): if path == old or path.startswith(old + '/'): path = new + path[len(old):] break claude_repository = 'https://github.com/anthropics/skills' claude_revision = '1ed29a03dc852d30fa6ef2ca53a67dc2c2c2c563' claude_initializer = 'skills/skill-creator/scripts/init_skill.py' if path in {'.github/init_skill.py', '.github/extensions/init_skill.py'}: return (f'{claude_repository}/blob/{claude_revision}/{claude_initializer}', fragment) retired_claude = '.github/extensions/claude-official-skills' if path == retired_claude or path.startswith(retired_claude + '/'): suffix = path[len(retired_claude):].lstrip('/') route = 'blob' if suffix and Path(suffix).suffix else 'tree' return (f'{claude_repository}/{route}/{claude_revision}' + ('/' + quote(suffix, safe='/') if suffix else ''), fragment) retired_upstream = '.github/extensions/Skill_Seekers-development' if path == retired_upstream or path.startswith(retired_upstream + '/'): suffix = path[len(retired_upstream):].lstrip('/') repository = 'https://github.com/yusufkaraaslan/Skill_Seekers' revision = '26638b248279a3b001b651475e0f28975f5ff069' route = 'blob' if suffix and Path(suffix).suffix else 'tree' return (f'{repository}/{route}/{revision}' + ('/' + quote(suffix, safe='/') if suffix else ''), fragment) retired = { '.github/extensions/prompts-library': ('b0e3454bb865b95bdc70127a431214c384542dee', 'tree'), '.github/extensions/chat-vault': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'tree'), '.github/extensions/my-nvim': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'tree'), '.github/extensions/codex-config': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'tree'), '.github/extensions/MCPlayerTransfer': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'tree'), '.github/extensions/XHS-image-to-PDF-conversion': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'tree'), '.github/extensions/xhs graphic production - 1.0.html': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'blob'), '.github/extensions/xhs graphic production.html': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'blob'), '.github/extensions/html-tools-main.md': ('44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58', 'blob'), '.github/context/assets': ('71bf460cfa894ad45163a9591d2043422974a681', 'tree'), '.github/context/external-resources': ('71bf460cfa894ad45163a9591d2043422974a681', 'tree'), '.github/skills/auto-tmux': ('71bf460cfa894ad45163a9591d2043422974a681', 'tree'), } for prefix, (revision, root_route) in sorted(retired.items(), key=lambda item: len(item[0]), reverse=True): if path == prefix or path.startswith(prefix + '/'): # 原正文保持冻结字节;退役实现只链接到最后仍存在的已发布修订,不能冒充当前入口。 route = root_route if path == prefix else 'blob' return ('https://github.com/tradecatlabs/vibe-coding-cn/' + route + '/' + revision + '/' + quote(path, safe='/')), fragment return path, '' if path == '.github/god.md' else fragment def reference(value, original_path, output_path): value = value.strip() if not value or urlsplit(value).scheme or value.startswith(('/', '#')): if value.startswith('#') and original_path.startswith('.github/docs/gongfa/'): return '#' + chapter_id(original_path) + '-' + value[1:] return value path, marker, fragment = unquote(value).partition('#') original = posixpath.normpath(posixpath.join(posixpath.dirname(original_path), path)) target, anchor = relocated(original, fragment) if urlsplit(target).scheme: return target + ('#' + anchor if anchor else '') relative = os.path.relpath(target, posixpath.dirname(output_path)).replace(os.sep, '/') if target == output_path: relative = '' if anchor else Path(output_path).name return relative + ('#' + anchor if anchor else '') def rewrite_links(line, original_path, output_path): for pattern in _links.LINK_PATTERNS: def replace(match): value = match.group(1) new = reference(value, original_path, output_path) start = match.start(1) - match.start() end = match.end(1) - match.start() return match.group(0)[:start] + new + match.group(0)[end:] line = pattern.sub(replace, line) return line def source_keys(root, kind): store = source_archive.bundles(Path(root)).get(kind) if not store: return [] keys = [key for key in store['bytes'] if key.startswith('.github/docs/gongfa/') and key.endswith('.md') and Path(key).name not in {'README.md', 'AGENTS.md'}] # 顺序也来自本集合JSON中的原导航,不依赖另一本可编辑索引。 original_index = '.github/docs/gongfa/README.md' order = [] if original_index in store['bytes']: tokens = MarkdownIt('commonmark').parse(store['bytes'][original_index].decode('utf-8')) for token in tokens: for child in token.children or []: if child.type != 'link_open': continue value = child.attrGet('href') or '' target = posixpath.normpath(posixpath.join(posixpath.dirname(original_index), unquote(value).split('#')[0])) if target in keys and target not in order: order.append(target) return order + sorted(set(keys) - set(order)) def render_sources(root, kind): output = f'.github/context/{kind}/README.md' sections = ['> 文集保留历史正文;已退役实现的相关链接指向已发布的固定Git修订,' '不是当前安装或运行入口。现存程序以Extensions清单为准。'] for key in source_keys(root, kind): text = source_archive.read_text(Path(root) / key) tokens = MarkdownIt('commonmark').parse(text) protected = {line for token in tokens if token.type in {'fence', 'code_block'} and token.map for line in range(*token.map)} headings = {token.map[0] for token in tokens if token.type == 'heading_open' and token.map} lines = text.splitlines() title = next((re.sub(r'^#+\s*', '', lines[line]).strip() for line in sorted(headings)), Path(key).stem) prefix = chapter_id(key) result = [f'', '## ' + title, '', '原始文集:`' + key + '`;下文为阅读呈现,原字节和来源保存在[data.json](data.json)。', ''] used, emitted = {}, set() for number, line in enumerate(lines): if number in protected: result.append(line) continue def anchor(match): name = prefix + '-' + match.group(1) if name in emitted: return '' emitted.add(name) return '' line = re.sub(r'', anchor, line) if number in headings: label = re.sub(r'^#{1,6}\s+', '', line).rstrip(' #') value = slug(label) count = used.get(value, 0) used[value] = count + 1 name = prefix + '-' + value + ('-' + str(count) if count else '') if name not in emitted: result.append('') emitted.add(name) line = '**' + label + '**' result.append(rewrite_links(line, key, output)) sections.append('\n'.join(result)) return '\n\n'.join(sections) + ('\n' if sections else '') def source_toc(root, kind): # 从实际Markdown语法节点取锚点;代码示例里的HTML不进入导航。 tokens = MarkdownIt('commonmark').parse(render_sources(root, kind)) prefixes = sorted((chapter_id(key) for key in source_keys(root, kind)), key=len, reverse=True) result, seen = [], set() def visit(nodes): for token in nodes: if token.type in {'html_inline', 'html_block'}: for name in re.findall(r'