md_to_docx.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python3
  2. """将操作说明 Markdown 转为 docx(供交付文档使用)。"""
  3. import re
  4. import sys
  5. from pathlib import Path
  6. from docx import Document
  7. from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
  8. from docx.oxml.ns import qn
  9. from docx.shared import Pt
  10. def set_doc_font(doc):
  11. style = doc.styles["Normal"]
  12. style.font.name = "宋体"
  13. style.font.size = Pt(11)
  14. style._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
  15. def add_rich_text(paragraph, text):
  16. pattern = re.compile(r"(\*\*[^*]+\*\*)")
  17. parts = pattern.split(text)
  18. for part in parts:
  19. if not part:
  20. continue
  21. if part.startswith("**") and part.endswith("**"):
  22. run = paragraph.add_run(part[2:-2])
  23. run.bold = True
  24. else:
  25. paragraph.add_run(part)
  26. def parse_table_row(line):
  27. line = line.strip()
  28. if not line.startswith("|"):
  29. return None
  30. cells = [c.strip() for c in line.strip("|").split("|")]
  31. return cells
  32. def is_separator_row(cells):
  33. return all(re.fullmatch(r":?-+:?", c.replace(" ", "")) for c in cells if c != "")
  34. def md_to_docx(md_path, docx_path):
  35. lines = Path(md_path).read_text(encoding="utf-8").splitlines()
  36. doc = Document()
  37. set_doc_font(doc)
  38. i = 0
  39. while i < len(lines):
  40. line = lines[i]
  41. stripped = line.strip()
  42. if not stripped:
  43. i += 1
  44. continue
  45. if stripped == "---":
  46. doc.add_paragraph("")
  47. i += 1
  48. continue
  49. if stripped.startswith("#"):
  50. level = len(stripped) - len(stripped.lstrip("#"))
  51. title = stripped[level:].strip()
  52. style = "Title" if level == 1 else f"Heading {min(level, 4)}"
  53. p = doc.add_paragraph(style=style)
  54. add_rich_text(p, title)
  55. i += 1
  56. continue
  57. if stripped.startswith("```"):
  58. lang = stripped[3:].strip()
  59. i += 1
  60. code_lines = []
  61. while i < len(lines) and not lines[i].strip().startswith("```"):
  62. code_lines.append(lines[i])
  63. i += 1
  64. if i < len(lines):
  65. i += 1
  66. p = doc.add_paragraph()
  67. run = p.add_run("\n".join(code_lines))
  68. run.font.name = "Consolas"
  69. run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
  70. run.font.size = Pt(9)
  71. doc.add_paragraph("")
  72. continue
  73. if stripped.startswith("|"):
  74. table_lines = []
  75. while i < len(lines) and lines[i].strip().startswith("|"):
  76. row = parse_table_row(lines[i])
  77. if row is not None:
  78. table_lines.append(row)
  79. i += 1
  80. data_rows = []
  81. for row in table_lines:
  82. if is_separator_row(row):
  83. continue
  84. data_rows.append(row)
  85. if not data_rows:
  86. continue
  87. cols = max(len(r) for r in data_rows)
  88. table = doc.add_table(rows=len(data_rows), cols=cols)
  89. table.style = "Table Grid"
  90. for r_idx, row in enumerate(data_rows):
  91. for c_idx in range(cols):
  92. cell_text = row[c_idx] if c_idx < len(row) else ""
  93. cell = table.rows[r_idx].cells[c_idx]
  94. cell.text = ""
  95. p = cell.paragraphs[0]
  96. add_rich_text(p, cell_text)
  97. doc.add_paragraph("")
  98. continue
  99. if stripped.startswith(">"):
  100. p = doc.add_paragraph(style="Intense Quote")
  101. add_rich_text(p, stripped.lstrip("> ").strip())
  102. i += 1
  103. continue
  104. if stripped.startswith("- "):
  105. p = doc.add_paragraph(style="List Bullet")
  106. add_rich_text(p, stripped[2:].strip())
  107. i += 1
  108. continue
  109. p = doc.add_paragraph()
  110. add_rich_text(p, stripped)
  111. i += 1
  112. for section in doc.sections:
  113. section.top_margin = Pt(72)
  114. section.bottom_margin = Pt(72)
  115. section.left_margin = Pt(90)
  116. section.right_margin = Pt(90)
  117. doc.save(docx_path)
  118. print(f"Generated: {docx_path}")
  119. def main():
  120. base = Path(__file__).parent
  121. pairs = [
  122. ("OPERATION_MANUAL_HEALTH.md", "OPERATION_MANUAL_HEALTH.docx"),
  123. ("OPERATION_MANUAL_ANOMALY.md", "OPERATION_MANUAL_ANOMALY.docx"),
  124. ("OPERATION_MANUAL_ANOMALY.md", "异常检测及风资源-操作说明.docx"),
  125. ("OPERATION_MANUAL_LEDGER.md", "OPERATION_MANUAL_LEDGER.docx"),
  126. ("OPERATION_MANUAL_LEDGER.md", "台账管理-操作说明.docx"),
  127. ("DEPLOY_FRONTEND.md", "DEPLOY_FRONTEND.docx"),
  128. ("DESIGN_SPEC_HEALTH.md", "健康评估系统设计说明.docx"),
  129. ("DESIGN_SPEC_ANOMALY_WRW.md", "异常检测及风资源尾流系统设计说明.docx"),
  130. ]
  131. for md_name, docx_name in pairs:
  132. md_to_docx(base / md_name, base / docx_name)
  133. if __name__ == "__main__":
  134. main()