189 lines
5.4 KiB
Python
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()
|