160 lines
5.8 KiB
Python
160 lines
5.8 KiB
Python
"""Pre-assign section-scoped numbers and resolve @Type:label references."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
from .numberer import Numberer, APPENDIX_LETTERS
|
||
from .renderable import Renderable
|
||
from .renderable.heading import Heading
|
||
from .renderable.requires_numbering import RequiresNumbering
|
||
from .renderable.equation import Equation
|
||
from .renderable.paragraph import Paragraph
|
||
from .renderable.caption import CaptionInfo
|
||
|
||
|
||
APPENDIX_RE = re.compile(r"^ПРИЛОЖЕНИЕ\s+([А-ЯA-Z])\b", re.IGNORECASE)
|
||
|
||
|
||
class LabelRegistry:
|
||
def __init__(self):
|
||
self._map: dict[tuple[str, str], str] = {}
|
||
|
||
def put(self, category: str, name: str, number: str):
|
||
self._map[(category.lower(), name)] = number
|
||
# also store without category for loose lookup
|
||
self._map[("*", name)] = f"{category} {number}"
|
||
|
||
def get(self, category: str, name: str) -> str | None:
|
||
return self._map.get((category.lower(), name))
|
||
|
||
def get_display(self, category: str, name: str) -> str | None:
|
||
n = self.get(category, name)
|
||
if n:
|
||
return f"{category} {n}"
|
||
# try Russian category aliases
|
||
aliases = {
|
||
"рисунок": "Рисунок",
|
||
"figure": "Рисунок",
|
||
"таблица": "Таблица",
|
||
"table": "Таблица",
|
||
"листинг": "Листинг",
|
||
"listing": "Листинг",
|
||
"формула": "Формула",
|
||
"formula": "Формула",
|
||
"equation": "Формула",
|
||
}
|
||
cat = aliases.get(category.lower(), category)
|
||
n = self.get(cat, name)
|
||
if n:
|
||
return f"{cat} {n}"
|
||
return None
|
||
|
||
|
||
def assign_numbers(renderables: list[Renderable],
|
||
numbered_equations: set[str] | None = None,
|
||
numbering_scope: str = "section") -> LabelRegistry:
|
||
"""Walk renderables once, set_number on objects, fill LabelRegistry."""
|
||
numbered_equations = numbered_equations or set()
|
||
numberer = Numberer(mode=numbering_scope)
|
||
registry = LabelRegistry()
|
||
section_count = 0
|
||
appendix_index = 0
|
||
|
||
for r in renderables:
|
||
if isinstance(r, Heading):
|
||
text = (r.text or "").strip()
|
||
upper = text.upper()
|
||
if r.level == 1 and r.is_numbered:
|
||
section_count += 1
|
||
numberer.enter_section(section_count)
|
||
m = APPENDIX_RE.match(upper)
|
||
if m:
|
||
numberer.enter_appendix(m.group(1).upper())
|
||
elif upper.startswith("ПРИЛОЖЕНИЕ"):
|
||
parts = upper.split()
|
||
if len(parts) >= 2 and parts[1] in APPENDIX_LETTERS:
|
||
numberer.enter_appendix(parts[1])
|
||
elif appendix_index < len(APPENDIX_LETTERS):
|
||
numberer.enter_appendix(APPENDIX_LETTERS[appendix_index])
|
||
appendix_index += 1
|
||
|
||
if isinstance(r, Equation):
|
||
label = r.unique_name
|
||
if r.needs_numbering or (label and label in numbered_equations):
|
||
r.enable_numbering()
|
||
num = numberer.next_number("Формула", label)
|
||
r.set_number(num)
|
||
if label:
|
||
registry.put("Формула", label, num)
|
||
continue
|
||
|
||
if isinstance(r, RequiresNumbering):
|
||
label = getattr(r, "unique_name", None)
|
||
num = numberer.next_number(r.numbering_category, label)
|
||
r.set_number(num)
|
||
if label:
|
||
registry.put(r.numbering_category, label, num)
|
||
# Diagram +listing → also number Листинг with same label
|
||
from .renderable.diagram import DiagramFigure
|
||
if isinstance(r, DiagramFigure) and r.listing is not None:
|
||
lnum = numberer.next_number("Листинг", label)
|
||
r.set_listing_number(lnum)
|
||
if label:
|
||
registry.put("Листинг", label, lnum)
|
||
# Images nested in paragraphs
|
||
if isinstance(r, Paragraph):
|
||
pass
|
||
|
||
# Images attached to paragraphs
|
||
if isinstance(r, Paragraph):
|
||
for img in getattr(r, "_images", []):
|
||
label = getattr(img, "unique_name", None)
|
||
num = numberer.next_number(img.numbering_category, label)
|
||
img.set_number(num)
|
||
if label:
|
||
registry.put(img.numbering_category, label, num)
|
||
|
||
return registry
|
||
|
||
|
||
def resolve_pending_in_renderables(renderables: list[Renderable], resolve_fn) -> None:
|
||
"""Resolve @Type:label placeholders in paragraphs, lists, headings and table cells."""
|
||
from .renderable.list import List as RList
|
||
from .renderable.table import Table
|
||
from .renderable.heading import Heading
|
||
|
||
def resolve_para(p):
|
||
if hasattr(p, "resolve_pending_refs"):
|
||
p.resolve_pending_refs(resolve_fn)
|
||
|
||
for r in renderables:
|
||
if isinstance(r, Paragraph):
|
||
resolve_para(r)
|
||
elif isinstance(r, Heading):
|
||
resolve_para(r)
|
||
elif isinstance(r, RList):
|
||
for p in getattr(r, "_paragraphs", []):
|
||
resolve_para(p)
|
||
elif isinstance(r, Table):
|
||
for row in getattr(r, "_rows", []):
|
||
for cell in row:
|
||
for p in cell:
|
||
resolve_para(p)
|
||
|
||
|
||
# Patch Paragraph to resolve references at run-creation time via module-level registry
|
||
_ACTIVE_REGISTRY: LabelRegistry | None = None
|
||
|
||
|
||
def set_active_registry(registry: LabelRegistry | None):
|
||
global _ACTIVE_REGISTRY
|
||
_ACTIVE_REGISTRY = registry
|
||
|
||
|
||
def resolve_reference(type_: str, name: str) -> str:
|
||
if _ACTIVE_REGISTRY is None:
|
||
return f"{type_}?"
|
||
display = _ACTIVE_REGISTRY.get_display(type_, name)
|
||
return display or f"{type_}?"
|