2026-09-02 03:17:53 +03:00
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
from md2gost.latex.titlepages import TITLEPAGES
|
2026-08-25 13:58:01 +03:00
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-02 03:17:53 +03:00
|
|
|
|
def render_fm(extra: str, body: str = "Текст.\n") -> str:
|
|
|
|
|
|
head = f"---\nназвание: Тестовая работа\nтип: Отчёт\n{extra}---\n\n"
|
|
|
|
|
|
parser = Parser(head + body)
|
|
|
|
|
|
return Renderer(parser.tokens, parser.front_matter).render()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 13:58:01 +03:00
|
|
|
|
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"
|
|
|
|
|
|
)
|
2026-08-28 03:53:45 +03:00
|
|
|
|
assert "\\begin{figure}[H]" in tex
|
2026-08-25 13:58:01 +03:00
|
|
|
|
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")
|
2026-08-28 03:53:45 +03:00
|
|
|
|
assert "\\begin{figure}[H]" in tex
|
2026-08-25 13:58:01 +03:00
|
|
|
|
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("Текст с  внутри.\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]
|
2026-08-28 03:53:45 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TABLE_MD = (
|
|
|
|
|
|
"Таблица 1 —— Сравнение скорости\n\n"
|
|
|
|
|
|
"| № Запуска | Python (сек) | C++ (сек) |\n"
|
|
|
|
|
|
"| :--- | :---: | ---: |\n"
|
|
|
|
|
|
"| **1** | 0.027852 | 0.000285 |\n"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_with_caption() -> None:
|
|
|
|
|
|
tex = render(TABLE_MD)
|
|
|
|
|
|
assert "\\begin{table}[H]" in tex
|
|
|
|
|
|
assert "\\caption{Сравнение скорости}" in tex
|
|
|
|
|
|
assert "\\begin{tabular}{|l|c|r|}" in tex
|
|
|
|
|
|
assert "\\textbf{1} & 0.027852 & 0.000285 \\\\" in tex
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_caption_number_is_discarded() -> None:
|
|
|
|
|
|
tex = render(TABLE_MD)
|
|
|
|
|
|
block = _table_block(tex)
|
|
|
|
|
|
assert "Таблица 1" not in block
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_without_caption_stays_bare() -> None:
|
|
|
|
|
|
tex = render("| a | b |\n| --- | --- |\n| 1 | 2 |\n")
|
|
|
|
|
|
assert "\\begin{table}" not in tex
|
|
|
|
|
|
assert "\\begin{tabular}{|l|l|}" in tex
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_table_caption_paragraph_is_not_duplicated() -> None:
|
|
|
|
|
|
tex = render(TABLE_MD)
|
|
|
|
|
|
assert tex.count("Сравнение скорости") == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _table_block(tex: str) -> str:
|
|
|
|
|
|
start = tex.index("\\begin{table}")
|
|
|
|
|
|
end = tex.index("\\end{table}") + len("\\end{table}")
|
|
|
|
|
|
return tex[start:end]
|
2026-09-02 03:17:53 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_titlepage_default_is_used_when_field_absent() -> None:
|
|
|
|
|
|
tex = render("Текст.\n")
|
|
|
|
|
|
assert "\\begin{titlepage}" in tex
|
|
|
|
|
|
assert "ОТЧЁТ ПО УЧЕБНОЙ ПРАКТИКЕ" not in tex
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_titlepage_registry_lists_three_layouts() -> None:
|
|
|
|
|
|
assert set(TITLEPAGES) == {"default", "ptp-report", "ptp-task"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_titlepage_unknown_name_raises() -> None:
|
|
|
|
|
|
with pytest.raises(ValueError, match="ptp-report"):
|
|
|
|
|
|
render_fm("титульник: несуществующий\n")
|