974d49b056
- md2gost.sh: монтировать директорию входного файла (/input:ro) вместо одного файла — картинки по относительным путям (img/) теперь видны внутри контейнера; основное исправление падений make run - render.py: вставлять TOC до следующего блока, а не после — порядок стал АННОТАЦИЯ -> СОДЕРЖАНИЕ -> ПЕРЕЧЕНЬ -> ВВЕДЕНИЕ (ранее заголовок ПЕРЕЧЕНЬ без тела оказывался перед СОДЕРЖАНИЕМ, а его тело — после) - heading.py: не устанавливать current_heading для СОДЕРЖАНИЕ, чтобы не сбивать спецрежим перечня сокращений - .gitignore: игнорировать сгенерированные report.tex/pdf и img/
90 lines
2.9 KiB
Python
90 lines
2.9 KiB
Python
"""Главный цикл рендера: проход по токенам и диспетчеризация.
|
|
|
|
Порядок диспетчеризации для каждого top-level токена:
|
|
|
|
1. Паттерны (registry.PATTERNS) — составные условия на набор токенов
|
|
(например «абзац из ровно одной картинки»).
|
|
2. Словарь registry.BLOCK — обработчики одиночных top-level токенов.
|
|
|
|
Контракт block-обработчика: ``handler(renderer, tok) -> str``; обработчик
|
|
обязан продвинуть ``renderer.pos`` за все потреблённые токены. Нарушение
|
|
контракта — ошибка (RuntimeError), а не тихий неверный вывод.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
from markdown_it.token import Token
|
|
|
|
from md2gost import registry
|
|
from md2gost.latex import END_DOCUMENT, PREAMBLE, render_titlepage
|
|
|
|
# Импорт обработчиков регистрирует их в реестрах.
|
|
from md2gost.handlers import ( # noqa: F401
|
|
bullet_list,
|
|
fence,
|
|
figure,
|
|
heading,
|
|
hr,
|
|
math_block,
|
|
ordered_list,
|
|
paragraph,
|
|
table,
|
|
)
|
|
|
|
TOC_HEADING = "СОДЕРЖАНИЕ"
|
|
|
|
|
|
class Renderer:
|
|
def __init__(self, tokens: list[Token], front_matter: dict[str, str]) -> None:
|
|
self.tokens = tokens
|
|
self.fm = front_matter
|
|
self.pos = 0
|
|
self.current_heading = ""
|
|
self.pending_toc = False
|
|
|
|
def render(self) -> str:
|
|
parts: list[str] = [
|
|
PREAMBLE,
|
|
render_titlepage(self.fm),
|
|
"\\setcounter{page}{2}",
|
|
]
|
|
|
|
while self.pos < len(self.tokens):
|
|
if self.pending_toc:
|
|
parts.append("\\newpage\n\\tableofcontents\n\\newpage")
|
|
self.pending_toc = False
|
|
rendered = self._dispatch()
|
|
parts.append(rendered)
|
|
|
|
# TOC в конце файла (если # СОДЕРЖАНИЕ был последним блоком)
|
|
if self.pending_toc:
|
|
parts.append("\\newpage\n\\tableofcontents\n\\newpage")
|
|
|
|
parts.append(END_DOCUMENT)
|
|
return "\n\n".join(p for p in parts if p)
|
|
|
|
def _dispatch(self) -> str:
|
|
tokens = self.tokens
|
|
pos = self.pos
|
|
tok = tokens[pos]
|
|
|
|
for match_fn, handler_fn in registry.PATTERNS:
|
|
span = match_fn(tokens, pos)
|
|
if span is not None and span > 0:
|
|
self.pos += span
|
|
return handler_fn(tokens[pos : pos + span])
|
|
|
|
handler = registry.BLOCK.get(tok.type)
|
|
if handler is None:
|
|
self.pos += 1
|
|
return ""
|
|
|
|
result = handler(self, tok)
|
|
if self.pos <= pos:
|
|
raise RuntimeError(
|
|
f"обработчик {handler.__name__} не продвинул pos "
|
|
f"для токена '{tok.type}'"
|
|
)
|
|
return result
|