153 lines
5.6 KiB
Python
153 lines
5.6 KiB
Python
import argparse
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import md2gost
|
|
from md2gost.latex.log import STRICT_KINDS, format_diagnostics, parse_log
|
|
from md2gost.parser import Parser, iter_image_sources
|
|
from md2gost.render import Renderer
|
|
|
|
|
|
def _report_latex_log(log_path: Path) -> bool:
|
|
"""Печатает диагностику сборки. Возвращает True при серьёзных дефектах.
|
|
|
|
Журнал живёт во временном каталоге и исчезает вместе с ним, поэтому всё,
|
|
о чём сообщил xelatex, нужно вывести наружу здесь — иначе при запуске
|
|
в контейнере узнать о дефектах вёрстки неоткуда.
|
|
"""
|
|
if not log_path.exists():
|
|
return False
|
|
diags = parse_log(log_path.read_text(errors="replace"))
|
|
summary = format_diagnostics(diags)
|
|
if summary:
|
|
print(summary, file=sys.stderr)
|
|
return any(d.kind in STRICT_KINDS for d in diags)
|
|
|
|
|
|
def run_xelatex(tex_path: Path, output_dir: Path) -> bool:
|
|
result = subprocess.run(
|
|
[
|
|
"xelatex",
|
|
"-interaction=nonstopmode",
|
|
"-output-directory",
|
|
str(output_dir),
|
|
str(tex_path),
|
|
],
|
|
# Относительные пути картинок резолвятся от CWD, поэтому компилируем
|
|
# из каталога, куда _copy_assets сложил все изображения.
|
|
cwd=str(output_dir),
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
pdf_path = output_dir / tex_path.with_suffix(".pdf").name
|
|
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",
|
|
description="Markdown -> LaTeX конвертер по ГОСТ Р 7.0.5-2008",
|
|
)
|
|
cli.add_argument("input", type=Path, help="путь к .md файлу")
|
|
cli.add_argument(
|
|
"-o",
|
|
"--output",
|
|
type=Path,
|
|
default=Path("."),
|
|
help="выходная директория (по умолчанию текущая)",
|
|
)
|
|
cli.add_argument(
|
|
"--no-pdf",
|
|
action="store_true",
|
|
help="только .tex, без компиляции в PDF",
|
|
)
|
|
cli.add_argument(
|
|
"--strict",
|
|
action="store_true",
|
|
help="выйти с ошибкой, если xelatex сообщил о предупреждениях",
|
|
)
|
|
args = cli.parse_args()
|
|
|
|
if not args.input.exists():
|
|
print(f"ошибка: {args.input} не найден", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
args.output.mkdir(parents=True, exist_ok=True)
|
|
|
|
md_text = args.input.read_text(encoding="utf-8")
|
|
parser = Parser(md_text)
|
|
renderer = Renderer(parser.tokens, parser.front_matter)
|
|
latex = renderer.render()
|
|
|
|
with tempfile.TemporaryDirectory(prefix="md2gost-") as cache:
|
|
cache_dir = Path(cache)
|
|
|
|
tex_name = args.input.with_suffix(".tex").name
|
|
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:
|
|
log_path = cache_dir / tex_path.with_suffix(".log").name
|
|
pdf_name = args.input.with_suffix(".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)
|
|
_report_latex_log(log_path)
|
|
sys.exit(1)
|
|
|
|
final_pdf = args.output / pdf_name
|
|
shutil.copy2(cache_dir / pdf_name, final_pdf)
|
|
print(f"-> {final_pdf}")
|
|
|
|
# Разбирается журнал второго прохода: после первого ссылки и номера
|
|
# страниц ещё не разложены, и часть предупреждений ложная.
|
|
has_defects = _report_latex_log(log_path)
|
|
if has_defects and args.strict:
|
|
print("ошибка: --strict, а вёрстка содержит дефекты", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|