"""Parse ГОСТ-style captions: Рисунок / Таблица / Листинг (+ продолжения).""" from __future__ import annotations import re from dataclasses import dataclass from md2gost.word_fix import parse_caption_text _FIGURE_RE = re.compile( r"^(?:Рисунок|Рис\.?)\s+(?P[\d.]+)\s*(?:[—–\-]\s*(?P.+))?$", re.IGNORECASE, ) _TABLE_FULL_RE = re.compile( r"^(?:Продолжение\s+)?" r"(?P<kind>Таблица|Таблицы|Листинг|Листинга)\s+" r"(?P<number>[\d.]+)" r"(?:\s*[—–\-]\s*(?P<title>.+))?$", re.IGNORECASE, ) _CONTINUATION_RE = re.compile(r"^Продолжение\s+(?:Таблицы|Листинга)\b", re.IGNORECASE) @dataclass class ParsedCaption: kind: str # figure | table | listing number: str text: str | None = None is_continuation: bool = False def caption_unique_id(kind: str, number: str) -> str: """Stable id from display number: 1.1 → fig1_1 / tbl1_1 / lst1_1.""" safe = str(number).replace(".", "_") prefix = {"figure": "fig", "table": "tbl", "listing": "lst"}.get(kind, "obj") return f"{prefix}{safe}" def parse_any_caption(text: str) -> ParsedCaption | None: """Parse figure / table / listing captions (including «Продолжение…»).""" raw = (text or "").replace("\r", "").replace("\x07", "").strip() if not raw: return None first = raw.split("\n", 1)[0].strip() m = _FIGURE_RE.match(first) if m: title = (m.group("title") or "").strip().rstrip(".") or None return ParsedCaption(kind="figure", number=m.group("number"), text=title) info = parse_caption_text(first) if info is not None: title = None m2 = _TABLE_FULL_RE.match(first) if m2 and m2.group("title"): title = m2.group("title").strip().rstrip(".") or None else: # Strip leading «Таблица N — » / «Продолжение…» for leftover title rest = _TABLE_FULL_RE.sub("", first).strip() if rest.startswith(("—", "–", "-")): rest = rest[1:].strip() title = rest.rstrip(".") or None if rest else None return ParsedCaption( kind=info.kind, number=info.number, text=title, is_continuation=info.is_continuation, ) m3 = _TABLE_FULL_RE.match(first) if m3: kind_raw = m3.group("kind").lower() if kind_raw.startswith("табл"): kind = "table" elif kind_raw.startswith("лист"): kind = "listing" else: return None return ParsedCaption( kind=kind, number=m3.group("number"), text=(m3.group("title") or "").strip().rstrip(".") or None, is_continuation=bool(_CONTINUATION_RE.match(first)), ) return None