"""Bibliography parsing helpers (no docx dependency) and optional renderable.""" from __future__ import annotations import re from dataclasses import dataclass BIBLIO_LINE_RE = re.compile( r"^\[(\d+(?:\.\d+)?)\]:\s*(.+)$" ) CITE_RE = re.compile( r"\[(\d+(?:\.\d+)?(?:\s*,\s*\d+(?:\.\d+)?)*)" r"(?:\s*,\s*[cс]\.\s*[\d\-–]+)?" r"\]" ) def biblio_bookmark_name(key: str) -> str: """Word bookmark name for bibliography key (dots → underscores).""" return "biblio_" + str(key).replace(".", "_") @dataclass class BiblioEntry: key: str text: str year: int | None = None def extract_year(text: str) -> int | None: years = re.findall(r"(?:^|[^\d])((?:19|20)\d{2})(?:[^\d]|$)", text) if not years: return None return max(int(y) for y in years) def parse_biblio_block(lines: list[str]) -> list[BiblioEntry]: entries = [] for line in lines: m = BIBLIO_LINE_RE.match(line.strip()) if m: key, text = m.group(1), m.group(2).strip() entries.append(BiblioEntry(key=key, text=text, year=extract_year(text))) return entries def find_citations(text: str) -> list[str]: seen: list[str] = [] for m in CITE_RE.finditer(text): inner = m.group(1) for part in re.split(r"\s*,\s*", inner): part = part.strip() if part and part not in seen: seen.append(part) return seen def Bibliography(*args, **kwargs): """Lazy wrapper to avoid importing docx/freetype at checker import time.""" from .bibliography_renderable import Bibliography as _Bibliography return _Bibliography(*args, **kwargs)