Coverage for src\genesis\parsers\rule_doc_parser.py: 100%

45 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 14:20 +0800

1from __future__ import annotations 

2 

3import hashlib 

4from pathlib import Path 

5 

6from docx import Document 

7from docx.oxml.ns import qn 

8 

9from genesis.data_models import RuleDocument 

10from genesis.parsers._word_common import heading_level 

11 

12# 列表项前缀(真实样本为 Normal 样式 + ・ 前缀,样式名通道不足以命中) 

13LIST_PREFIXES = ("・", "-", "•") 

14 

15 

16def _is_list_item(text: str, style_name: str) -> bool: 

17 """双通道列表检测:List 样式或文本前缀(spec §3.3)。""" 

18 if "List" in style_name: 

19 return True 

20 return text.startswith(LIST_PREFIXES) 

21 

22 

23def _table_to_markdown(table) -> list[str]: 

24 """docx 表格 → GFM 表格行。""" 

25 lines: list[str] = [] 

26 for r_idx, row in enumerate(table.rows): 

27 cells = [cell.text.replace("|", "\\|").strip() for cell in row.cells] 

28 lines.append("| " + " | ".join(cells) + " |") 

29 if r_idx == 0: 

30 lines.append("| " + " | ".join(["---"] * len(cells)) + " |") 

31 return lines 

32 

33 

34class RuleDocParser: 

35 """规则文档 docx 解析:Markdown 化 + 分类。""" 

36 

37 def parse(self, path: str | Path, category: str = "write") -> RuleDocument: 

38 doc = Document(str(path)) 

39 md: list[str] = [] 

40 

41 # 按文档顺序遍历段落与表格(body 级子元素) 

42 for child in doc.element.body.iterchildren(): 

43 tag = child.tag 

44 if tag == qn("w:p"): 

45 from docx.text.paragraph import Paragraph 

46 para = Paragraph(child, doc) 

47 text = para.text 

48 style_name = para.style.name if para.style else "Normal" 

49 stripped = text.strip() 

50 if not stripped: 

51 md.append("") 

52 elif style_name.startswith("Heading"): 

53 md.append("#" * heading_level(style_name) + " " + stripped) 

54 elif _is_list_item(stripped, style_name): 

55 md.append("- " + stripped.lstrip("・-•").strip()) 

56 else: 

57 md.append(stripped) 

58 elif tag == qn("w:tbl"): 

59 from docx.table import Table 

60 md.extend(_table_to_markdown(Table(child, doc))) 

61 md.append("") 

62 

63 content = "\n".join(md).strip() 

64 return RuleDocument( 

65 file_name=Path(path).name, 

66 category=category, 

67 markdown_content=content, 

68 source_path=str(path), 

69 file_type="word", 

70 hash=hashlib.sha256(Path(path).read_bytes()).hexdigest(), 

71 )