_gen_acceptance_plan_docx.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. # -*- coding: utf-8 -*-
  2. """从《项目交付验收方案.md》生成 Word 文档。"""
  3. from __future__ import annotations
  4. import re
  5. from pathlib import Path
  6. from docx import Document
  7. from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
  8. from docx.oxml.ns import qn
  9. from docx.shared import Cm, Pt
  10. BASE = Path(__file__).resolve().parent
  11. SRC = BASE / "项目交付验收方案.md"
  12. OUT = BASE / "项目交付验收方案.docx"
  13. def set_run_font(run, name_cn="宋体", name_en="Times New Roman", size=12, bold=False):
  14. run.font.name = name_en
  15. run._element.rPr.rFonts.set(qn("w:eastAsia"), name_cn)
  16. run.font.size = Pt(size)
  17. run.bold = bold
  18. def style_normal(doc: Document):
  19. style = doc.styles["Normal"]
  20. style.font.name = "Times New Roman"
  21. style.font.size = Pt(12)
  22. style._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
  23. pf = style.paragraph_format
  24. pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
  25. pf.space_after = Pt(6)
  26. def add_title(doc: Document, text: str):
  27. p = doc.add_paragraph()
  28. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  29. p.paragraph_format.space_after = Pt(12)
  30. run = p.add_run(text)
  31. set_run_font(run, "黑体", "SimHei", 18, True)
  32. def add_h1(doc: Document, text: str):
  33. p = doc.add_paragraph()
  34. p.paragraph_format.space_before = Pt(14)
  35. p.paragraph_format.space_after = Pt(8)
  36. run = p.add_run(text)
  37. set_run_font(run, "黑体", "SimHei", 14, True)
  38. def add_h2(doc: Document, text: str):
  39. p = doc.add_paragraph()
  40. p.paragraph_format.space_before = Pt(10)
  41. p.paragraph_format.space_after = Pt(6)
  42. run = p.add_run(text)
  43. set_run_font(run, "黑体", "SimHei", 12, True)
  44. def add_rich_paragraph(doc: Document, text: str, *, indent=True, left_indent_cm=0.0):
  45. p = doc.add_paragraph()
  46. if left_indent_cm:
  47. p.paragraph_format.left_indent = Cm(left_indent_cm)
  48. p.paragraph_format.first_line_indent = Pt(0)
  49. else:
  50. p.paragraph_format.first_line_indent = Pt(24) if indent else Pt(0)
  51. p.paragraph_format.space_after = Pt(6)
  52. parts = re.split(r"(\*\*.*?\*\*|`[^`]+`)", text)
  53. for part in parts:
  54. if not part:
  55. continue
  56. if part.startswith("**") and part.endswith("**"):
  57. run = p.add_run(part[2:-2])
  58. set_run_font(run, "宋体", "Times New Roman", 12, True)
  59. elif part.startswith("`") and part.endswith("`"):
  60. run = p.add_run(part[1:-1])
  61. set_run_font(run, "宋体", "Consolas", 10.5)
  62. else:
  63. run = p.add_run(part)
  64. set_run_font(run, "宋体", "Times New Roman", 12)
  65. def add_table(doc: Document, headers: list[str], rows: list[list[str]], font_size=9.0):
  66. table = doc.add_table(rows=1 + len(rows), cols=len(headers))
  67. table.style = "Table Grid"
  68. table.autofit = True
  69. for i, h in enumerate(headers):
  70. cell = table.rows[0].cells[i]
  71. cell.text = ""
  72. p = cell.paragraphs[0]
  73. p.alignment = WD_ALIGN_PARAGRAPH.CENTER
  74. run = p.add_run(h)
  75. set_run_font(run, "宋体", "Times New Roman", font_size, True)
  76. tcPr = cell._tc.get_or_add_tcPr()
  77. shd = tcPr.makeelement(
  78. qn("w:shd"),
  79. {
  80. qn("w:val"): "clear",
  81. qn("w:color"): "auto",
  82. qn("w:fill"): "D9E2F3",
  83. },
  84. )
  85. tcPr.append(shd)
  86. for r_idx, row in enumerate(rows):
  87. for c_idx, val in enumerate(row):
  88. cell = table.rows[r_idx + 1].cells[c_idx]
  89. cell.text = ""
  90. p = cell.paragraphs[0]
  91. # strip inline code markers in table cells for readability
  92. text = re.sub(r"`([^`]+)`", r"\1", str(val))
  93. run = p.add_run(text)
  94. set_run_font(run, "宋体", "Times New Roman", font_size)
  95. doc.add_paragraph()
  96. def parse_table_block(lines: list[str], start: int) -> tuple[list[str], list[list[str]], int]:
  97. header = [c.strip() for c in lines[start].strip().strip("|").split("|")]
  98. i = start + 1
  99. if i < len(lines) and re.match(r"^\|?\s*:?-{3,}", lines[i].strip()):
  100. i += 1
  101. rows: list[list[str]] = []
  102. while i < len(lines) and lines[i].strip().startswith("|"):
  103. row = [c.strip() for c in lines[i].strip().strip("|").split("|")]
  104. rows.append(row)
  105. i += 1
  106. return header, rows, i
  107. def build_docx(md_text: str) -> Document:
  108. doc = Document()
  109. sec = doc.sections[0]
  110. sec.page_width = Cm(21.0)
  111. sec.page_height = Cm(29.7)
  112. sec.top_margin = Cm(2.54)
  113. sec.bottom_margin = Cm(2.54)
  114. sec.left_margin = Cm(2.5)
  115. sec.right_margin = Cm(2.5)
  116. style_normal(doc)
  117. lines = md_text.replace("\r\n", "\n").split("\n")
  118. i = 0
  119. while i < len(lines):
  120. line = lines[i].rstrip()
  121. stripped = line.strip()
  122. if not stripped:
  123. i += 1
  124. continue
  125. if stripped == "---":
  126. i += 1
  127. continue
  128. if stripped.startswith("# "):
  129. add_title(doc, stripped[2:].strip())
  130. i += 1
  131. continue
  132. if stripped.startswith("## "):
  133. add_h1(doc, stripped[3:].strip())
  134. i += 1
  135. continue
  136. if stripped.startswith("### "):
  137. add_h2(doc, stripped[4:].strip())
  138. i += 1
  139. continue
  140. if stripped.startswith("|"):
  141. headers, rows, i = parse_table_block(lines, i)
  142. font = 8.5 if len(headers) >= 6 else 10.5
  143. add_table(doc, headers, rows, font_size=font)
  144. continue
  145. m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
  146. if m:
  147. add_rich_paragraph(doc, f"{m.group(1)}. {m.group(2)}", indent=False, left_indent_cm=0.5)
  148. i += 1
  149. continue
  150. add_rich_paragraph(doc, stripped, indent=True)
  151. i += 1
  152. return doc
  153. def main():
  154. md = SRC.read_text(encoding="utf-8")
  155. doc = build_docx(md)
  156. doc.save(OUT)
  157. print(f"OK: {OUT}")
  158. if __name__ == "__main__":
  159. main()