[ADD] вывод предупреждений и ошибок LaTeX наружу из контейнера

This commit is contained in:
Arseny
2026-09-02 12:52:05 +03:00
parent 4f0c2ce16d
commit 6b1f508825
4 changed files with 277 additions and 10 deletions
+128
View File
@@ -0,0 +1,128 @@
"""Разбор журнала xelatex: ошибки, предупреждения и дефекты вёрстки.
Без этого разбора журнал остаётся во временном каталоге сборки и исчезает
вместе с ним — при запуске в контейнере узнать о выходе текста за поле
можно было только открыв PDF и заметив это глазом.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
# «Overfull \hbox (56.7pt too wide) in alignment at lines 220--220»
# «Underfull \hbox (badness 7000) in paragraph at lines 10--11»
# «Overfull \hbox (7.2pt too wide) detected at line 14»
_BOX_RE = re.compile(
r"^(?P<kind>Overfull|Underfull)\s+\\hbox\s+"
r"\((?:(?P<pt>[\d.]+)pt too wide|badness (?P<badness>\d+))\)"
r"(?:.*?(?:at lines (?P<lines>\d+)--\d+|detected at line (?P<line>\d+)))?",
re.MULTILINE,
)
_ERROR_RE = re.compile(r"^! (?P<message>.+)$", re.MULTILINE)
_WARNING_RE = re.compile(
r"^(?P<message>(?:LaTeX|Package|Class)[^\n]*Warning:[^\n]*"
r"(?:\n(?!\s*$)(?!(?:LaTeX|Package|Class)[^\n]*Warning:)[^\n]*)*)",
re.MULTILINE,
)
# Пакет unicode-math на каждой сборке сообщает, что перекрывает команды
# mathtools. Это ожидаемо и ничего не требует, но два многострочных сообщения
# в каждом документе заглушают собой те предупреждения, на которые надо смотреть.
_IGNORED_WARNINGS = (
"unicode-math Warning: Using \\overbracket",
"unicode-math Warning: I'm going to overwrite",
# Оформление подписей объявлено в преамбуле для всех видов объектов сразу;
# в документе без таблиц или листингов часть настроек закономерно не нужна.
"caption Warning: Unused \\captionsetup",
)
# Виды, при которых --strict считает сборку неудачной. Разреженные строки и
# предупреждения пакетов бывают следствием намеренной вёрстки (растяжки на
# титульном листе), а выход текста за поле — всегда видимый брак.
STRICT_KINDS = ("error", "overfull")
_TITLES = {
"error": "ошибки LaTeX",
"warning": "предупреждения LaTeX",
"overfull": "текст выходит за правое поле",
"underfull": "разреженные строки",
}
_MAX_PER_KIND = 20
@dataclass(frozen=True)
class Diagnostic:
kind: str
message: str
line: int | None = None
amount: float | None = None
def parse_log(
text: str,
*,
overfull_threshold_pt: float = 1.0,
underfull_threshold: int = 5000,
) -> list[Diagnostic]:
"""Диагностика из журнала сборки.
Пороги отсекают шум: вылет в доли пункта глазом не виден, а строки с
небольшой разреженностью встречаются в любом нормально свёрстанном тексте.
"""
diags: list[Diagnostic] = []
for match in _ERROR_RE.finditer(text):
diags.append(Diagnostic("error", match.group("message").strip()))
for match in _WARNING_RE.finditer(text):
message = " ".join(match.group("message").split())
if any(noise in message for noise in _IGNORED_WARNINGS):
continue
diags.append(Diagnostic("warning", message))
for match in _BOX_RE.finditer(text):
line_group = match.group("lines") or match.group("line")
line = int(line_group) if line_group else None
if match.group("pt") is not None:
amount = float(match.group("pt"))
if amount < overfull_threshold_pt:
continue
diags.append(Diagnostic("overfull", match.group(0).strip(), line, amount))
else:
badness = float(match.group("badness"))
if badness < underfull_threshold:
continue
diags.append(Diagnostic("underfull", match.group(0).strip(), line, badness))
return diags
def _describe(diag: Diagnostic) -> str:
where = f"строка {diag.line}" if diag.line is not None else "место не указано"
if diag.kind == "overfull":
return f" {where}: на {diag.amount:.1f} pt шире поля"
if diag.kind == "underfull":
return f" {where}: разреженность {diag.amount:.0f}"
return f" {diag.message}"
def format_diagnostics(diags: list[Diagnostic]) -> str:
"""Человекочитаемая сводка; пустая строка, если сообщать не о чем."""
if not diags:
return ""
blocks: list[str] = []
for kind in ("error", "warning", "overfull", "underfull"):
group = [d for d in diags if d.kind == kind]
if not group:
continue
if kind in ("overfull", "underfull"):
group.sort(key=lambda d: d.amount or 0.0, reverse=True)
lines = [f"{_TITLES[kind]}: {len(group)}"]
lines += [_describe(d) for d in group[:_MAX_PER_KIND]]
if len(group) > _MAX_PER_KIND:
lines.append(f" и ещё {len(group) - _MAX_PER_KIND}")
blocks.append("\n".join(lines))
return "\n".join(blocks)