|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- #!/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()
|