#!/usr/bin/env python3 """Build the versioned business-instruction DOCX from its Markdown source. The existing DOCX is used only as a layout/style template. The document body is regenerated in full, so the Markdown file remains the single editable source. """ from __future__ import annotations import argparse import re from pathlib import Path from docx import Document from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Pt, RGBColor HEADING_STYLE = { 1: "Doc Title", 2: "Heading 1", 3: "Heading 2", 4: "Heading 3", } EAST_ASIA_FONT = "Arial Unicode MS" LATIN_FONT = "Arial Unicode MS" CODE_FONT = "Arial Unicode MS" def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--source", type=Path, required=True) parser.add_argument("--template", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--version", required=True) return parser.parse_args() def set_font(element, latin: str = LATIN_FONT, east_asia: str = EAST_ASIA_FONT) -> None: r_pr = element.get_or_add_rPr() r_fonts = r_pr.rFonts if r_fonts is None: r_fonts = OxmlElement("w:rFonts") r_pr.insert(0, r_fonts) r_fonts.set(qn("w:ascii"), latin) r_fonts.set(qn("w:hAnsi"), latin) r_fonts.set(qn("w:eastAsia"), east_asia) r_fonts.set(qn("w:cs"), latin) def configure_styles(doc: Document) -> None: for style_name in ( "Normal", "Subtitle", "Doc Title", "Heading 1", "Heading 2", "Heading 3", "Instruction List", "Reference Bullet", "Code Block", "Header", "Footer", ): style = doc.styles[style_name] latin = CODE_FONT if style_name == "Code Block" else LATIN_FONT style.font.name = latin set_font(style.element, latin=latin) doc.styles["Normal"].font.size = Pt(10.5) doc.styles["Subtitle"].font.size = Pt(9) doc.styles["Subtitle"].font.color.rgb = RGBColor(89, 99, 110) doc.styles["Doc Title"].font.size = Pt(22) doc.styles["Code Block"].font.size = Pt(9) for style_name in ( "Normal", "Subtitle", "Doc Title", "Heading 1", "Heading 2", "Heading 3", "Instruction List", "Reference Bullet", "Code Block", ): doc.styles[style_name].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.LEFT def clear_body(doc: Document) -> None: body = doc._element.body for child in list(body): if child.tag != qn("w:sectPr"): body.remove(child) def normalize_inline(text: str) -> str: return text.replace("`", "").replace(" ", " ").strip() def add_text(doc: Document, text: str, style: str) -> None: paragraph = doc.add_paragraph(style=style) run = paragraph.add_run(normalize_inline(text)) latin = CODE_FONT if style == "Code Block" else LATIN_FONT run.font.name = latin set_font(run._element, latin=latin) def add_code_block(doc: Document, lines: list[str]) -> None: paragraph = doc.add_paragraph(style="Code Block") for index, line in enumerate(lines): run = paragraph.add_run(line) run.font.name = CODE_FONT set_font(run._element, latin=CODE_FONT) if index < len(lines) - 1: run.add_break(WD_BREAK.LINE) def build_document(source: Path, template: Path, output: Path, version: str) -> None: markdown = source.read_text(encoding="utf-8") doc = Document(template) clear_body(doc) configure_styles(doc) lines = markdown.splitlines() code_lines: list[str] = [] in_code = False for raw_line in lines: stripped = raw_line.strip() if stripped.startswith("```"): if in_code: add_code_block(doc, code_lines) code_lines = [] in_code = not in_code continue if in_code: code_lines.append(raw_line.rstrip()) continue if not stripped: continue heading = re.match(r"^(#{1,4})\s+(.+)$", stripped) if heading: level = len(heading.group(1)) title = heading.group(2) if level == 1: title = f"{title} {version}" add_text(doc, title, HEADING_STYLE[level]) continue if stripped.startswith("交付版本:") or stripped.startswith("适用范围:"): add_text(doc, stripped, "Subtitle") continue if re.match(r"^\d+\.\s+", stripped): add_text(doc, stripped, "Instruction List") continue if stripped.startswith("- "): add_text(doc, f"• {stripped[2:]}", "Reference Bullet") continue add_text(doc, stripped, "Normal") if in_code: raise ValueError("Unclosed fenced code block in Markdown source") doc.core_properties.title = f"运营业务 AI 输入模板总表 {version}" doc.core_properties.subject = "老挝联泰 ERP 业务指令模板与示例" doc.core_properties.comments = ( f"Generated from {source.as_posix()} by tools/build_business_instruction_docx.py" ) output.parent.mkdir(parents=True, exist_ok=True) doc.save(output) def main() -> None: args = parse_args() build_document(args.source, args.template, args.output, args.version) if __name__ == "__main__": main()