From 2a0333c673b7e91cce2a5ccd718701e3a7d415b1 Mon Sep 17 00:00:00 2001 From: Arseny <20096ninja@gmail.com> Date: Wed, 2 Sep 2026 13:10:18 +0300 Subject: [PATCH] =?UTF-8?q?[FIX]=20=D1=80=D0=B8=D1=81=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D0=B8,=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D1=8B=20=D0=B8?= =?UTF-8?q?=20=D0=BB=D0=B8=D1=81=D1=82=D0=B8=D0=BD=D0=B3=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BB=D0=B0=D0=B2=D0=B0=D1=8E=D1=82:=20=D0=BD=D0=B5=D1=82=20?= =?UTF-8?q?=D0=BF=D1=83=D1=81=D1=82=D1=8B=D1=85=20=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=86=20=D0=B8=20=D1=80=D0=B0=D0=B7=D1=80=D1=8B?= =?UTF-8?q?=D0=B2=D0=BE=D0=B2=20=D1=80=D0=B0=D0=BC=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- md2gost/handlers/code.py | 8 +++++++- md2gost/handlers/image.py | 7 +++++-- md2gost/handlers/table.py | 2 +- md2gost/latex/preamble.py | 3 +-- tests/test_render.py | 30 +++++++++++++++++++++--------- 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/md2gost/handlers/code.py b/md2gost/handlers/code.py index 44314bd..4187fed 100644 --- a/md2gost/handlers/code.py +++ b/md2gost/handlers/code.py @@ -51,9 +51,15 @@ def _lstlisting(tok: Token, caption: str | None) -> str: language = _language(tok.info) opts = [] if language: - opts.append(f"language={language}") + # Значение в фигурных скобках обязательно: имя диалекта задаётся + # квадратными скобками (`[modern]C++`), и без обёртки парсер опций + # listings принимает их за начало следующего опционального аргумента. + opts.append(f"language={{{language}}}") if caption: opts.append(f"caption={{{escape_latex(caption)}}}") + # Плавающий листинг не разрывается между страницами: иначе рамка кода + # обрывается на границе и продолжается на следующей странице. + opts.append("float=htbp") suffix = f"[{','.join(opts)}]" if opts else "" return f"\\begin{{lstlisting}}{suffix}\n{tok.content.rstrip()}\n\\end{{lstlisting}}" diff --git a/md2gost/handlers/image.py b/md2gost/handlers/image.py index cf2fa1a..d845e80 100644 --- a/md2gost/handlers/image.py +++ b/md2gost/handlers/image.py @@ -14,7 +14,10 @@ - абзац с картинкой и посторонним текстом не является рисунком: инлайн- картинка внутри текста пропускается с предупреждением; - ширина задаётся суффиксом ``|N`` или ``|N%`` (доля от textwidth), - по умолчанию 80 %. + по умолчанию 80 %; +- размещение ``[ht]``, а не ``[H]``: жёсткая привязка к месту оставляла до + половины страницы пустой, когда рисунок не помещался в её остаток. ГОСТ + допускает перенос иллюстрации на следующую страницу. Паттерн чистый: match() смотрит на срез токенов, render_figure() строит LaTeX из того же среза; renderer.pos двигает рендерер. @@ -93,7 +96,7 @@ def figure(tokens: list[Token]) -> str: caption = escape_latex(m.group(1)) lines = [ - "\\begin{figure}[H]", + "\\begin{figure}[ht]", " \\centering", rf" \includegraphics[width={width}]{{\detokenize{{{path}}}}}", ] diff --git a/md2gost/handlers/table.py b/md2gost/handlers/table.py index 2d688ca..1a0f5ca 100644 --- a/md2gost/handlers/table.py +++ b/md2gost/handlers/table.py @@ -191,7 +191,7 @@ def captioned_table(tokens: list[Token]) -> str: return "\n".join( [ - "\\begin{table}[H]", + "\\begin{table}[ht]", f" \\caption{{{caption}}}", " \\centering", _tabular(rows, aligns), diff --git a/md2gost/latex/preamble.py b/md2gost/latex/preamble.py index 53229bf..793ea74 100644 --- a/md2gost/latex/preamble.py +++ b/md2gost/latex/preamble.py @@ -14,6 +14,7 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle} \usepackage{graphicx} \graphicspath{ {./} {./images/} } \usepackage{float} +\usepackage[section]{placeins} \usepackage{wrapfig} \sloppy \setlength{\emergencystretch}{2em} @@ -36,7 +37,6 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle} \definecolor{lstkeyword}{RGB}{0,0,139} \definecolor{lstcomment}{RGB}{110,110,110} \definecolor{lststring}{RGB}{139,0,0} -\definecolor{lstnumber}{RGB}{0,100,0} \definecolor{lstlineno}{RGB}{130,130,130} % Встроенный диалект C++ в listings старше стандарта: auto, nullptr, override @@ -59,7 +59,6 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle} commentstyle=\color{lstcomment}, stringstyle=\color{lststring}, numberstyle=\tiny\color{lstlineno}, - directivestyle=\color{lstnumber}, identifierstyle=, breakatwhitespace=false, breaklines=true, diff --git a/tests/test_render.py b/tests/test_render.py index 70b7a5f..4641823 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -48,7 +48,7 @@ def test_figure_with_caption(tmp_path) -> None: "![[images/antenna.png]]\n\nРисунок 1 — Антенна\n\n" "Данная антенна используется для ...\n" ) - assert "\\begin{figure}[H]" in tex + assert "\\begin{figure}[ht]" in tex assert "\\centering" in tex assert ( "\\includegraphics[width=0.8\\textwidth]{\\detokenize{images/antenna.png}}" @@ -60,7 +60,7 @@ def test_figure_with_caption(tmp_path) -> None: def test_figure_without_caption() -> None: tex = render("![[images/photo.png]]\n\nОбычный абзац после картинки.\n") - assert "\\begin{figure}[H]" in tex + assert "\\begin{figure}[ht]" in tex assert "\\caption{" not in tex assert "Обычный абзац после картинки." in tex @@ -127,7 +127,7 @@ TABLE_MD = ( def test_table_with_caption() -> None: tex = render(TABLE_MD) - assert "\\begin{table}[H]" in tex + assert "\\begin{table}[ht]" in tex assert "\\caption{Сравнение скорости}" in tex assert "\\begin{tabularx}{\\textwidth}" in tex assert "\\textbf{1} & 0.027852 & 0.000285 \\\\" in tex @@ -334,7 +334,7 @@ LISTING_MD = ( def test_listing_with_caption() -> None: tex = render(LISTING_MD) assert "caption={Потокобезопасная очередь}" in tex - assert "language=[modern]C++" in tex + assert "language={[modern]C++}" in tex assert "class ThreadSafeQueue {};" in tex @@ -350,14 +350,14 @@ def test_listing_caption_paragraph_is_not_duplicated() -> None: def test_listing_without_caption_stays_bare() -> None: tex = render("```cpp\nint main() {}\n```\n") - assert "\\begin{lstlisting}[language=[modern]C++]" in tex + assert "\\begin{lstlisting}[language={[modern]C++}]" in tex assert "caption=" not in tex def test_listing_caption_without_language() -> None: tex = render("Листинг 2 —— Формат строки\n\n```\nid,freq,power\n```\n") assert "caption={Формат строки}" in tex - assert "language=" not in tex + assert "language={" not in tex def test_paragraph_starting_with_listing_word_is_not_a_caption() -> None: @@ -369,12 +369,12 @@ def test_paragraph_starting_with_listing_word_is_not_a_caption() -> None: def test_listing_language_is_mapped_to_listings_name() -> None: """listings не знает 'cpp', только 'C++'.""" tex = render("```cpp\nint x;\n```\n") - assert "language=[modern]C++" in tex + assert "language={[modern]C++}" in tex def test_listing_unsupported_language_falls_back_to_plain() -> None: tex = render("```yaml\nkey: value\n```\n") - assert "language=" not in tex + assert "language={" not in tex assert "key: value" in tex @@ -485,4 +485,16 @@ def test_abbreviation_table_has_no_extra_column_padding() -> None: def test_cpp_uses_extended_dialect() -> None: """Встроенный C++ в listings не знает auto, nullptr и override.""" tex = body(render("```cpp\nauto x = nullptr;\n```\n")) - assert "language=[modern]C++" in tex + assert "language={[modern]C++}" in tex + + +def test_captioned_listing_floats() -> None: + """Неплавающий листинг рвётся между страницами с незамкнутой рамкой.""" + tex = body(render(LISTING_MD)) + assert "float=htbp" in tex + + +def test_bare_listing_does_not_float() -> None: + """Фрагмент без подписи — часть абзаца, ему плавать незачем.""" + tex = body(render("```cpp\nint x;\n```\n")) + assert "float=" not in tex