[FIX] рисунки, таблицы и листинги плавают: нет пустых страниц и разрывов рамки
This commit is contained in:
@@ -51,9 +51,15 @@ def _lstlisting(tok: Token, caption: str | None) -> str:
|
|||||||
language = _language(tok.info)
|
language = _language(tok.info)
|
||||||
opts = []
|
opts = []
|
||||||
if language:
|
if language:
|
||||||
opts.append(f"language={language}")
|
# Значение в фигурных скобках обязательно: имя диалекта задаётся
|
||||||
|
# квадратными скобками (`[modern]C++`), и без обёртки парсер опций
|
||||||
|
# listings принимает их за начало следующего опционального аргумента.
|
||||||
|
opts.append(f"language={{{language}}}")
|
||||||
if caption:
|
if caption:
|
||||||
opts.append(f"caption={{{escape_latex(caption)}}}")
|
opts.append(f"caption={{{escape_latex(caption)}}}")
|
||||||
|
# Плавающий листинг не разрывается между страницами: иначе рамка кода
|
||||||
|
# обрывается на границе и продолжается на следующей странице.
|
||||||
|
opts.append("float=htbp")
|
||||||
suffix = f"[{','.join(opts)}]" if opts else ""
|
suffix = f"[{','.join(opts)}]" if opts else ""
|
||||||
return f"\\begin{{lstlisting}}{suffix}\n{tok.content.rstrip()}\n\\end{{lstlisting}}"
|
return f"\\begin{{lstlisting}}{suffix}\n{tok.content.rstrip()}\n\\end{{lstlisting}}"
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
- абзац с картинкой и посторонним текстом не является рисунком: инлайн-
|
- абзац с картинкой и посторонним текстом не является рисунком: инлайн-
|
||||||
картинка внутри текста пропускается с предупреждением;
|
картинка внутри текста пропускается с предупреждением;
|
||||||
- ширина задаётся суффиксом ``|N`` или ``|N%`` (доля от textwidth),
|
- ширина задаётся суффиксом ``|N`` или ``|N%`` (доля от textwidth),
|
||||||
по умолчанию 80 %.
|
по умолчанию 80 %;
|
||||||
|
- размещение ``[ht]``, а не ``[H]``: жёсткая привязка к месту оставляла до
|
||||||
|
половины страницы пустой, когда рисунок не помещался в её остаток. ГОСТ
|
||||||
|
допускает перенос иллюстрации на следующую страницу.
|
||||||
|
|
||||||
Паттерн чистый: match() смотрит на срез токенов, render_figure() строит
|
Паттерн чистый: match() смотрит на срез токенов, render_figure() строит
|
||||||
LaTeX из того же среза; renderer.pos двигает рендерер.
|
LaTeX из того же среза; renderer.pos двигает рендерер.
|
||||||
@@ -93,7 +96,7 @@ def figure(tokens: list[Token]) -> str:
|
|||||||
caption = escape_latex(m.group(1))
|
caption = escape_latex(m.group(1))
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"\\begin{figure}[H]",
|
"\\begin{figure}[ht]",
|
||||||
" \\centering",
|
" \\centering",
|
||||||
rf" \includegraphics[width={width}]{{\detokenize{{{path}}}}}",
|
rf" \includegraphics[width={width}]{{\detokenize{{{path}}}}}",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ def captioned_table(tokens: list[Token]) -> str:
|
|||||||
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
"\\begin{table}[H]",
|
"\\begin{table}[ht]",
|
||||||
f" \\caption{{{caption}}}",
|
f" \\caption{{{caption}}}",
|
||||||
" \\centering",
|
" \\centering",
|
||||||
_tabular(rows, aligns),
|
_tabular(rows, aligns),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle}
|
|||||||
\usepackage{graphicx}
|
\usepackage{graphicx}
|
||||||
\graphicspath{ {./} {./images/} }
|
\graphicspath{ {./} {./images/} }
|
||||||
\usepackage{float}
|
\usepackage{float}
|
||||||
|
\usepackage[section]{placeins}
|
||||||
\usepackage{wrapfig}
|
\usepackage{wrapfig}
|
||||||
\sloppy
|
\sloppy
|
||||||
\setlength{\emergencystretch}{2em}
|
\setlength{\emergencystretch}{2em}
|
||||||
@@ -36,7 +37,6 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle}
|
|||||||
\definecolor{lstkeyword}{RGB}{0,0,139}
|
\definecolor{lstkeyword}{RGB}{0,0,139}
|
||||||
\definecolor{lstcomment}{RGB}{110,110,110}
|
\definecolor{lstcomment}{RGB}{110,110,110}
|
||||||
\definecolor{lststring}{RGB}{139,0,0}
|
\definecolor{lststring}{RGB}{139,0,0}
|
||||||
\definecolor{lstnumber}{RGB}{0,100,0}
|
|
||||||
\definecolor{lstlineno}{RGB}{130,130,130}
|
\definecolor{lstlineno}{RGB}{130,130,130}
|
||||||
|
|
||||||
% Встроенный диалект C++ в listings старше стандарта: auto, nullptr, override
|
% Встроенный диалект C++ в listings старше стандарта: auto, nullptr, override
|
||||||
@@ -59,7 +59,6 @@ PREAMBLE = r"""\documentclass[a4paper, 14pt]{extarticle}
|
|||||||
commentstyle=\color{lstcomment},
|
commentstyle=\color{lstcomment},
|
||||||
stringstyle=\color{lststring},
|
stringstyle=\color{lststring},
|
||||||
numberstyle=\tiny\color{lstlineno},
|
numberstyle=\tiny\color{lstlineno},
|
||||||
directivestyle=\color{lstnumber},
|
|
||||||
identifierstyle=,
|
identifierstyle=,
|
||||||
breakatwhitespace=false,
|
breakatwhitespace=false,
|
||||||
breaklines=true,
|
breaklines=true,
|
||||||
|
|||||||
+21
-9
@@ -48,7 +48,7 @@ def test_figure_with_caption(tmp_path) -> None:
|
|||||||
"![[images/antenna.png]]\n\nРисунок 1 — Антенна\n\n"
|
"![[images/antenna.png]]\n\nРисунок 1 — Антенна\n\n"
|
||||||
"Данная антенна используется для ...\n"
|
"Данная антенна используется для ...\n"
|
||||||
)
|
)
|
||||||
assert "\\begin{figure}[H]" in tex
|
assert "\\begin{figure}[ht]" in tex
|
||||||
assert "\\centering" in tex
|
assert "\\centering" in tex
|
||||||
assert (
|
assert (
|
||||||
"\\includegraphics[width=0.8\\textwidth]{\\detokenize{images/antenna.png}}"
|
"\\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:
|
def test_figure_without_caption() -> None:
|
||||||
tex = render("![[images/photo.png]]\n\nОбычный абзац после картинки.\n")
|
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 "\\caption{" not in tex
|
||||||
assert "Обычный абзац после картинки." in tex
|
assert "Обычный абзац после картинки." in tex
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ TABLE_MD = (
|
|||||||
|
|
||||||
def test_table_with_caption() -> None:
|
def test_table_with_caption() -> None:
|
||||||
tex = render(TABLE_MD)
|
tex = render(TABLE_MD)
|
||||||
assert "\\begin{table}[H]" in tex
|
assert "\\begin{table}[ht]" in tex
|
||||||
assert "\\caption{Сравнение скорости}" in tex
|
assert "\\caption{Сравнение скорости}" in tex
|
||||||
assert "\\begin{tabularx}{\\textwidth}" in tex
|
assert "\\begin{tabularx}{\\textwidth}" in tex
|
||||||
assert "\\textbf{1} & 0.027852 & 0.000285 \\\\" in tex
|
assert "\\textbf{1} & 0.027852 & 0.000285 \\\\" in tex
|
||||||
@@ -334,7 +334,7 @@ LISTING_MD = (
|
|||||||
def test_listing_with_caption() -> None:
|
def test_listing_with_caption() -> None:
|
||||||
tex = render(LISTING_MD)
|
tex = render(LISTING_MD)
|
||||||
assert "caption={Потокобезопасная очередь}" in tex
|
assert "caption={Потокобезопасная очередь}" in tex
|
||||||
assert "language=[modern]C++" in tex
|
assert "language={[modern]C++}" in tex
|
||||||
assert "class ThreadSafeQueue {};" 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:
|
def test_listing_without_caption_stays_bare() -> None:
|
||||||
tex = render("```cpp\nint main() {}\n```\n")
|
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
|
assert "caption=" not in tex
|
||||||
|
|
||||||
|
|
||||||
def test_listing_caption_without_language() -> None:
|
def test_listing_caption_without_language() -> None:
|
||||||
tex = render("Листинг 2 —— Формат строки\n\n```\nid,freq,power\n```\n")
|
tex = render("Листинг 2 —— Формат строки\n\n```\nid,freq,power\n```\n")
|
||||||
assert "caption={Формат строки}" in tex
|
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:
|
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:
|
def test_listing_language_is_mapped_to_listings_name() -> None:
|
||||||
"""listings не знает 'cpp', только 'C++'."""
|
"""listings не знает 'cpp', только 'C++'."""
|
||||||
tex = render("```cpp\nint x;\n```\n")
|
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:
|
def test_listing_unsupported_language_falls_back_to_plain() -> None:
|
||||||
tex = render("```yaml\nkey: value\n```\n")
|
tex = render("```yaml\nkey: value\n```\n")
|
||||||
assert "language=" not in tex
|
assert "language={" not in tex
|
||||||
assert "key: value" 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:
|
def test_cpp_uses_extended_dialect() -> None:
|
||||||
"""Встроенный C++ в listings не знает auto, nullptr и override."""
|
"""Встроенный C++ в listings не знает auto, nullptr и override."""
|
||||||
tex = body(render("```cpp\nauto x = nullptr;\n```\n"))
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user