[ADD] поддержка изображений и рефакторинг диспетчера токенов

- registry.py: реестры BLOCK/PATTERNS/INLINE с декораторами @block/@pattern/@inline
- render.py: единый диспетчер, слой паттернов, проверка контракта pos (RuntimeError),
  pending_toc вместо current_heading для единственной вставки СОДЕРЖАНИЯ
- handlers/image.py: вики-ссылки ![[путь|ширина]], подпись Рисунок N —— ... ,
  figure-окружение, авто-нумерация, копирование файлов в tmpdir
- handlers/inline.py: табличная диспетчеризация вместо if/elif
- Все block-обработчики подписаны @block, корректно продвигают pos
- parser.py: препроцессинг ![[...]] → ![](path), iter_image_sources()
- main.py: _copy_assets (emblem+картинки в tmpdir), фикс лога (имя .tex),
  cwd=tmpdir для xelatex, цикл вместо дублирования, force-include emblem
- tests/test_render.py: pytest (10 тестов), pyproject dev-dep, make test
- preamble.py: name=Рисунок для figure-caption (было Рис. от babel)
- Исправлены баги: мёртвый обработчик, emblem не копировалась,
  множественный TOC, захардкожен report.log, маскировка pos
- README/AGENTS.md обновлены
This commit is contained in:
Arseny
2026-08-25 13:58:01 +03:00
parent 3e67937b8b
commit 8a921e2337
20 changed files with 592 additions and 138 deletions
+102
View File
@@ -0,0 +1,102 @@
from md2gost.parser import Parser
from md2gost.render import Renderer
FRONT_MATTER = "---\nназвание: Тестовая работа\nтип: Отчёт\n---\n\n"
def render(md: str) -> str:
parser = Parser(FRONT_MATTER + md)
return Renderer(parser.tokens, parser.front_matter).render()
def test_heading_numbering() -> None:
tex = render("# Исследовательская часть\n\n## Обзор\n")
assert "\\section{Исследовательская часть}" in tex
assert "\\subsection{Обзор}" in tex
def test_unnumbered_heading() -> None:
tex = render("# ВВЕДЕНИЕ\n\nТекст.\n")
assert "\\section{" not in tex
assert "\\clearpage" in tex
assert "ВВЕДЕНИЕ" in tex
def test_list_punctuation() -> None:
tex = render("- один\n- два\n- три\n")
assert " \\item один;" in tex
assert " \\item три." in tex
def test_figure_with_caption(tmp_path) -> None:
tex = render(
"![[images/antenna.png]]\n\nРисунок 1 — Антенна\n\n"
"Данная антенна используется для ...\n"
)
assert "\\begin{figure}[ht]" in tex
assert "\\centering" in tex
assert (
"\\includegraphics[width=0.8\\textwidth]{\\detokenize{images/antenna.png}}"
in tex
)
assert "\\caption{Антенна}" in tex
assert "Данная антенна используется для ..." in tex
def test_figure_without_caption() -> None:
tex = render("![[images/photo.png]]\n\nОбычный абзац после картинки.\n")
assert "\\begin{figure}[ht]" in tex
assert "\\caption{" not in tex
assert "Обычный абзац после картинки." in tex
def test_figure_custom_width() -> None:
tex = render("![[images/scheme.png|50]]\n\nРисунок 2 —— Схема\n")
assert (
"\\includegraphics[width=0.5\\textwidth]{\\detokenize{images/scheme.png}}"
in tex
)
def test_caption_number_is_discarded() -> None:
tex = render("![[images/a.png]]\n\nРисунок 7 —— Что-то\n")
assert "7" not in _figure_block(tex)
def test_inline_image_skipped_with_warning(capsys) -> None:
tex = render("Текст с ![alt](img.png) внутри.\n")
assert "предупреждение: картинка внутри текста пропущена" in capsys.readouterr().err
assert "\\begin{figure}" not in tex
assert "img.png" not in tex
def test_toc_inserted_once() -> None:
tex = render("# СОДЕРЖАНИЕ\n\n# ВВЕДЕНИЕ\n\nТекст.\n\n# ЗАКЛЮЧЕНИЕ\n\nВыводы.\n")
assert tex.count("\\tableofcontents") == 1
def test_handler_must_advance_pos() -> None:
from markdown_it import MarkdownIt
tokens = MarkdownIt().parse(FRONT_MATTER + "# Заголовок\n")
renderer = Renderer(tokens, {"название": "Т", "тип": "Отчёт"})
renderer.render()
broken = Renderer(tokens, {"название": "Т", "тип": "Отчёт"})
from md2gost.registry import BLOCK
original = BLOCK["heading_open"]
BLOCK["heading_open"] = lambda r, tok: ""
try:
try:
broken.render()
raise AssertionError("ожидался RuntimeError")
except RuntimeError as e:
assert "heading_open" in str(e)
finally:
BLOCK["heading_open"] = original
def _figure_block(tex: str) -> str:
start = tex.index("\\begin{figure}")
end = tex.index("\\end{figure}") + len("\\end{figure}")
return tex[start:end]