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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1from __future__ import annotations
3import hashlib
4from pathlib import Path
6from docx import Document
7from docx.oxml.ns import qn
9from genesis.data_models import RuleDocument
10from genesis.parsers._word_common import heading_level
12# 列表项前缀(真实样本为 Normal 样式 + ・ 前缀,样式名通道不足以命中)
13LIST_PREFIXES = ("・", "-", "•")
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)
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
34class RuleDocParser:
35 """规则文档 docx 解析:Markdown 化 + 分类。"""
37 def parse(self, path: str | Path, category: str = "write") -> RuleDocument:
38 doc = Document(str(path))
39 md: list[str] = []
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("")
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 )