[ADD] поддержка изображений и рефакторинг диспетчера токенов

- registry.py: реестры BLOCK/PATTERNS/INLINE с декораторами @block/@pattern/@inline
- render.py: единый диспетчер, слой паттернов, проверка контракта pos (RuntimeError),
  pending_toc вместо current_heading для единственной вставки СОДЕРЖАНИЯ
- handlers/image.py: вики-ссылки ![[путь|ширина]], подпись Рисунок N —— ... ,
  figure-окружение, авто-нумерация, копирование файлов в tmpdir
- handlers/inline.py: табличная диспетчеризация вместо if/elif
- Все block-обработчики подписаны @block, корректно продвигают pos
- parser.py: препроцессинг ![[...]] → ![](path), iter_image_sources()
- main.py: _copy_assets (emblem+картинки в tmpdir), фикс лога (имя .tex),
  cwd=tmpdir для xelatex, цикл вместо дублирования, force-include emblem
- tests/test_render.py: pytest (10 тестов), pyproject dev-dep, make test
- preamble.py: name=Рисунок для figure-caption (было Рис. от babel)
- Исправлены баги: мёртвый обработчик, emblem не копировалась,
  множественный TOC, захардкожен report.log, маскировка pos
- README/AGENTS.md обновлены
This commit is contained in:
Arseny
2026-08-25 13:58:01 +03:00
parent 3e67937b8b
commit 8a921e2337
20 changed files with 592 additions and 138 deletions
+47 -13
View File
@@ -5,7 +5,8 @@ import sys
import tempfile
from pathlib import Path
from md2gost.parser import Parser
import md2gost
from md2gost.parser import Parser, iter_image_sources
from md2gost.render import Renderer
@@ -25,6 +26,9 @@ def run_xelatex(tex_path: Path, output_dir: Path) -> bool:
str(output_dir),
str(tex_path),
],
# Относительные пути картинок резолвятся от CWD, поэтому компилируем
# из каталога, куда _copy_assets сложил все изображения.
cwd=str(output_dir),
capture_output=True,
text=True,
)
@@ -32,6 +36,37 @@ def run_xelatex(tex_path: Path, output_dir: Path) -> bool:
return pdf_path.exists() and result.returncode == 0
def _find_emblem(input_dir: Path) -> Path | None:
candidates = [
input_dir / "emblem.png",
Path(md2gost.__file__).parent / "emblem.png",
]
return next((p for p in candidates if p.is_file()), None)
def _copy_assets(parser: Parser, input_dir: Path, cache_dir: Path) -> None:
"""Копирует эмблему и все картинки документа в каталог компиляции."""
emblem = _find_emblem(input_dir)
if emblem is None:
print(
"предупреждение: emblem.png не найден — титульный лист без эмблемы",
file=sys.stderr,
)
else:
shutil.copy2(emblem, cache_dir / "emblem.png")
for src in iter_image_sources(parser.tokens):
src_path = input_dir / src
if not src_path.is_file():
print(
f"предупреждение: изображение не найдено: {src_path}", file=sys.stderr
)
continue
dest = cache_dir / src
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest)
def main() -> None:
cli = argparse.ArgumentParser(
prog="md2gost",
@@ -70,25 +105,24 @@ def main() -> None:
tex_path = cache_dir / tex_name
tex_path.write_text(latex, encoding="utf-8")
_copy_assets(parser, args.input.parent, cache_dir)
final_tex = args.output / tex_name
shutil.copy2(tex_path, final_tex)
print(f"-> {final_tex}")
if not args.no_pdf:
if not run_xelatex(tex_path, cache_dir):
print("ошибка: xelatex не собрал PDF", file=sys.stderr)
_print_latex_errors(cache_dir / "report.log")
sys.exit(1)
if not run_xelatex(tex_path, cache_dir):
print("ошибка: xelatex (2-й проход) не собрал PDF", file=sys.stderr)
_print_latex_errors(cache_dir / "report.log")
sys.exit(1)
log_path = cache_dir / tex_path.with_suffix(".log").name
pdf_name = args.input.with_suffix(".pdf").name
pdf_path = cache_dir / pdf_name
for attempt in (1, 2):
if not run_xelatex(tex_path, cache_dir):
stage = "" if attempt == 1 else " (2-й проход)"
print(f"ошибка: xelatex{stage} не собрал PDF", file=sys.stderr)
_print_latex_errors(log_path)
sys.exit(1)
final_pdf = args.output / pdf_name
shutil.copy2(pdf_path, final_pdf)
shutil.copy2(cache_dir / pdf_name, final_pdf)
print(f"-> {final_pdf}")