diff --git a/scripts/build-github-wiki.py b/scripts/build-github-wiki.py index 4387d5d2c..ce7627dff 100755 --- a/scripts/build-github-wiki.py +++ b/scripts/build-github-wiki.py @@ -4,12 +4,14 @@ from __future__ import annotations import argparse +import json import re import shutil from pathlib import Path, PurePosixPath from urllib.parse import urlsplit, urlunsplit LANGUAGES = {"zh": "简体中文", "en": "English", "ja": "日本語"} +NAVIGATION_LANGUAGE_CODES = {"cn": "zh", "en": "en", "jp": "ja"} DOC_EXTENSIONS = (".md", ".mdx", ".html") CALLOUTS = {"Info": "NOTE", "Note": "NOTE", "Tip": "TIP", "Warning": "WARNING"} MOVED_SECTIONS = {"platforms", "models", "pipelines", "knowledge", "mcp"} @@ -134,6 +136,86 @@ def discover_documents(root: Path) -> list[Path]: return sorted(path for language in LANGUAGES for path in (root / language).rglob("*.mdx")) +def render_page_items( + items: list[object], titles: dict[str, str], *, indent: int = 0 +) -> tuple[list[str], set[str]]: + lines: list[str] = [] + included: set[str] = set() + prefix = " " * indent + for item in items: + if isinstance(item, str): + source_name = item.removesuffix(".mdx").removesuffix(".md") + title = titles.get(source_name) + if title: + lines.append(f"{prefix}- [{title}]({page_name(PurePosixPath(source_name))})") + included.add(source_name) + elif isinstance(item, dict) and not item.get("hidden"): + group = item.get("group") + children = item.get("pages", []) + if group and isinstance(children, list): + child_lines, child_pages = render_page_items(children, titles, indent=indent + 2) + if child_lines: + lines.append(f"{prefix}- **{group}**") + lines.extend(child_lines) + included.update(child_pages) + return lines, included + + +def build_sidebar(root: Path, titles: dict[str, str]) -> str: + navigation_path = root / "docs.json" + if not navigation_path.is_file(): + lines = ["## LangBot Documentation", ""] + for language, label in LANGUAGES.items(): + lines.extend((f"### {label}", "")) + lines.extend( + f"- [{title}]({page_name(PurePosixPath(source))})" + for source, title in titles.items() + if source.startswith(f"{language}/") + ) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + navigation = json.loads(navigation_path.read_text(encoding="utf-8"))["navigation"]["languages"] + nav_by_language = { + NAVIGATION_LANGUAGE_CODES[item["language"]]: item + for item in navigation + if item.get("language") in NAVIGATION_LANGUAGE_CODES + } + lines = ["## LangBot Documentation", "", "[Home](Home)", ""] + for language, label in LANGUAGES.items(): + language_nav = nav_by_language.get(language, {}) + open_attribute = " open" if language == "zh" else "" + lines.extend((f"", f"{label}", "")) + included: set[str] = set() + for tab_index, tab in enumerate(language_nav.get("tabs", [])): + tab_lines: list[str] = [] + tab_pages: set[str] = set() + for group in tab.get("groups", []): + group_lines, group_pages = render_page_items(group.get("pages", []), titles, indent=2) + if group_lines: + tab_lines.append(f"- **{group['group']}**") + tab_lines.extend(group_lines) + tab_pages.update(group_pages) + if tab_lines: + tab_open = " open" if language == "zh" and tab_index == 0 else "" + lines.extend((f"", f"{tab['tab']}", "")) + lines.extend(tab_lines) + lines.extend(("", "", "")) + included.update(tab_pages) + + unlisted = sorted( + (source, title) + for source, title in titles.items() + if source.startswith(f"{language}/") and source not in included + ) + if unlisted: + lines.extend(("
", "Other pages", "")) + lines.extend(f"- [{title}]({page_name(PurePosixPath(source))})" for source, title in unlisted) + lines.extend(("", "
", "")) + lines.extend(("", "")) + return "\n".join(lines).rstrip() + "\n" + + def build(root: Path, output: Path) -> int: documents = discover_documents(root) if not documents: @@ -142,27 +224,27 @@ def build(root: Path, output: Path) -> int: shutil.rmtree(output) output.mkdir(parents=True) index: dict[str, list[tuple[str, str]]] = {language: [] for language in LANGUAGES} + titles: dict[str, str] = {} for path in documents: source = PurePosixPath(path.relative_to(root).as_posix()) title, converted = convert_mdx(source, path.read_text(encoding="utf-8")) name = page_name(source) (output / f"{name}.md").write_text(converted, encoding="utf-8") index[source.parts[0]].append((name, title)) + titles[source.with_suffix("").as_posix()] = title home = [ "# LangBot Documentation", "", "This GitHub Wiki is automatically synchronized from [langbot-app/langbot-wiki](https://github.com/langbot-app/langbot-wiki).", "", ] - sidebar = ["## LangBot Documentation", ""] for language, label in LANGUAGES.items(): pages = index[language] - home.extend((f"## {label}", "", f"[{pages[0][1]}]({pages[0][0]})", "")) - sidebar.extend((f"### {label}", "")) - sidebar.extend(f"- [{title}]({name})" for name, title in pages) - sidebar.append("") + guide_name = f"{language}-insight-guide" + guide = next(((name, title) for name, title in pages if name == guide_name), pages[0]) + home.extend((f"## {label}", "", f"[{guide[1]}]({guide[0]})", "")) (output / "Home.md").write_text("\n".join(home).rstrip() + "\n", encoding="utf-8") - (output / "_Sidebar.md").write_text("\n".join(sidebar).rstrip() + "\n", encoding="utf-8") + (output / "_Sidebar.md").write_text(build_sidebar(root, titles), encoding="utf-8") (output / "_Footer.md").write_text( "Automatically synchronized from [langbot-app/langbot-wiki](https://github.com/langbot-app/langbot-wiki).\n", encoding="utf-8", diff --git a/tests/test_build_github_wiki.py b/tests/test_build_github_wiki.py index 2aa6b52d6..bea84aecb 100644 --- a/tests/test_build_github_wiki.py +++ b/tests/test_build_github_wiki.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib.util +import json import tempfile import unittest from pathlib import Path, PurePosixPath @@ -68,6 +69,66 @@ description: "ignored" self.assertIn("[zh Guide](zh-guide)", (output / "_Sidebar.md").read_text()) self.assertTrue((output / "Home.md").is_file()) + def test_sidebar_follows_docs_navigation_hierarchy(self) -> None: + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + documents = { + "zh/insight/guide.mdx": "---\ntitle: 中文首页\n---\nContent\n", + "zh/usage/models/readme.mdx": "---\ntitle: 模型配置\n---\nContent\n", + "zh/usage/models/custom.mdx": "---\ntitle: 自定义模型\n---\nContent\n", + "en/insight/guide.mdx": "---\ntitle: English Home\n---\nContent\n", + "ja/insight/guide.mdx": "---\ntitle: 日本語ホーム\n---\nContent\n", + } + for relative, content in documents.items(): + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + (root / "docs.json").write_text( + json.dumps( + { + "navigation": { + "languages": [ + { + "language": "cn", + "tabs": [ + { + "tab": "指南", + "groups": [ + {"group": "快速开始", "pages": ["zh/insight/guide"]}, + { + "group": "配置 AI", + "pages": [ + "zh/usage/models/readme", + { + "group": "模型供应商", + "pages": ["zh/usage/models/custom"], + }, + ], + }, + ], + } + ], + } + ] + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + output = root / "out" + wiki.build(root, output) + sidebar = (output / "_Sidebar.md").read_text(encoding="utf-8") + + self.assertIn("简体中文", sidebar) + self.assertIn("指南", sidebar) + self.assertIn("- **快速开始**", sidebar) + self.assertIn(" - [中文首页](zh-insight-guide)", sidebar) + self.assertIn(" - **模型供应商**", sidebar) + self.assertIn(" - [自定义模型](zh-usage-models-custom)", sidebar) + self.assertLess(sidebar.index("中文首页"), sidebar.index("模型配置")) + if __name__ == "__main__": unittest.main()