mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-29 20:05:25 +08:00
fix: preserve docs hierarchy in GitHub Wiki (#106)
Co-authored-by: dadachann <[email protected]>
This commit is contained in:
@@ -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"<details{open_attribute}>", f"<summary><strong>{label}</strong></summary>", ""))
|
||||
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"<details{tab_open}>", f"<summary>{tab['tab']}</summary>", ""))
|
||||
lines.extend(tab_lines)
|
||||
lines.extend(("", "</details>", ""))
|
||||
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(("<details>", "<summary>Other pages</summary>", ""))
|
||||
lines.extend(f"- [{title}]({page_name(PurePosixPath(source))})" for source, title in unlisted)
|
||||
lines.extend(("", "</details>", ""))
|
||||
lines.extend(("</details>", ""))
|
||||
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",
|
||||
|
||||
@@ -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("<summary><strong>简体中文</strong></summary>", sidebar)
|
||||
self.assertIn("<summary>指南</summary>", 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()
|
||||
|
||||
Reference in New Issue
Block a user