Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 

194 řádky
5.7 KiB

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Export user-facing guides (Markdown) to Word (.docx).
  5. Usage (from repo root):
  6. pip install python-docx
  7. python scripts/export_user_guide_office.py
  8. Outputs under docs/exports/
  9. """
  10. from __future__ import annotations
  11. import re
  12. from datetime import datetime, timezone
  13. from pathlib import Path
  14. from docx import Document
  15. from docx.enum.text import WD_ALIGN_PARAGRAPH
  16. from docx.oxml.ns import qn
  17. from docx.shared import Cm, Pt
  18. ROOT = Path(__file__).resolve().parents[1]
  19. GUIDE_DIR = ROOT / "docs" / "user-guides"
  20. OUT_DIR = ROOT / "docs" / "exports"
  21. GUIDES = [
  22. # (markdown path, output docx filename — English names avoid Windows garbling)
  23. (
  24. GUIDE_DIR / "MTMS_排程與工單_使用說明.md",
  25. "MTMS_Schedule_JobOrder_UserGuide.docx",
  26. ),
  27. (
  28. GUIDE_DIR / "MTMS_BOM_使用說明.md",
  29. "MTMS_BOM_UserGuide.docx",
  30. ),
  31. (
  32. GUIDE_DIR / "MTMS_工單提料報工上架_使用說明.md",
  33. "MTMS_JO_Pick_Production_PutAway_UserGuide.docx",
  34. ),
  35. (
  36. GUIDE_DIR / "MTMS_送貨訂單與出貨_使用說明.md",
  37. "MTMS_DO_Shipping_UserGuide.docx",
  38. ),
  39. ]
  40. def strip_md_inline(s: str) -> str:
  41. s = s.strip()
  42. s = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", s)
  43. s = s.replace("**", "").replace("`", "").replace("*", "")
  44. return s.strip()
  45. def add_runs(paragraph, text: str) -> None:
  46. parts = re.split(r"(「[^」]+」)", text)
  47. for part in parts:
  48. if not part:
  49. continue
  50. run = paragraph.add_run(part)
  51. run.font.name = "Calibri"
  52. run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")
  53. if part.startswith("「") and part.endswith("」"):
  54. run.bold = True
  55. run.font.color.rgb = None
  56. def md_to_docx(md_path: Path, out_path: Path) -> None:
  57. doc = Document()
  58. section = doc.sections[0]
  59. section.top_margin = Cm(2)
  60. section.bottom_margin = Cm(2)
  61. section.left_margin = Cm(2.2)
  62. section.right_margin = Cm(2.2)
  63. style = doc.styles["Normal"]
  64. style.font.name = "Calibri"
  65. style.font.size = Pt(11)
  66. style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei")
  67. lines = md_path.read_text(encoding="utf-8").splitlines()
  68. i = 0
  69. in_code = False
  70. code_buf: list[str] = []
  71. while i < len(lines):
  72. line = lines[i]
  73. if line.strip().startswith("```"):
  74. if not in_code:
  75. in_code = True
  76. code_buf = []
  77. else:
  78. in_code = False
  79. p = doc.add_paragraph()
  80. run = p.add_run("\n".join(code_buf))
  81. run.font.name = "Consolas"
  82. run.font.size = Pt(9)
  83. i += 1
  84. continue
  85. if in_code:
  86. code_buf.append(line)
  87. i += 1
  88. continue
  89. if line.startswith("#"):
  90. level = len(re.match(r"^#+", line).group(0))
  91. text = strip_md_inline(re.sub(r"^#+\s*", "", line))
  92. doc.add_heading(text, level=0 if level == 1 else min(level, 3))
  93. i += 1
  94. continue
  95. if line.strip().startswith("|") and i + 1 < len(lines) and re.match(
  96. r"^\|[\s\-:|]+\|$", lines[i + 1].strip()
  97. ):
  98. header = [strip_md_inline(c) for c in line.strip().strip("|").split("|")]
  99. i += 2
  100. rows: list[list[str]] = []
  101. while i < len(lines) and lines[i].strip().startswith("|"):
  102. rows.append(
  103. [strip_md_inline(c) for c in lines[i].strip().strip("|").split("|")]
  104. )
  105. i += 1
  106. table = doc.add_table(rows=1 + len(rows), cols=len(header))
  107. table.style = "Table Grid"
  108. for c, h in enumerate(header):
  109. cell = table.rows[0].cells[c]
  110. cell.text = h
  111. for p in cell.paragraphs:
  112. for r in p.runs:
  113. r.bold = True
  114. for r_idx, row in enumerate(rows):
  115. for c, val in enumerate(row):
  116. if c < len(header):
  117. table.rows[r_idx + 1].cells[c].text = val
  118. doc.add_paragraph()
  119. continue
  120. if line.strip().startswith("> "):
  121. p = doc.add_paragraph()
  122. p.paragraph_format.left_indent = Cm(0.4)
  123. add_runs(p, strip_md_inline(line.strip()[2:]))
  124. for r in p.runs:
  125. r.italic = True
  126. i += 1
  127. continue
  128. if re.match(r"^[-*]\s+", line.strip()):
  129. p = doc.add_paragraph(style="List Bullet")
  130. add_runs(p, strip_md_inline(re.sub(r"^[-*]\s+", "", line.strip())))
  131. i += 1
  132. continue
  133. if re.match(r"^\d+\.\s+", line.strip()):
  134. p = doc.add_paragraph(style="List Number")
  135. add_runs(p, strip_md_inline(re.sub(r"^\d+\.\s+", "", line.strip())))
  136. i += 1
  137. continue
  138. if line.strip() in ("", "---"):
  139. i += 1
  140. continue
  141. p = doc.add_paragraph()
  142. add_runs(p, strip_md_inline(line))
  143. i += 1
  144. footer = doc.sections[0].footer.paragraphs[0]
  145. footer.text = (
  146. f"MTMS 使用說明 · {md_path.name} · "
  147. f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}"
  148. )
  149. footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
  150. out_path.parent.mkdir(parents=True, exist_ok=True)
  151. doc.save(out_path)
  152. print(f"Wrote {out_path.relative_to(ROOT)}")
  153. def main() -> None:
  154. OUT_DIR.mkdir(parents=True, exist_ok=True)
  155. for guide, out_name in GUIDES:
  156. if not guide.is_file():
  157. print(f"Skip missing: {guide}")
  158. continue
  159. out = OUT_DIR / out_name
  160. md_to_docx(guide, out)
  161. if __name__ == "__main__":
  162. main()