chapter_paths.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. #!/usr/bin/env python3
  2. """
  3. Chapter file path helpers.
  4. This project has seen multiple chapter filename conventions:
  5. 1) Legacy flat layout: 正文/第0007章.md
  6. 2) Volume layout: 正文/第1卷/第007章-章节标题.md
  7. To keep scripts robust, always resolve chapter files via these helpers instead of hardcoding a format.
  8. """
  9. from __future__ import annotations
  10. import re
  11. from pathlib import Path
  12. from typing import Optional
  13. _CHAPTER_NUM_RE = re.compile(r"第(?P<num>\d+)章")
  14. def volume_num_for_chapter(chapter_num: int, *, chapters_per_volume: int = 50) -> int:
  15. if chapter_num <= 0:
  16. raise ValueError("chapter_num must be >= 1")
  17. return (chapter_num - 1) // chapters_per_volume + 1
  18. def extract_chapter_num_from_filename(filename: str) -> Optional[int]:
  19. m = _CHAPTER_NUM_RE.search(filename)
  20. if not m:
  21. return None
  22. try:
  23. return int(m.group("num"))
  24. except ValueError:
  25. return None
  26. def find_chapter_file(project_root: Path, chapter_num: int) -> Optional[Path]:
  27. """
  28. Find an existing chapter file for chapter_num under project_root/正文.
  29. Returns the first match (stable sorted order) or None if not found.
  30. """
  31. chapters_dir = project_root / "正文"
  32. if not chapters_dir.exists():
  33. return None
  34. legacy = chapters_dir / f"第{chapter_num:04d}章.md"
  35. if legacy.exists():
  36. return legacy
  37. vol_dir = chapters_dir / f"第{volume_num_for_chapter(chapter_num)}卷"
  38. if vol_dir.exists():
  39. candidates = sorted(vol_dir.glob(f"第{chapter_num:03d}章*.md")) + sorted(vol_dir.glob(f"第{chapter_num:04d}章*.md"))
  40. for c in candidates:
  41. if c.is_file():
  42. return c
  43. # Fallback: search anywhere under 正文/ (supports custom layouts)
  44. candidates = sorted(chapters_dir.rglob(f"第{chapter_num:03d}章*.md")) + sorted(chapters_dir.rglob(f"第{chapter_num:04d}章*.md"))
  45. for c in candidates:
  46. if c.is_file():
  47. return c
  48. return None
  49. def default_chapter_draft_path(project_root: Path, chapter_num: int, *, use_volume_layout: bool = False) -> Path:
  50. """
  51. Preferred draft path when creating a new chapter file.
  52. Args:
  53. project_root: 项目根目录
  54. chapter_num: 章节号
  55. use_volume_layout: True 使用卷布局 (正文/第N卷/第NNN章.md),False 使用平坦布局 (正文/第NNNN章.md)
  56. Default is flat layout to match SKILL.md documentation.
  57. """
  58. if use_volume_layout:
  59. vol_dir = project_root / "正文" / f"第{volume_num_for_chapter(chapter_num)}卷"
  60. return vol_dir / f"第{chapter_num:03d}章.md"
  61. else:
  62. # Flat layout: 正文/第NNNN章.md (matches SKILL.md)
  63. return project_root / "正文" / f"第{chapter_num:04d}章.md"