| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- # -*- 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()
|