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

1from __future__ import annotations 

2 

3import re 

4from pathlib import Path 

5 

6from docx import Document 

7from docx.oxml.ns import qn 

8 

9from genesis.data_models import ChapterMarker, ParsedTemplate 

10from genesis.parsers._word_common import heading_level 

11 

12# 统一占位符正则:{{键名}} 或 {{键名:章节名}}(spec §3.2)。 

13# 宽容:键名大小写不敏感、分隔符支持半角(:)/全角(:)冒号;解析时归一为小写键名 + 半角冒号。 

14PLACEHOLDER_RE = re.compile( 

15 r"\{\{([A-Za-z][A-Za-z0-9_]*)(?:[::]([^}]+?))?\}\}", 

16 re.IGNORECASE, 

17) 

18 

19 

20class WordTemplateParser: 

21 """概要设计模板 docx 解析:章构成 / 占位符 / 样式名提取。""" 

22 

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() 

28 

29 # 文档命名样式(定义集合) 

30 defined = {s.name for s in doc.styles if s.name} 

31 

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 

36 

37 if style_name.startswith("Heading"): 

38 sections.append(ChapterMarker( 

39 type="heading", name=text, level=heading_level(style_name) 

40 )) 

41 

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)) 

49 

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)) 

55 

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 )