mirror of
https://github.com/openmlsys/openmlsys-zh.git
synced 2026-07-17 03:21:20 +08:00
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:
87
tests/test_assemble_docs_publish_tree.py
Normal file
87
tests/test_assemble_docs_publish_tree.py
Normal 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()
|
||||
55
tests/test_ensure_book_resources.py
Normal file
55
tests/test_ensure_book_resources.py
Normal 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()
|
||||
114
tests/test_prepare_mdbook.py
Normal file
114
tests/test_prepare_mdbook.py
Normal 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()
|
||||
227
tests/test_prepare_mdbook_zh.py
Normal file
227
tests/test_prepare_mdbook_zh.py
Normal 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(
|
||||
"""# 标题
|
||||
|
||||

|
||||
: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()
|
||||
Reference in New Issue
Block a user