Files
LWLT-AIBOT/tools/build_business_instruction_docx.py
2026-08-25 10:33:29 +08:00

189 lines
5.4 KiB
Python

#!/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()