[ADD] вывод предупреждений и ошибок LaTeX наружу из контейнера
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user