Spaces:
Running
Running
| """Helpers for extracting normalized text content from DOCX files.""" | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import re | |
| from typing import List | |
| from docx import Document # type: ignore[import-not-found] | |
| _VISIBLE_CLAUSE_RE = re.compile(r"^(?:[1-9]\d*(?:\.[0-9]+){0,4}|A\.[0-9]+(?:\.[0-9]+)?)\b") | |
| def read_docx_text(path: Path) -> str: | |
| """Read text from a DOCX file as newline-delimited logical paragraphs.""" | |
| doc = Document(str(path)) | |
| lines: List[str] = [] | |
| numbering_state: dict[int, List[int]] = {} | |
| for paragraph in doc.paragraphs: | |
| text = _clean_text(paragraph.text) | |
| if text: | |
| heading_prefix = _heading_number_prefix(paragraph, numbering_state) | |
| if heading_prefix and not _VISIBLE_CLAUSE_RE.match(text): | |
| text = f"{heading_prefix} {text}" | |
| lines.append(text) | |
| for table in doc.tables: | |
| for row in table.rows: | |
| for cell in row.cells: | |
| for paragraph in cell.paragraphs: | |
| text = _clean_text(paragraph.text) | |
| if text: | |
| lines.append(text) | |
| return "\n".join(lines) | |
| def _clean_text(value: str) -> str: | |
| cleaned = value.replace("\u200b", "").replace("\ufeff", "").replace("\u2060", "") | |
| cleaned = cleaned.replace("\xa0", " ") | |
| return cleaned.strip() | |
| def _heading_number_prefix(paragraph, numbering_state: dict[int, List[int]]) -> str | None: | |
| style_name = "" | |
| if paragraph.style is not None and paragraph.style.name: | |
| style_name = str(paragraph.style.name) | |
| if not style_name.lower().startswith("heading"): | |
| return None | |
| ppr = paragraph._p.pPr | |
| if ppr is None or ppr.numPr is None: | |
| return None | |
| if ppr.numPr.numId is None or ppr.numPr.ilvl is None: | |
| return None | |
| num_id = int(ppr.numPr.numId.val) | |
| level = int(ppr.numPr.ilvl.val) | |
| if level < 0: | |
| return None | |
| counters = numbering_state.setdefault(num_id, [0] * 9) | |
| counters[level] += 1 | |
| for idx in range(level + 1, len(counters)): | |
| counters[idx] = 0 | |
| segments = [str(counters[idx]) for idx in range(level + 1) if counters[idx] > 0] | |
| if not segments: | |
| return None | |
| return ".".join(segments) | |