# -*- coding: utf-8 -*- """从《项目交付验收方案.md》生成 Word 文档。""" from __future__ import annotations import re from pathlib import Path from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING from docx.oxml.ns import qn from docx.shared import Cm, Pt BASE = Path(__file__).resolve().parent SRC = BASE / "项目交付验收方案.md" OUT = BASE / "项目交付验收方案.docx" def set_run_font(run, name_cn="宋体", name_en="Times New Roman", size=12, bold=False): run.font.name = name_en run._element.rPr.rFonts.set(qn("w:eastAsia"), name_cn) run.font.size = Pt(size) run.bold = bold def style_normal(doc: Document): style = doc.styles["Normal"] style.font.name = "Times New Roman" style.font.size = Pt(12) style._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") pf = style.paragraph_format pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE pf.space_after = Pt(6) def add_title(doc: Document, text: str): p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(12) run = p.add_run(text) set_run_font(run, "黑体", "SimHei", 18, True) def add_h1(doc: Document, text: str): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(14) p.paragraph_format.space_after = Pt(8) run = p.add_run(text) set_run_font(run, "黑体", "SimHei", 14, True) def add_h2(doc: Document, text: str): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(10) p.paragraph_format.space_after = Pt(6) run = p.add_run(text) set_run_font(run, "黑体", "SimHei", 12, True) def add_rich_paragraph(doc: Document, text: str, *, indent=True, left_indent_cm=0.0): p = doc.add_paragraph() if left_indent_cm: p.paragraph_format.left_indent = Cm(left_indent_cm) p.paragraph_format.first_line_indent = Pt(0) else: p.paragraph_format.first_line_indent = Pt(24) if indent else Pt(0) p.paragraph_format.space_after = Pt(6) parts = re.split(r"(\*\*.*?\*\*|`[^`]+`)", text) for part in parts: if not part: continue if part.startswith("**") and part.endswith("**"): run = p.add_run(part[2:-2]) set_run_font(run, "宋体", "Times New Roman", 12, True) elif part.startswith("`") and part.endswith("`"): run = p.add_run(part[1:-1]) set_run_font(run, "宋体", "Consolas", 10.5) else: run = p.add_run(part) set_run_font(run, "宋体", "Times New Roman", 12) def add_table(doc: Document, headers: list[str], rows: list[list[str]], font_size=9.0): table = doc.add_table(rows=1 + len(rows), cols=len(headers)) table.style = "Table Grid" table.autofit = True for i, h in enumerate(headers): cell = table.rows[0].cells[i] cell.text = "" p = cell.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER run = p.add_run(h) set_run_font(run, "宋体", "Times New Roman", font_size, True) tcPr = cell._tc.get_or_add_tcPr() shd = tcPr.makeelement( qn("w:shd"), { qn("w:val"): "clear", qn("w:color"): "auto", qn("w:fill"): "D9E2F3", }, ) tcPr.append(shd) for r_idx, row in enumerate(rows): for c_idx, val in enumerate(row): cell = table.rows[r_idx + 1].cells[c_idx] cell.text = "" p = cell.paragraphs[0] # strip inline code markers in table cells for readability text = re.sub(r"`([^`]+)`", r"\1", str(val)) run = p.add_run(text) set_run_font(run, "宋体", "Times New Roman", font_size) doc.add_paragraph() def parse_table_block(lines: list[str], start: int) -> tuple[list[str], list[list[str]], int]: header = [c.strip() for c in lines[start].strip().strip("|").split("|")] i = start + 1 if i < len(lines) and re.match(r"^\|?\s*:?-{3,}", lines[i].strip()): i += 1 rows: list[list[str]] = [] while i < len(lines) and lines[i].strip().startswith("|"): row = [c.strip() for c in lines[i].strip().strip("|").split("|")] rows.append(row) i += 1 return header, rows, i def build_docx(md_text: str) -> Document: doc = Document() sec = doc.sections[0] sec.page_width = Cm(21.0) sec.page_height = Cm(29.7) sec.top_margin = Cm(2.54) sec.bottom_margin = Cm(2.54) sec.left_margin = Cm(2.5) sec.right_margin = Cm(2.5) style_normal(doc) lines = md_text.replace("\r\n", "\n").split("\n") i = 0 while i < len(lines): line = lines[i].rstrip() stripped = line.strip() if not stripped: i += 1 continue if stripped == "---": i += 1 continue if stripped.startswith("# "): add_title(doc, stripped[2:].strip()) i += 1 continue if stripped.startswith("## "): add_h1(doc, stripped[3:].strip()) i += 1 continue if stripped.startswith("### "): add_h2(doc, stripped[4:].strip()) i += 1 continue if stripped.startswith("|"): headers, rows, i = parse_table_block(lines, i) font = 8.5 if len(headers) >= 6 else 10.5 add_table(doc, headers, rows, font_size=font) continue m = re.match(r"^(\d+)\.\s+(.*)$", stripped) if m: add_rich_paragraph(doc, f"{m.group(1)}. {m.group(2)}", indent=False, left_indent_cm=0.5) i += 1 continue add_rich_paragraph(doc, stripped, indent=True) i += 1 return doc def main(): md = SRC.read_text(encoding="utf-8") doc = build_docx(md) doc.save(OUT) print(f"OK: {OUT}") if __name__ == "__main__": main()