+170
@@ -0,0 +1,170 @@
|
||||
"""Second pass: replace «Рисунок 1.1» / «табл. 2» with @Тип:id."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from .blocks import (
|
||||
Block,
|
||||
CaptionBlock,
|
||||
EquationBlock,
|
||||
ImageBlock,
|
||||
InlineSpan,
|
||||
ListBlock,
|
||||
ListingBlock,
|
||||
ParagraphBlock,
|
||||
TableBlock,
|
||||
)
|
||||
from .captions import caption_unique_id
|
||||
|
||||
# Number like 1 / 1.1 / 2.3.4 — do NOT use [\d.]+ (eats trailing sentence period).
|
||||
_NUM = r"(?P<num>\d+(?:\.\d+)*)"
|
||||
|
||||
_REF_PATTERNS = [
|
||||
(
|
||||
re.compile(
|
||||
rf"(?P<full>(?:Рисунк(?:е|а|у|ом)?|Рисунок|Рис\.?)\s+{_NUM})",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"figure",
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
rf"(?P<full>(?:Таблиц[аыеу]|табл\.?)\s+{_NUM})",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"table",
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
rf"(?P<full>(?:Листинг(?:а|у|е|ом)?|лист\.?)\s+{_NUM})",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"listing",
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
rf"(?P<full>(?:Формул[аыеу]|форм\.?)\s+{_NUM})",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
"equation",
|
||||
),
|
||||
]
|
||||
|
||||
_TYPE_LABEL = {
|
||||
"figure": "Рисунок",
|
||||
"table": "Таблица",
|
||||
"listing": "Листинг",
|
||||
"equation": "Формула",
|
||||
}
|
||||
|
||||
|
||||
def collect_number_maps(blocks: list[Block]) -> dict[str, dict[str, str]]:
|
||||
"""kind → {number → unique_id}."""
|
||||
maps: dict[str, dict[str, str]] = {
|
||||
"figure": {},
|
||||
"table": {},
|
||||
"listing": {},
|
||||
"equation": {},
|
||||
}
|
||||
for b in blocks:
|
||||
if isinstance(b, CaptionBlock) and not b.is_continuation:
|
||||
maps[b.kind][b.number] = b.unique_id or caption_unique_id(b.kind, b.number)
|
||||
elif isinstance(b, ImageBlock) and b.caption_id:
|
||||
num = _id_to_number(b.caption_id, "fig")
|
||||
if num:
|
||||
maps["figure"][num] = b.caption_id
|
||||
elif isinstance(b, TableBlock) and b.caption_id:
|
||||
num = _id_to_number(b.caption_id, "tbl")
|
||||
if num:
|
||||
maps["table"][num] = b.caption_id
|
||||
elif isinstance(b, ListingBlock) and b.caption_id:
|
||||
num = _id_to_number(b.caption_id, "lst")
|
||||
if num:
|
||||
maps["listing"][num] = b.caption_id
|
||||
elif isinstance(b, EquationBlock) and b.number and b.unique_id:
|
||||
maps["equation"][b.number] = b.unique_id
|
||||
return maps
|
||||
|
||||
|
||||
def _id_to_number(uid: str, prefix: str) -> str | None:
|
||||
if not uid.startswith(prefix):
|
||||
return None
|
||||
rest = uid[len(prefix):]
|
||||
if not rest:
|
||||
return None
|
||||
return rest.replace("_", ".")
|
||||
|
||||
|
||||
def rewrite_text(text: str, maps: dict[str, dict[str, str]]) -> str:
|
||||
"""Replace object references; leave bibliography [1] alone."""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
def repl_factory(kind: str):
|
||||
label = _TYPE_LABEL[kind]
|
||||
num_map = maps.get(kind) or {}
|
||||
|
||||
def repl(m: re.Match) -> str:
|
||||
num = m.group("num")
|
||||
uid = num_map.get(num)
|
||||
if not uid:
|
||||
return m.group(0)
|
||||
return f"@{label}:{uid}"
|
||||
|
||||
return repl
|
||||
|
||||
out = text
|
||||
for pattern, kind in _REF_PATTERNS:
|
||||
out = pattern.sub(repl_factory(kind), out)
|
||||
return out
|
||||
|
||||
|
||||
def _rewrite_paragraph_spans(para: ParagraphBlock, maps: dict[str, dict[str, str]]) -> None:
|
||||
"""Rewrite refs even when Word split «Таблице 5.1» across runs."""
|
||||
spans = para.spans
|
||||
if not spans:
|
||||
return
|
||||
new_spans: list[InlineSpan] = []
|
||||
i = 0
|
||||
while i < len(spans):
|
||||
s = spans[i]
|
||||
if s.math or s.href:
|
||||
new_spans.append(s)
|
||||
i += 1
|
||||
continue
|
||||
j = i
|
||||
chunk: list[InlineSpan] = []
|
||||
while j < len(spans) and not spans[j].math and not spans[j].href:
|
||||
chunk.append(spans[j])
|
||||
j += 1
|
||||
joined = "".join(p.text or "" for p in chunk)
|
||||
rewritten = rewrite_text(joined, maps)
|
||||
if rewritten == joined:
|
||||
new_spans.extend(chunk)
|
||||
else:
|
||||
first = chunk[0]
|
||||
same_fmt = all(
|
||||
p.bold == first.bold and p.italic == first.italic and p.strike == first.strike
|
||||
for p in chunk
|
||||
)
|
||||
new_spans.append(
|
||||
InlineSpan(
|
||||
text=rewritten,
|
||||
bold=first.bold if same_fmt else False,
|
||||
italic=first.italic if same_fmt else False,
|
||||
strike=first.strike if same_fmt else False,
|
||||
)
|
||||
)
|
||||
i = j
|
||||
para.spans = new_spans
|
||||
|
||||
|
||||
def apply_reference_pass(blocks: list[Block]) -> list[Block]:
|
||||
maps = collect_number_maps(blocks)
|
||||
for b in blocks:
|
||||
if isinstance(b, ParagraphBlock):
|
||||
_rewrite_paragraph_spans(b, maps)
|
||||
elif isinstance(b, ListBlock):
|
||||
b.items = [rewrite_text(item, maps) for item in b.items]
|
||||
return blocks
|
||||
Reference in New Issue
Block a user