|
1 | | -"""MkDocs 构建钩子:清理搜索索引中的零宽空格,避免中文搜索失效;将 Frontmatter 中的 synonyms 注入搜索索引;生成基于 Frontmatter updated 字段的最近更新列表。""" |
| 1 | +"""MkDocs 构建钩子: |
| 2 | +
|
| 3 | +- 清理搜索索引中的零宽空格,避免中文搜索失效; |
| 4 | +- 将 Frontmatter 中的 synonyms 注入搜索索引; |
| 5 | +- 生成基于 Frontmatter updated 字段的最近更新列表; |
| 6 | +- 从 changelog.md 解析最近发布版本并在首页注入“最近更新”。 |
| 7 | +""" |
2 | 8 |
|
3 | 9 | from __future__ import annotations |
4 | 10 |
|
5 | 11 | import json |
6 | 12 | import re |
7 | 13 | from datetime import datetime |
| 14 | +from dataclasses import dataclass |
8 | 15 | from pathlib import Path |
9 | 16 | from typing import Any, Dict |
10 | 17 |
|
11 | 18 | import yaml |
12 | 19 |
|
13 | 20 | ZERO_WIDTH_SPACE = "\u200b" |
14 | 21 | RECENTLY_UPDATED_PLACEHOLDER = "<!-- RECENTLY_UPDATED_DOCS -->" |
| 22 | +RECENT_RELEASES_PLACEHOLDER = "<!-- RECENT_RELEASES -->" |
15 | 23 |
|
16 | 24 |
|
17 | 25 | def _strip_zero_width(value: str) -> str: |
@@ -136,11 +144,24 @@ def _generate_recently_updated_html(docs_dir: Path, limit: int = 100) -> str: |
136 | 144 |
|
137 | 145 |
|
138 | 146 | def on_page_markdown(markdown: str, page: Any, config: Dict[str, Any], files: Any) -> str: |
139 | | - """在页面 Markdown 处理前替换最近更新占位符。""" |
| 147 | + """在页面 Markdown 处理前替换占位符。""" |
| 148 | + docs_dir = Path(config.get("docs_dir", "docs")) |
| 149 | + |
| 150 | + # 1) 最近更新(基于 Frontmatter.updated) |
140 | 151 | if RECENTLY_UPDATED_PLACEHOLDER in markdown: |
141 | | - docs_dir = Path(config.get("docs_dir", "docs")) |
142 | 152 | recently_updated_html = _generate_recently_updated_html(docs_dir, limit=100) |
143 | 153 | markdown = markdown.replace(RECENTLY_UPDATED_PLACEHOLDER, recently_updated_html) |
| 154 | + |
| 155 | + # 2) 最近发布版本(基于 docs/changelog.md) |
| 156 | + if RECENT_RELEASES_PLACEHOLDER in markdown: |
| 157 | + try: |
| 158 | + releases = _parse_recent_releases(docs_dir / "changelog.md", limit=3) |
| 159 | + recent_md = _render_recent_releases_md(releases) |
| 160 | + markdown = markdown.replace(RECENT_RELEASES_PLACEHOLDER, recent_md) |
| 161 | + except Exception: |
| 162 | + # 解析失败时保持原占位符,避免构建中断 |
| 163 | + pass |
| 164 | + |
144 | 165 | return markdown |
145 | 166 |
|
146 | 167 |
|
@@ -193,3 +214,73 @@ def on_post_build(config: Dict[str, Any]) -> None: |
193 | 214 | json.dumps(data, ensure_ascii=False, separators=(",", ":")), |
194 | 215 | encoding="utf-8", |
195 | 216 | ) |
| 217 | + |
| 218 | + |
| 219 | +# ===== 最近发布版本:从 changelog.md 解析并渲染到首页 ===== |
| 220 | + |
| 221 | +@dataclass |
| 222 | +class Release: |
| 223 | + tag: str # 例如 v3.15.0 |
| 224 | + title: str # 例如 角色体系扩充与内容格式标准化 |
| 225 | + date: str # 例如 2025-10-18 |
| 226 | + anchor: str # 例如 v3150---角色体系扩充与内容格式标准化-2025-10-18 |
| 227 | + |
| 228 | + |
| 229 | +def _slugify_heading(text: str) -> str: |
| 230 | + """使用与站点一致的规则生成锚点 slug。 |
| 231 | +
|
| 232 | + mkdocs.yml 中配置了 toc.slugify=pymdownx.slugs.slugify(case=lower-ascii), |
| 233 | + 这里对同样的纯文本进行 slug 化,确保首页锚点与 changelog 一致。 |
| 234 | + """ |
| 235 | + try: |
| 236 | + from pymdownx.slugs import slugify as _slugify_factory # type: ignore |
| 237 | + |
| 238 | + slugify_fn = _slugify_factory(case="lower-ascii") |
| 239 | + return slugify_fn(text, "-") |
| 240 | + except Exception: |
| 241 | + # 回退:保守处理,仅做最基本的替换 |
| 242 | + s = re.sub(r"\s+", "-", text.strip()) |
| 243 | + s = s.replace("/", "-") |
| 244 | + return s |
| 245 | + |
| 246 | + |
| 247 | +def _parse_recent_releases(changelog_path: Path, limit: int = 3) -> list[Release]: |
| 248 | + """从 docs/changelog.md 解析最近的发布版本信息。""" |
| 249 | + if not changelog_path.exists(): |
| 250 | + return [] |
| 251 | + |
| 252 | + releases: list[Release] = [] |
| 253 | + pattern = re.compile( |
| 254 | + r"^##\s*\[(v\d+\.\d+\.\d+)\](?:\([^)]+\))?\s*-\s*(.+?)\s*\((\d{4}-\d{2}-\d{2})\)\s*$" |
| 255 | + ) |
| 256 | + |
| 257 | + for line in changelog_path.read_text(encoding="utf-8").splitlines(): |
| 258 | + m = pattern.match(line) |
| 259 | + if not m: |
| 260 | + continue |
| 261 | + |
| 262 | + tag, title, date = m.group(1), m.group(2), m.group(3) |
| 263 | + |
| 264 | + # 构造用于 slugify 的纯文本标题(与渲染后的可见文本一致) |
| 265 | + heading_text = f"{tag} - {title} ({date})" |
| 266 | + anchor = _slugify_heading(heading_text) |
| 267 | + releases.append(Release(tag=tag, title=title, date=date, anchor=anchor)) |
| 268 | + |
| 269 | + if len(releases) >= limit: |
| 270 | + break |
| 271 | + |
| 272 | + return releases |
| 273 | + |
| 274 | + |
| 275 | +def _render_recent_releases_md(releases: list[Release]) -> str: |
| 276 | + """渲染最近发布版本为首页使用的 Markdown 列表。""" |
| 277 | + if not releases: |
| 278 | + return "- 暂无发布版本" |
| 279 | + |
| 280 | + lines = [] |
| 281 | + for r in releases: |
| 282 | + # 使用 changelog.md 的锚点链接 |
| 283 | + lines.append( |
| 284 | + f"- [{r.tag}({r.date}):{r.title}](changelog.md#{r.anchor})" |
| 285 | + ) |
| 286 | + return "\n".join(lines) |
0 commit comments