86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
"""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<number>[\d.]+)\s*(?:[—–\-]\s*(?P<title>.+))?$",
|
||
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
|