Coverage for src\genesis\parsers\word_template_parser.py: 100%
32 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1from __future__ import annotations
3import re
4from pathlib import Path
6from docx import Document
7from docx.oxml.ns import qn
9from genesis.data_models import ChapterMarker, ParsedTemplate
10from genesis.parsers._word_common import heading_level
12# 统一占位符正则:{{键名}} 或 {{键名:章节名}}(spec §3.2)。
13# 宽容:键名大小写不敏感、分隔符支持半角(:)/全角(:)冒号;解析时归一为小写键名 + 半角冒号。
14PLACEHOLDER_RE = re.compile(
15 r"\{\{([A-Za-z][A-Za-z0-9_]*)(?:[::]([^}]+?))?\}\}",
16 re.IGNORECASE,
17)
20class WordTemplateParser:
21 """概要设计模板 docx 解析:章构成 / 占位符 / 样式名提取。"""
23 def parse(self, path: str | Path) -> ParsedTemplate:
24 doc = Document(str(path))
25 sections: list[ChapterMarker] = []
26 placeholders: dict[str, str] = {}
27 used_styles: set[str] = set()
29 # 文档命名样式(定义集合)
30 defined = {s.name for s in doc.styles if s.name}
32 for para in doc.paragraphs:
33 style_name = para.style.name if para.style else "Normal"
34 used_styles.add(style_name)
35 text = para.text
37 if style_name.startswith("Heading"):
38 sections.append(ChapterMarker(
39 type="heading", name=text, level=heading_level(style_name)
40 ))
42 for m in PLACEHOLDER_RE.finditer(text):
43 if m.group(2):
44 key = f"{m.group(1).lower()}:{m.group(2).strip()}"
45 else:
46 key = m.group(1).lower()
47 placeholders[key] = text
48 sections.append(ChapterMarker(type="placeholder", name=key, level=0))
50 # 书签:遍历 body 中全部 bookmarkStart
51 for bm in doc.element.body.iter(qn("w:bookmarkStart")):
52 name = bm.get(qn("w:name"))
53 if name:
54 sections.append(ChapterMarker(type="bookmark", name=name, level=0))
56 return ParsedTemplate(
57 file_name=Path(path).name,
58 sections=sections,
59 placeholders=placeholders,
60 styles={"defined": sorted(defined), "used": sorted(used_styles)},
61 )