132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from docx import Document
|
|
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
|
from docx.oxml import OxmlElement
|
|
from docx.oxml.ns import qn
|
|
from docx.shared import Pt
|
|
|
|
from common import get_bundle_defaults, normalize_bundle, read_json
|
|
|
|
|
|
FONT_NAME = "Microsoft YaHei"
|
|
TOC_HINT = "打开文档后右键更新目录"
|
|
HEADING_FONT_SIZES = {1: 16, 2: 14, 3: 12}
|
|
MAX_HEADING_LEVEL = 9
|
|
|
|
|
|
def set_run_font(run, size: int = 12, bold: bool = False) -> None:
|
|
run.bold = bold
|
|
run.font.size = Pt(size)
|
|
run.font.name = FONT_NAME
|
|
rpr = run._element.get_or_add_rPr()
|
|
rfonts = rpr.rFonts
|
|
if rfonts is None:
|
|
rfonts = OxmlElement("w:rFonts")
|
|
rpr.append(rfonts)
|
|
rfonts.set(qn("w:eastAsia"), FONT_NAME)
|
|
rfonts.set(qn("w:ascii"), FONT_NAME)
|
|
rfonts.set(qn("w:hAnsi"), FONT_NAME)
|
|
|
|
|
|
def configure_style(style, size: int, bold: bool = False) -> None:
|
|
style.font.name = FONT_NAME
|
|
style.font.size = Pt(size)
|
|
style.font.bold = bold
|
|
rpr = style._element.get_or_add_rPr()
|
|
rfonts = rpr.rFonts
|
|
if rfonts is None:
|
|
rfonts = OxmlElement("w:rFonts")
|
|
rpr.append(rfonts)
|
|
rfonts.set(qn("w:eastAsia"), FONT_NAME)
|
|
rfonts.set(qn("w:ascii"), FONT_NAME)
|
|
rfonts.set(qn("w:hAnsi"), FONT_NAME)
|
|
|
|
|
|
def add_toc(paragraph) -> None:
|
|
run = paragraph.add_run()
|
|
fld_begin = OxmlElement("w:fldChar")
|
|
fld_begin.set(qn("w:fldCharType"), "begin")
|
|
instr = OxmlElement("w:instrText")
|
|
instr.set(qn("xml:space"), "preserve")
|
|
instr.text = 'TOC \\o "1-9" \\h \\z \\u'
|
|
fld_sep = OxmlElement("w:fldChar")
|
|
fld_sep.set(qn("w:fldCharType"), "separate")
|
|
text = OxmlElement("w:t")
|
|
text.text = TOC_HINT
|
|
fld_end = OxmlElement("w:fldChar")
|
|
fld_end.set(qn("w:fldCharType"), "end")
|
|
run._r.append(fld_begin)
|
|
run._r.append(instr)
|
|
run._r.append(fld_sep)
|
|
run._r.append(text)
|
|
run._r.append(fld_end)
|
|
|
|
|
|
def add_cover(doc: Document, project_name: str, doc_title: str) -> None:
|
|
p = doc.add_paragraph()
|
|
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
|
set_run_font(p.add_run(project_name), 18, True)
|
|
|
|
p = doc.add_paragraph()
|
|
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
|
|
set_run_font(p.add_run(doc_title), 22, True)
|
|
|
|
|
|
def normalize_outline_item(item: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"title": item["title"],
|
|
"children": [normalize_outline_item(child) for child in item.get("children", [])],
|
|
}
|
|
|
|
|
|
def render_outline_nodes(doc: Document, items: list[dict[str, Any]], prefix: tuple[int, ...] = ()) -> None:
|
|
for index, item in enumerate(items, start=1):
|
|
numbers = prefix + (index,)
|
|
level = min(len(numbers), MAX_HEADING_LEVEL)
|
|
font_size = HEADING_FONT_SIZES.get(level, 12)
|
|
paragraph = doc.add_paragraph(style=f"Heading {level}")
|
|
set_run_font(paragraph.add_run(f"{'.'.join(str(number) for number in numbers)} {item['title']}"), font_size, True)
|
|
render_outline_nodes(doc, item.get("children", []), numbers)
|
|
|
|
|
|
def build_docx(outline_spec: dict[str, Any], out_path: Path) -> None:
|
|
doc = Document()
|
|
configure_style(doc.styles["Normal"], 12)
|
|
for level in range(1, MAX_HEADING_LEVEL + 1):
|
|
configure_style(doc.styles[f"Heading {level}"], HEADING_FONT_SIZES.get(level, 12), True)
|
|
|
|
bundle = normalize_bundle(outline_spec.get("bundle"))
|
|
bundle_defaults = get_bundle_defaults(bundle) if bundle else None
|
|
doc_title = outline_spec.get("doc_title") or (bundle_defaults["outline_doc_title"] if bundle_defaults else "投标文件(目录版)")
|
|
toc_title = outline_spec.get("toc_title") or (bundle_defaults["outline_toc_title"] if bundle_defaults else "目录")
|
|
|
|
add_cover(doc, outline_spec.get("project_name", "项目名称待补充"), doc_title)
|
|
doc.add_page_break()
|
|
title = doc.add_paragraph()
|
|
set_run_font(title.add_run(toc_title), 16, True)
|
|
add_toc(doc.add_paragraph())
|
|
doc.add_page_break()
|
|
|
|
sections = [normalize_outline_item(section) for section in outline_spec.get("sections", [])]
|
|
render_outline_nodes(doc, sections)
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
doc.save(str(out_path))
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--outline", required=True)
|
|
parser.add_argument("--out", required=True)
|
|
args = parser.parse_args()
|
|
|
|
build_docx(read_json(Path(args.outline).resolve()), Path(args.out).resolve())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|