build: migrate docs publishing to mdbook

Move the English root site to mdBook, keep the Chinese site as a sub-book, and update CI/deploy to publish .mdbook outputs to docs/ and docs/cn/. Also add regression coverage for placeholder skipping, publish-tree assembly, and shared resource setup.
This commit is contained in:
cydia2001
2026-03-11 00:57:34 +00:00
parent 7143925e01
commit 9d75cdc6c3
22 changed files with 1432 additions and 673 deletions

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from tools.assemble_docs_publish_tree import assemble_publish_tree
class AssembleDocsPublishTreeTests(unittest.TestCase):
def test_assemble_publish_tree_uses_legacy_docs_layout(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
pages_repo = root / "pages"
en_source = root / "en-html"
zh_source = root / "zh-html"
pages_repo.mkdir()
en_source.mkdir()
zh_source.mkdir()
(en_source / "index.html").write_text("english home", encoding="utf-8")
(en_source / "guide.html").write_text("english guide", encoding="utf-8")
(zh_source / "index.html").write_text("chinese home", encoding="utf-8")
(zh_source / "searchindex.js").write_text("zh search", encoding="utf-8")
assemble_publish_tree(
destination_root=pages_repo,
docs_subdir="docs",
en_source=en_source,
zh_source=zh_source,
)
self.assertEqual(
(pages_repo / "docs" / "index.html").read_text(encoding="utf-8"),
"english home",
)
self.assertEqual(
(pages_repo / "docs" / "guide.html").read_text(encoding="utf-8"),
"english guide",
)
self.assertEqual(
(pages_repo / "docs" / "cn" / "index.html").read_text(encoding="utf-8"),
"chinese home",
)
self.assertEqual(
(pages_repo / "docs" / "cn" / "searchindex.js").read_text(encoding="utf-8"),
"zh search",
)
def test_assemble_publish_tree_replaces_stale_docs_content(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
pages_repo = root / "pages"
en_source = root / "en-html"
zh_source = root / "zh-html"
(pages_repo / "docs" / "cn").mkdir(parents=True)
(pages_repo / "docs" / "old.html").write_text("stale en", encoding="utf-8")
(pages_repo / "docs" / "cn" / "old.html").write_text("stale zh", encoding="utf-8")
en_source.mkdir()
zh_source.mkdir()
(en_source / "index.html").write_text("fresh en", encoding="utf-8")
(zh_source / "index.html").write_text("fresh zh", encoding="utf-8")
assemble_publish_tree(
destination_root=pages_repo,
docs_subdir="docs",
en_source=en_source,
zh_source=zh_source,
)
self.assertFalse((pages_repo / "docs" / "old.html").exists())
self.assertFalse((pages_repo / "docs" / "cn" / "old.html").exists())
self.assertEqual(
(pages_repo / "docs" / "index.html").read_text(encoding="utf-8"),
"fresh en",
)
self.assertEqual(
(pages_repo / "docs" / "cn" / "index.html").read_text(encoding="utf-8"),
"fresh zh",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from tools.ensure_book_resources import ensure_resource_views
class EnsureBookResourcesTests(unittest.TestCase):
def test_ensure_resource_views_creates_missing_symlinks(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
chapter_dir = root / "en_chapters"
chapter_dir.mkdir()
for directory in ("img", "references", "static"):
(root / directory).mkdir()
(root / "mlsys.bib").write_text("bib", encoding="utf-8")
ensure_resource_views(chapter_dir, root)
for name in ("img", "references", "static", "mlsys.bib"):
path = chapter_dir / name
self.assertTrue(path.is_symlink(), f"{name} should be a symlink")
self.assertEqual(path.resolve(), (root / name).resolve())
def test_ensure_resource_views_keeps_existing_non_symlink_paths(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
chapter_dir = root / "en_chapters"
chapter_dir.mkdir()
for directory in ("img", "references", "static"):
(root / directory).mkdir()
(root / "mlsys.bib").write_text("root bib", encoding="utf-8")
local_bib = chapter_dir / "mlsys.bib"
local_bib.write_text("local bib", encoding="utf-8")
local_static = chapter_dir / "static"
local_static.mkdir()
(local_static / "frontpage.html").write_text("local static", encoding="utf-8")
ensure_resource_views(chapter_dir, root)
self.assertFalse(local_bib.is_symlink())
self.assertEqual(local_bib.read_text(encoding="utf-8"), "local bib")
self.assertFalse(local_static.is_symlink())
self.assertTrue((local_static / "frontpage.html").exists())
self.assertTrue((chapter_dir / "img").is_symlink())
self.assertTrue((chapter_dir / "references").is_symlink())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,114 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from tools.prepare_mdbook import build_title_cache, rewrite_markdown, write_summary
class PrepareMdBookTests(unittest.TestCase):
def test_write_summary_skips_placeholder_pages(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
source = root / "en_chapters"
source.mkdir()
(source / "index.md").write_text(
"""Machine Learning Systems
========================
```toc
:maxdepth: 2
chapter_preface/index
chapter_introduction/index
```
```toc
:maxdepth: 1
appendix/index
```
""",
encoding="utf-8",
)
chapter_preface = source / "chapter_preface"
chapter_preface.mkdir()
(chapter_preface / "index.md").write_text(
"[TODO: src = zh_chapters/chapter_preface/index.md]\n",
encoding="utf-8",
)
chapter_intro = source / "chapter_introduction"
chapter_intro.mkdir()
(chapter_intro / "index.md").write_text("# Introduction\n", encoding="utf-8")
appendix = source / "appendix"
appendix.mkdir()
(appendix / "index.md").write_text("# Appendix\n", encoding="utf-8")
summary_path = write_summary(
source,
placeholder_prefix="[TODO: src = zh_chapters/",
)
summary = summary_path.read_text(encoding="utf-8")
self.assertEqual(
summary,
"""# Summary
[Machine Learning Systems](index.md)
[Introduction](chapter_introduction/index.md)
[Appendix](appendix/index.md)
""",
)
title_cache = build_title_cache(
source,
placeholder_prefix="[TODO: src = zh_chapters/",
)
rewritten = rewrite_markdown(
(source / "index.md").read_text(encoding="utf-8"),
(source / "index.md").resolve(),
title_cache,
)
self.assertIn("- [Introduction](chapter_introduction/index.md)", rewritten)
self.assertIn("- [Appendix](appendix/index.md)", rewritten)
self.assertNotIn("chapter_preface/index.md", rewritten)
def test_rewrite_markdown_uses_configured_bibliography_title(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
page = root / "chapter.md"
page.write_text(
"""# Introduction
Reference :cite:`smith2024`.
""",
encoding="utf-8",
)
rewritten = rewrite_markdown(
page.read_text(encoding="utf-8"),
page.resolve(),
{page.resolve(): "Introduction"},
bib_db={
"smith2024": {
"author": "Smith, Alice and Doe, Bob",
"title": "Systems Paper",
"year": "2024",
"journal": "ML Systems Journal",
}
},
bibliography_title="References",
)
self.assertIn("## References", rewritten)
self.assertNotIn("## 参考文献", rewritten)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,227 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from tools.prepare_mdbook_zh import extract_title, rewrite_markdown, write_summary
class PrepareMdBookZhTests(unittest.TestCase):
def test_extract_title_supports_atx_and_setext_headings(self) -> None:
self.assertEqual(extract_title("# 导论\n"), "导论")
self.assertEqual(extract_title("前言文字\n\n## 机器学习应用\n"), "机器学习应用")
self.assertEqual(extract_title("机器学习系统:设计和实现\n=========================\n"), "机器学习系统:设计和实现")
def test_write_summary_generates_nested_navigation(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
source = root / "zh_chapters"
source.mkdir()
(source / "index.md").write_text(
"""机器学习系统:设计和实现
=========================
```eval_rst
.. raw:: html
:file: frontpage.html
```
```toc
:maxdepth: 2
[前言](chapter_preface/index)
# 基础篇
chapter_introduction/index
# 附录
[机器学习基础附录](appendix_machine_learning_introduction/index)
```
""",
encoding="utf-8",
)
chapter_preface = source / "chapter_preface"
chapter_preface.mkdir()
(chapter_preface / "index.md").write_text("# 前言\n", encoding="utf-8")
static_dir = source / "static"
static_dir.mkdir()
(static_dir / "frontpage.html").write_text(
"<div class=\"hero\">frontpage</div>\n",
encoding="utf-8",
)
chapter_intro = source / "chapter_introduction"
chapter_intro.mkdir()
(chapter_intro / "index.md").write_text(
"""# 导论
```toc
:maxdepth: 2
applications
design
```
""",
encoding="utf-8",
)
(chapter_intro / "applications.md").write_text("# 机器学习应用\n", encoding="utf-8")
(chapter_intro / "design.md").write_text("# 设计目标\n", encoding="utf-8")
appendix = source / "appendix_machine_learning_introduction"
appendix.mkdir()
(appendix / "index.md").write_text("# 机器学习基础附录\n", encoding="utf-8")
for name in ("img", "static", "references"):
(root / name).mkdir()
(root / "mlsys.bib").write_text("% bibliography\n", encoding="utf-8")
summary_path = write_summary(source)
summary = summary_path.read_text(encoding="utf-8")
self.assertEqual(
summary,
"""# Summary
[机器学习系统:设计和实现](index.md)
[前言](chapter_preface/index.md)
# 基础篇
- [导论](chapter_introduction/index.md)
- [机器学习应用](chapter_introduction/applications.md)
- [设计目标](chapter_introduction/design.md)
# 附录
- [机器学习基础附录](appendix_machine_learning_introduction/index.md)
""",
)
title_cache = {
(source / "chapter_preface" / "index.md").resolve(): "前言",
(source / "chapter_introduction" / "index.md").resolve(): "导论",
(source / "chapter_introduction" / "applications.md").resolve(): "机器学习应用",
(source / "chapter_introduction" / "design.md").resolve(): "设计目标",
(source / "appendix_machine_learning_introduction" / "index.md").resolve(): "机器学习基础附录",
}
root_index = rewrite_markdown((source / "index.md").read_text(encoding="utf-8"), (source / "index.md").resolve(), title_cache)
self.assertNotIn("```eval_rst", root_index)
self.assertNotIn("```toc", root_index)
self.assertIn("- [前言](chapter_preface/index.md)", root_index)
self.assertIn("- 基础篇", root_index)
self.assertIn(" - [导论](chapter_introduction/index.md)", root_index)
self.assertIn("- 附录", root_index)
self.assertIn(" - [机器学习基础附录](appendix_machine_learning_introduction/index.md)", root_index)
intro_index = rewrite_markdown(
(source / "chapter_introduction" / "index.md").read_text(encoding="utf-8"),
(source / "chapter_introduction" / "index.md").resolve(),
title_cache,
)
self.assertNotIn("```toc", intro_index)
self.assertIn("- [机器学习应用](applications.md)", intro_index)
self.assertIn("- [设计目标](design.md)", intro_index)
def test_write_summary_raises_for_missing_toc_entries(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
source = root / "zh_chapters"
source.mkdir()
(source / "index.md").write_text(
"""# 首页
```toc
:maxdepth: 2
existing
missing
```
""",
encoding="utf-8",
)
(source / "existing.md").write_text("# 现有章节\n", encoding="utf-8")
with self.assertRaises(FileNotFoundError):
write_summary(source)
def test_rewrite_markdown_normalizes_common_d2l_directives(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
source = root / "zh_chapters"
source.mkdir()
page = source / "chapter.md"
page.write_text(
"""# 标题
![配图](../img/example.png)
:width:`800px`
:label:`fig_example`
参见 :numref:`fig_example` 和公式 :eqref:`eq_example`,引用 :cite:`foo2024`。
""",
encoding="utf-8",
)
rewritten = rewrite_markdown(page.read_text(encoding="utf-8"), page.resolve(), {page.resolve(): "标题"})
self.assertNotIn(":width:", rewritten)
self.assertNotIn(":label:", rewritten)
self.assertNotIn(":numref:", rewritten)
self.assertNotIn(":eqref:", rewritten)
self.assertNotIn(":cite:", rewritten)
self.assertIn("`fig_example`", rewritten)
self.assertIn("`eq_example`", rewritten)
self.assertIn("[foo2024]", rewritten)
def test_rewrite_markdown_inlines_frontpage_html_include(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
source = root / "zh_chapters"
static_dir = source / "static"
static_dir.mkdir(parents=True)
index = source / "index.md"
index.write_text(
"""# 首页
```eval_rst
.. raw:: html
:file: frontpage.html
```
""",
encoding="utf-8",
)
(static_dir / "frontpage.html").write_text(
"""<head>
<style>
.hero { color: red; }
.other { color: blue; }
</style>
</head>
<div class="hero">
<img src="_images/logo.png" />
<img src="./_images/jinxuefeng.png" />
</div>
<script>console.log('frontpage');</script>
""",
encoding="utf-8",
)
rewritten = rewrite_markdown(index.read_text(encoding="utf-8"), index.resolve(), {index.resolve(): "首页"})
self.assertNotIn("```eval_rst", rewritten)
self.assertNotIn("<head>", rewritten)
self.assertIn('class="openmlsys-frontpage"', rewritten)
self.assertIn('<div class="hero">', rewritten)
self.assertIn('<style>', rewritten)
self.assertIn(".hero { color: red; } .other { color: blue; }", rewritten)
self.assertIn("static/image/logo.png", rewritten)
self.assertIn("static/image/jinxuefeng.png", rewritten)
self.assertIn("console.log('frontpage')", rewritten)
if __name__ == "__main__":
unittest.main()