#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Export user-facing guides (Markdown) to Word (.docx). Usage (from repo root): pip install python-docx python scripts/export_user_guide_office.py Outputs under docs/exports/ """ from __future__ import annotations import re from datetime import datetime, timezone from pathlib import Path from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.oxml.ns import qn from docx.shared import Cm, Pt ROOT = Path(__file__).resolve().parents[1] GUIDE_DIR = ROOT / "docs" / "user-guides" OUT_DIR = ROOT / "docs" / "exports" GUIDES = [ # (markdown path, output docx filename — English names avoid Windows garbling) ( GUIDE_DIR / "MTMS_排程與工單_使用說明.md", "MTMS_Schedule_JobOrder_UserGuide.docx", ), ( GUIDE_DIR / "MTMS_BOM_使用說明.md", "MTMS_BOM_UserGuide.docx", ), ( GUIDE_DIR / "MTMS_工單提料報工上架_使用說明.md", "MTMS_JO_Pick_Production_PutAway_UserGuide.docx", ), ( GUIDE_DIR / "MTMS_送貨訂單與出貨_使用說明.md", "MTMS_DO_Shipping_UserGuide.docx", ), ] def strip_md_inline(s: str) -> str: s = s.strip() s = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", s) s = s.replace("**", "").replace("`", "").replace("*", "") return s.strip() def add_runs(paragraph, text: str) -> None: parts = re.split(r"(「[^」]+」)", text) for part in parts: if not part: continue run = paragraph.add_run(part) run.font.name = "Calibri" run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei") if part.startswith("「") and part.endswith("」"): run.bold = True run.font.color.rgb = None def md_to_docx(md_path: Path, out_path: Path) -> None: doc = Document() section = doc.sections[0] section.top_margin = Cm(2) section.bottom_margin = Cm(2) section.left_margin = Cm(2.2) section.right_margin = Cm(2.2) style = doc.styles["Normal"] style.font.name = "Calibri" style.font.size = Pt(11) style._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft JhengHei") lines = md_path.read_text(encoding="utf-8").splitlines() i = 0 in_code = False code_buf: list[str] = [] while i < len(lines): line = lines[i] if line.strip().startswith("```"): if not in_code: in_code = True code_buf = [] else: in_code = False p = doc.add_paragraph() run = p.add_run("\n".join(code_buf)) run.font.name = "Consolas" run.font.size = Pt(9) i += 1 continue if in_code: code_buf.append(line) i += 1 continue if line.startswith("#"): level = len(re.match(r"^#+", line).group(0)) text = strip_md_inline(re.sub(r"^#+\s*", "", line)) doc.add_heading(text, level=0 if level == 1 else min(level, 3)) i += 1 continue if line.strip().startswith("|") and i + 1 < len(lines) and re.match( r"^\|[\s\-:|]+\|$", lines[i + 1].strip() ): header = [strip_md_inline(c) for c in line.strip().strip("|").split("|")] i += 2 rows: list[list[str]] = [] while i < len(lines) and lines[i].strip().startswith("|"): rows.append( [strip_md_inline(c) for c in lines[i].strip().strip("|").split("|")] ) i += 1 table = doc.add_table(rows=1 + len(rows), cols=len(header)) table.style = "Table Grid" for c, h in enumerate(header): cell = table.rows[0].cells[c] cell.text = h for p in cell.paragraphs: for r in p.runs: r.bold = True for r_idx, row in enumerate(rows): for c, val in enumerate(row): if c < len(header): table.rows[r_idx + 1].cells[c].text = val doc.add_paragraph() continue if line.strip().startswith("> "): p = doc.add_paragraph() p.paragraph_format.left_indent = Cm(0.4) add_runs(p, strip_md_inline(line.strip()[2:])) for r in p.runs: r.italic = True i += 1 continue if re.match(r"^[-*]\s+", line.strip()): p = doc.add_paragraph(style="List Bullet") add_runs(p, strip_md_inline(re.sub(r"^[-*]\s+", "", line.strip()))) i += 1 continue if re.match(r"^\d+\.\s+", line.strip()): p = doc.add_paragraph(style="List Number") add_runs(p, strip_md_inline(re.sub(r"^\d+\.\s+", "", line.strip()))) i += 1 continue if line.strip() in ("", "---"): i += 1 continue p = doc.add_paragraph() add_runs(p, strip_md_inline(line)) i += 1 footer = doc.sections[0].footer.paragraphs[0] footer.text = ( f"MTMS 使用說明 · {md_path.name} · " f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}" ) footer.alignment = WD_ALIGN_PARAGRAPH.CENTER out_path.parent.mkdir(parents=True, exist_ok=True) doc.save(out_path) print(f"Wrote {out_path.relative_to(ROOT)}") def main() -> None: OUT_DIR.mkdir(parents=True, exist_ok=True) for guide, out_name in GUIDES: if not guide.is_file(): print(f"Skip missing: {guide}") continue out = OUT_DIR / out_name md_to_docx(guide, out) if __name__ == "__main__": main()