[ADD] раздел ПРИЛОЖЕНИЕ и листинги длиннее страницы
ПРИЛОЖЕНИЕ добавлено к ненумерованным разделам с новой страницы и попадает
в содержание наравне с введением и заключением.
Листинг длиннее страницы больше не оборачивается в плавающий объект: LaTeX
не смог бы его разместить и вынес бы в конец документа. Рамка у такого
листинга снимается — она всё равно обрывается на границе страницы.
В приложении не плавает ни один листинг независимо от длины: там они идут
подряд и без текста вокруг, а плавающий объект переставлял короткий листинг
за длинный и ломал порядок. Чтобы обработчик знал раздел, составные
обработчики теперь получают рендерер — как и блочные.
Имя ненумерованного раздела ищется в исходном тексте, а не в готовом LaTeX:
у выделенного «==ПРИЛОЖЕНИЕ==» готовый текст — это \hlw{ПРИЛОЖЕНИЕ}, и
раздел нумеровался как обычный.
This commit is contained in:
@@ -31,6 +31,14 @@ _LANGUAGES = {
|
||||
}
|
||||
_PLAIN = {"yaml", "yml", "json", "dockerfile", "cmake", "toml", "ini", "text", ""}
|
||||
|
||||
# Сколько строк кода помещается на странице. Листинг длиннее не поместится ни
|
||||
# в один плавающий объект, и LaTeX выбросит его в конец документа, поэтому
|
||||
# такой набирается на месте. Рамка при этом снимается: она всё равно
|
||||
# оборвалась бы на границе страницы и продолжилась на следующей.
|
||||
PAGE_LINES = 34
|
||||
|
||||
APPENDIX = "ПРИЛОЖЕНИЕ"
|
||||
|
||||
|
||||
def _language(info: str) -> str | None:
|
||||
"""Имя языка для listings или None, если подсветка не нужна."""
|
||||
@@ -47,8 +55,15 @@ def _language(info: str) -> str | None:
|
||||
return language
|
||||
|
||||
|
||||
def _lstlisting(tok: Token, caption: str | None) -> str:
|
||||
def _lstlisting(tok: Token, caption: str | None, in_appendix: bool = False) -> str:
|
||||
language = _language(tok.info)
|
||||
code = tok.content.rstrip()
|
||||
# В приложении листинги идут подряд и без текста вокруг. Плавающий объект
|
||||
# там только путает порядок: LaTeX волен переставить короткий листинг за
|
||||
# длинный. Рамка снимается заодно, чтобы соседние листинги выглядели
|
||||
# одинаково независимо от длины.
|
||||
fits_on_page = not in_appendix and code.count("\n") + 1 <= PAGE_LINES
|
||||
|
||||
opts = []
|
||||
if language:
|
||||
# Значение в фигурных скобках обязательно: имя диалекта задаётся
|
||||
@@ -57,17 +72,20 @@ def _lstlisting(tok: Token, caption: str | None) -> str:
|
||||
opts.append(f"language={{{language}}}")
|
||||
if caption:
|
||||
opts.append(f"caption={{{escape_latex(caption)}}}")
|
||||
# Плавающий листинг не разрывается между страницами: иначе рамка кода
|
||||
# обрывается на границе и продолжается на следующей странице.
|
||||
if fits_on_page:
|
||||
# Плавающий листинг не разрывается между страницами: иначе рамка
|
||||
# кода обрывается на границе и продолжается на следующей странице.
|
||||
opts.append("float=htbp")
|
||||
if not fits_on_page:
|
||||
opts.append("frame=none")
|
||||
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{code}\n\\end{{lstlisting}}"
|
||||
|
||||
|
||||
@block("fence")
|
||||
def fence(r, tok: Token) -> str:
|
||||
r.pos += 1
|
||||
return _lstlisting(tok, None)
|
||||
return _lstlisting(tok, None, r.current_heading == APPENDIX)
|
||||
|
||||
|
||||
def match_listing(tokens: list[Token], pos: int) -> int | None:
|
||||
@@ -88,7 +106,7 @@ def match_listing(tokens: list[Token], pos: int) -> int | None:
|
||||
|
||||
|
||||
@pattern(match_listing)
|
||||
def captioned_listing(tokens: list[Token]) -> str:
|
||||
def captioned_listing(r, tokens: list[Token]) -> str:
|
||||
match = _CAPTION_RE.match(tokens[1].content)
|
||||
assert match is not None # гарантировано match_listing
|
||||
return _lstlisting(tokens[3], match.group(1))
|
||||
return _lstlisting(tokens[3], match.group(1), r.current_heading == APPENDIX)
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from markdown_it.token import Token
|
||||
|
||||
from md2gost.handlers.inline import render_inline
|
||||
from md2gost.handlers.inline import render_inline, strip_marks
|
||||
from md2gost.registry import block
|
||||
|
||||
UNNUMBERED = {
|
||||
@@ -11,6 +11,7 @@ UNNUMBERED = {
|
||||
"ВВЕДЕНИЕ",
|
||||
"ЗАКЛЮЧЕНИЕ",
|
||||
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
|
||||
"ПРИЛОЖЕНИЕ",
|
||||
}
|
||||
|
||||
TOC_HEADING = "СОДЕРЖАНИЕ"
|
||||
@@ -19,6 +20,7 @@ NEWPAGE_BEFORE = {
|
||||
"ВВЕДЕНИЕ",
|
||||
"ЗАКЛЮЧЕНИЕ",
|
||||
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
|
||||
"ПРИЛОЖЕНИЕ",
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +31,10 @@ def heading(r, tok: Token) -> str:
|
||||
text = render_inline(inline_tok)
|
||||
r.pos += 2
|
||||
|
||||
upper = text.upper()
|
||||
# Имя берётся из исходного текста, а не из готового LaTeX: в выделенном
|
||||
# заголовке «==ПРИЛОЖЕНИЕ==» готовый текст — это \\hlw{ПРИЛОЖЕНИЕ}, и
|
||||
# ненумерованный раздел не узнавался бы по имени.
|
||||
upper = strip_marks(inline_tok.content).strip().upper()
|
||||
|
||||
if upper == TOC_HEADING:
|
||||
r.pending_toc = True
|
||||
|
||||
@@ -83,7 +83,7 @@ def match_figure(tokens: list[Token], pos: int) -> int | None:
|
||||
|
||||
|
||||
@pattern(match_figure)
|
||||
def figure(tokens: list[Token]) -> str:
|
||||
def figure(r, tokens: list[Token]) -> str:
|
||||
img_tok = _single_image_paragraph(tokens, 0)
|
||||
assert img_tok is not None
|
||||
raw_src = unquote(str(img_tok.attrGet("src") or ""))
|
||||
|
||||
@@ -90,6 +90,11 @@ def bind_initials(text: str) -> str:
|
||||
_MARK_RE = re.compile(r"==(?=\S)(.+?)(?<=\S)==")
|
||||
|
||||
|
||||
def strip_marks(text: str) -> str:
|
||||
"""Снять пометки правок, оставив сам текст."""
|
||||
return _MARK_RE.sub(lambda match: match.group(1), text)
|
||||
|
||||
|
||||
def render_text(text: str) -> str:
|
||||
return split_compounds(bind_initials(escape_latex(text)))
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ def match_table(tokens: list[Token], pos: int) -> int | None:
|
||||
|
||||
|
||||
@pattern(match_table)
|
||||
def captioned_table(tokens: list[Token]) -> str:
|
||||
def captioned_table(r, tokens: list[Token]) -> str:
|
||||
m = _CAPTION_RE.match(tokens[1].content)
|
||||
assert m is not None
|
||||
caption = escape_latex(m.group(1))
|
||||
|
||||
+5
-2
@@ -8,6 +8,8 @@
|
||||
- ``PATTERNS`` — упорядоченный список составных условий. Функция сопоставления
|
||||
``match(tokens, pos) -> int | None`` возвращает число потребляемых токенов
|
||||
или None; чистая функция без побочных эффектов. Проверяются до BLOCK.
|
||||
Обработчик получает ``(renderer, tokens)``: составному блоку бывает нужен
|
||||
раздел, в котором он оказался.
|
||||
- ``INLINE`` — обработчики инлайн-токенов внутри абзацев/заголовков.
|
||||
Обработчик получает только токен и возвращает LaTeX-строку.
|
||||
|
||||
@@ -27,7 +29,7 @@ if TYPE_CHECKING:
|
||||
BlockHandler = Callable[["Renderer", "Token"], str]
|
||||
InlineHandler = Callable[["Token"], str]
|
||||
MatchFn = Callable[[list["Token"], int], "int | None"]
|
||||
PatternHandler = Callable[[list["Token"]], str]
|
||||
PatternHandler = Callable[["Renderer", list["Token"]], str]
|
||||
|
||||
BLOCK: dict[str, BlockHandler] = {}
|
||||
INLINE: dict[str, InlineHandler] = {}
|
||||
@@ -50,7 +52,8 @@ def pattern(match: MatchFn) -> Callable[[PatternHandler], PatternHandler]:
|
||||
|
||||
``match(tokens, pos)`` возвращает количество потребляемых токенов,
|
||||
начиная с ``pos``, либо None если условие не выполнено. Обработчик
|
||||
получает срез ``tokens[pos : pos + n]``; pos двигает сам рендерер.
|
||||
получает рендерер и срез ``tokens[pos : pos + n]``; pos двигает сам
|
||||
рендерер.
|
||||
"""
|
||||
|
||||
def deco(fn: PatternHandler) -> PatternHandler:
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ class Renderer:
|
||||
span = match_fn(tokens, pos)
|
||||
if span is not None and span > 0:
|
||||
self.pos += span
|
||||
return handler_fn(tokens[pos : pos + span])
|
||||
return handler_fn(self, tokens[pos : pos + span])
|
||||
|
||||
handler = registry.BLOCK.get(tok.type)
|
||||
if handler is None:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from md2gost.handlers.code import PAGE_LINES
|
||||
from md2gost.handlers.table import _STRETCH
|
||||
from md2gost.latex.titlepages import TITLEPAGES
|
||||
from md2gost.parser import Parser
|
||||
@@ -518,6 +519,45 @@ def test_abbreviation_column_is_as_wide_as_the_abbreviation() -> None:
|
||||
assert "\\begin{tabularx}{\\linewidth}" in tex
|
||||
|
||||
|
||||
def test_appendix_starts_a_new_page_and_reaches_the_contents() -> None:
|
||||
tex = render("# ПРИЛОЖЕНИЕ\n\nЛистинг 1 — Код\n\n```cpp\nint x;\n```\n")
|
||||
assert "\\section{" not in tex
|
||||
assert "\\clearpage" in tex
|
||||
assert "\\addcontentsline{toc}{section}{ПРИЛОЖЕНИЕ}" in tex
|
||||
|
||||
|
||||
def test_marked_unnumbered_heading_is_still_recognised() -> None:
|
||||
"""В «==ПРИЛОЖЕНИЕ==» имя раздела не должно теряться за разметкой."""
|
||||
tex = render("# ==ПРИЛОЖЕНИЕ==\n\nТекст.\n")
|
||||
assert "\\section{" not in tex
|
||||
assert "\\clearpage" in tex
|
||||
assert "\\hlw{ПРИЛОЖЕНИЕ}" in tex
|
||||
|
||||
|
||||
def test_long_listing_does_not_float() -> None:
|
||||
"""Листинг длиннее страницы не помещается в плавающий объект."""
|
||||
code = "\n".join(f"int x{i};" for i in range(PAGE_LINES + 1))
|
||||
tex = body(render(f"Листинг 1 — Длинный\n\n```cpp\n{code}\n```\n"))
|
||||
assert "caption={Длинный}" in tex
|
||||
assert "float=" not in tex
|
||||
assert "frame=none" in tex
|
||||
|
||||
|
||||
def test_listing_in_appendix_does_not_float() -> None:
|
||||
"""В приложении плавающий объект переставил бы листинги местами."""
|
||||
tex = body(render(
|
||||
"# ПРИЛОЖЕНИЕ\n\nЛистинг 1 — Короткий\n\n```cpp\nint x;\n```\n"
|
||||
))
|
||||
assert "float=" not in tex
|
||||
assert "frame=none" in tex
|
||||
|
||||
|
||||
def test_short_listing_keeps_the_frame() -> None:
|
||||
tex = body(render("Листинг 1 — Короткий\n\n```cpp\nint x;\n```\n"))
|
||||
assert "float=htbp" in tex
|
||||
assert "frame=none" not in tex
|
||||
|
||||
|
||||
def test_cpp_uses_extended_dialect() -> None:
|
||||
"""Встроенный C++ в listings не знает auto, nullptr и override."""
|
||||
tex = body(render("```cpp\nauto x = nullptr;\n```\n"))
|
||||
|
||||
Reference in New Issue
Block a user