@@ -0,0 +1,167 @@
|
||||
"""Classify DOCX paragraphs/tables into IR kinds (style first, then heuristics)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from md2gost.renderable.toc import SPECIAL_TITLES
|
||||
|
||||
from .captions import parse_any_caption
|
||||
|
||||
CAPTION_STYLE_KINDS = {
|
||||
"Caption Figure": "figure",
|
||||
"Название таблицы": "table",
|
||||
"Caption Table": "table",
|
||||
"Caption Listing": "listing",
|
||||
"Caption": None, # need text heuristic
|
||||
}
|
||||
|
||||
HEADING_STYLE_RE = re.compile(r"^(?:Heading|Заголовок)\s*(\d+)$", re.IGNORECASE)
|
||||
TOC_STYLE_RE = re.compile(r"^toc\s*\d+$", re.IGNORECASE)
|
||||
|
||||
SPECIAL_UPPER = {t.upper() for t in SPECIAL_TITLES}
|
||||
APPENDIX_RE = re.compile(
|
||||
r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-ZЁ])(?:\s+(.+))?$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
NUMBERED_HEADING_RE = re.compile(r"^(\d+(?:\.\d+)*)\.?\s+(.+)$")
|
||||
PRACTICE_RE = re.compile(r"^Практическая\s+работа\b", re.IGNORECASE)
|
||||
LIST_MARKER_RE = re.compile(r"^(?:[–—\-•▪]|(\d+)\.)\s*\t?\s*(.*)$")
|
||||
BIBLIO_LINE_RE = re.compile(r"^\[(\d+(?:\.\d+)?)\]\s*[:.]?\s*(.+)$")
|
||||
BODY_START_TITLES = SPECIAL_UPPER | {"СОДЕРЖАНИЕ"}
|
||||
|
||||
|
||||
def style_name(paragraph) -> str:
|
||||
try:
|
||||
if paragraph.style is None:
|
||||
return ""
|
||||
return paragraph.style.name or ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def is_toc_style(name: str) -> bool:
|
||||
return bool(TOC_STYLE_RE.match((name or "").strip()))
|
||||
|
||||
|
||||
def heading_level_from_style(name: str) -> int | None:
|
||||
m = HEADING_STYLE_RE.match((name or "").strip())
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
level = int(m.group(1))
|
||||
except ValueError:
|
||||
return None
|
||||
return level if 1 <= level <= 9 else None
|
||||
|
||||
|
||||
def is_code_style(name: str) -> bool:
|
||||
n = (name or "").strip().lower()
|
||||
return n == "code" or n.startswith("code ")
|
||||
|
||||
|
||||
def is_bibliography_style(name: str) -> bool:
|
||||
n = (name or "").strip().lower()
|
||||
return n in ("bibliography", "bibliography heading")
|
||||
|
||||
|
||||
def is_caption_style(name: str) -> bool:
|
||||
return (name or "").strip() in CAPTION_STYLE_KINDS
|
||||
|
||||
|
||||
def caption_kind_from_style(name: str) -> str | None:
|
||||
return CAPTION_STYLE_KINDS.get((name or "").strip())
|
||||
|
||||
|
||||
def is_special_title(text: str) -> bool:
|
||||
t = (text or "").strip().upper()
|
||||
if t in SPECIAL_UPPER:
|
||||
return True
|
||||
# allow «СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ» variants already in set
|
||||
if t.startswith("СПИСОК ИСПОЛЬЗ"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_body_start_heading(text: str, style: str = "") -> bool:
|
||||
"""True if this heading marks the start of report body (skip title pages before it)."""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return False
|
||||
if heading_level_from_style(style) or style.lower().startswith("heading"):
|
||||
upper = t.upper()
|
||||
if upper.lstrip("* ").upper() in BODY_START_TITLES or is_special_title(t):
|
||||
return True
|
||||
if PRACTICE_RE.match(t):
|
||||
return True
|
||||
if NUMBERED_HEADING_RE.match(t):
|
||||
return True
|
||||
if APPENDIX_RE.match(t):
|
||||
return True
|
||||
# Any Heading 1 after front matter
|
||||
if heading_level_from_style(style) == 1:
|
||||
return True
|
||||
# Heuristic without style: special titles alone
|
||||
if is_special_title(t) or PRACTICE_RE.match(t):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def classify_heading_text(text: str, style_level: int | None) -> tuple[int, str, bool]:
|
||||
"""
|
||||
Return (level, title_text, numbered).
|
||||
numbered=False → emit as # *TITLE
|
||||
"""
|
||||
raw = (text or "").strip()
|
||||
level = style_level or 1
|
||||
|
||||
app = APPENDIX_RE.match(raw)
|
||||
if app:
|
||||
letter = app.group(1).upper()
|
||||
name = (app.group(2) or "").strip()
|
||||
title = f"Приложение {letter}" + (f" {name}" if name else "")
|
||||
return 2, title, True
|
||||
|
||||
if is_special_title(raw):
|
||||
return 1, raw.upper(), False
|
||||
|
||||
m = NUMBERED_HEADING_RE.match(raw)
|
||||
if m:
|
||||
# keep digits in text for manual numbering dialect
|
||||
return level, raw, True
|
||||
|
||||
if PRACTICE_RE.match(raw):
|
||||
return 1, raw, True
|
||||
|
||||
return level, raw, True
|
||||
|
||||
|
||||
def look_like_list_item(text: str) -> tuple[bool, bool, str] | None:
|
||||
"""
|
||||
Detect md2gost-style list lines (–\\t / 1.\\t) or plain markers.
|
||||
Returns (ordered, marker_ok, body) or None.
|
||||
"""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return None
|
||||
if t[0] in ("–", "—", "-", "•", "▪") and (len(t) == 1 or t[1] in "\t "):
|
||||
body = t[1:].lstrip("\t ").strip()
|
||||
return False, True, body
|
||||
m = re.match(r"^(\d+)\.\s*\t?\s*(.+)$", t)
|
||||
if m:
|
||||
return True, True, m.group(2).strip()
|
||||
return None
|
||||
|
||||
|
||||
def parse_biblio_line(text: str) -> tuple[str, str] | None:
|
||||
m = BIBLIO_LINE_RE.match((text or "").strip())
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1), m.group(2).strip()
|
||||
|
||||
|
||||
def paragraph_has_numpr(paragraph) -> bool:
|
||||
pPr = paragraph._element.pPr
|
||||
if pPr is None:
|
||||
return False
|
||||
return pPr.numPr is not None
|
||||
Reference in New Issue
Block a user