diff --git a/README.md b/README.md index e69de29..7af00f1 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,77 @@ +# md2gost + +Конвертер Markdown в LaTeX по ГОСТ Р 7.0.5-2008. Генерирует PDF-отчёты с титульным листом, содержанием, нумерацией и форматированием по ГОСТ. + +## Установка + +Требуется Docker. Локальная установка LaTeX не нужна. + +```bash +docker build -t md2gost . +``` + +## Использование + +```bash +./md2gost.sh report.md -o ./build +``` + +Скрипт создаст `build/report.tex` и `build/report.pdf`. + +### Флаги + +- `-o dir` -- выходная директория (по умолчанию `./build`) +- `--no-pdf` -- только .tex, без компиляции + +## Формат Markdown + +Используется front matter в начале файла: + +```yaml +--- +факультет: Информатика и системы управления +кафедра: Программные средства +тип: Отчёт по практике +название: Исследование алгоритмов оптимизации +студент: Иванов И.И. +преподаватель: Петров П.П. +группа: ИУ1-12 +год: 2026 +--- +``` + +Обязательные поля: `название`, `тип`. + +### Поддерживаемые элементы + +- Заголовки (1-3 уровень) с нумерацией +- Параграфы, списки (маркированные и нумерованные) +- Таблицы +- Код с подсветкой синтаксиса +- Изображения +- Аббревиатуры (раздел "Перечень принятых сокращений") +- Ссылки, жирный, курсив + +## Структура проекта + +``` +md2gost/ + md2gost/ + main.py # точка входа + parser.py # парсинг markdown + render.py # рендер в LaTeX + handlers/ # обработчики блоков + latex/ # преамбула, титульный лист + md2gost.sh # обёртка для Docker + Dockerfile + Makefile +``` + +## Команды + +```bash +make build # сборка Docker-образа +make run # сборка + конвертация report.md +make format # форматирование кода (black + ruff) +make tidy # статический анализ (ruff + mypy) +``` diff --git a/md2gost.sh b/md2gost.sh index 9198e09..25bb254 100755 --- a/md2gost.sh +++ b/md2gost.sh @@ -6,8 +6,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" usage() { echo "Использование: md2gost file.md [-o dir]" - echo " file.md — исходный Markdown-файл" - echo " -o dir — выходная директория (по умолчанию ./build)" + echo " file.md - исходный Markdown-файл" + echo " -o dir - выходная директория (по умолчанию ./build)" exit 1 } @@ -57,7 +57,7 @@ if ! docker image inspect "$IMAGE" &>/dev/null; then docker build -t "$IMAGE" "$SCRIPT_DIR" fi -echo "→ конвертация $INPUT_FILE → $OUTPUT_DIR/" +echo "-> конвертация $INPUT_FILE -> $OUTPUT_DIR/" # Запуск контейнера: # - вход: только .md файл (read-only) diff --git a/md2gost/handlers/code.py b/md2gost/handlers/code.py index cbe4843..d5823ff 100644 --- a/md2gost/handlers/code.py +++ b/md2gost/handlers/code.py @@ -2,8 +2,9 @@ from __future__ import annotations from typing import TYPE_CHECKING +from markdown_it.token import Token + if TYPE_CHECKING: - from markdown_it.token import Token from md2gost.render import Renderer diff --git a/md2gost/handlers/heading.py b/md2gost/handlers/heading.py index 4c00178..ffe421a 100644 --- a/md2gost/handlers/heading.py +++ b/md2gost/handlers/heading.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING +from markdown_it.token import Token + from md2gost.handlers.inline import render_inline if TYPE_CHECKING: - from markdown_it.token import Token from md2gost.render import Renderer UNNUMBERED = { diff --git a/md2gost/handlers/hr.py b/md2gost/handlers/hr.py index 0807014..3ac62a6 100644 --- a/md2gost/handlers/hr.py +++ b/md2gost/handlers/hr.py @@ -2,8 +2,9 @@ from __future__ import annotations from typing import TYPE_CHECKING +from markdown_it.token import Token + if TYPE_CHECKING: - from markdown_it.token import Token from md2gost.render import Renderer diff --git a/md2gost/handlers/image.py b/md2gost/handlers/image.py index d74ce64..7570dc9 100644 --- a/md2gost/handlers/image.py +++ b/md2gost/handlers/image.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING +from markdown_it.token import Token + from md2gost.handlers.inline import render_inline if TYPE_CHECKING: - from markdown_it.token import Token from md2gost.render import Renderer diff --git a/md2gost/handlers/inline.py b/md2gost/handlers/inline.py index 734dd2a..47bf02d 100644 --- a/md2gost/handlers/inline.py +++ b/md2gost/handlers/inline.py @@ -1,22 +1,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from markdown_it.token import Token -if TYPE_CHECKING: - from markdown_it.token import Token - -_LATEX_SPECIAL = str.maketrans({ - "&": r"\&", - "%": r"\%", - "$": r"\$", - "#": r"\#", - "_": r"\_", - "{": r"\{", - "}": r"\}", - "~": r"\textasciitilde{}", - "^": r"\textasciicircum{}", - "\\": r"\textbackslash{}", -}) +_LATEX_SPECIAL = str.maketrans( + { + "&": r"\&", + "%": r"\%", + "$": r"\$", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\textasciicircum{}", + "\\": r"\textbackslash{}", + } +) def escape_latex(text: str) -> str: @@ -45,7 +44,7 @@ def render_inline_tokens(tokens: list[Token]) -> str: elif tok.type == "code_inline": parts.append(f"\\verb|{tok.content}|") elif tok.type == "link_open": - href = tok.attrGet("href") or "" + href = str(tok.attrGet("href") or "") parts.append(f"\\href{{{escape_latex(href)}}}{{") elif tok.type == "link_close": parts.append("}") diff --git a/md2gost/handlers/lists.py b/md2gost/handlers/lists.py index 796eb0e..c3aa660 100644 --- a/md2gost/handlers/lists.py +++ b/md2gost/handlers/lists.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING -from md2gost.handlers.inline import render_inline, render_inline_tokens +from markdown_it.token import Token + +from md2gost.handlers.inline import render_inline_tokens if TYPE_CHECKING: - from markdown_it.token import Token from md2gost.render import Renderer diff --git a/md2gost/handlers/table.py b/md2gost/handlers/table.py index f8e94db..64553c8 100644 --- a/md2gost/handlers/table.py +++ b/md2gost/handlers/table.py @@ -5,10 +5,11 @@ from typing import TYPE_CHECKING from md2gost.handlers.inline import render_inline if TYPE_CHECKING: + from markdown_it.token import Token from md2gost.render import Renderer -def table(r: Renderer, tok: None = None) -> str: +def table(r: Renderer, tok: Token | None = None) -> str: rows: list[list[str]] = [] r.pos += 1 while r.pos < len(r.tokens): diff --git a/md2gost/handlers/text.py b/md2gost/handlers/text.py index f3c8808..eb3327f 100644 --- a/md2gost/handlers/text.py +++ b/md2gost/handlers/text.py @@ -2,10 +2,11 @@ from __future__ import annotations from typing import TYPE_CHECKING +from markdown_it.token import Token + from md2gost.handlers.inline import render_inline if TYPE_CHECKING: - from markdown_it.token import Token from md2gost.render import Renderer diff --git a/md2gost/main.py b/md2gost/main.py index 36516db..4659d65 100644 --- a/md2gost/main.py +++ b/md2gost/main.py @@ -18,9 +18,15 @@ def _print_latex_errors(log_path: Path) -> None: def run_xelatex(tex_path: Path, output_dir: Path) -> bool: result = subprocess.run( - ["xelatex", "-interaction=nonstopmode", - "-output-directory", str(output_dir), str(tex_path)], - capture_output=True, text=True, + [ + "xelatex", + "-interaction=nonstopmode", + "-output-directory", + str(output_dir), + str(tex_path), + ], + capture_output=True, + text=True, ) pdf_path = output_dir / tex_path.with_suffix(".pdf").name return pdf_path.exists() and result.returncode == 0 @@ -29,15 +35,19 @@ def run_xelatex(tex_path: Path, output_dir: Path) -> bool: def main() -> None: cli = argparse.ArgumentParser( prog="md2gost", - description="Markdown → LaTeX конвертер по ГОСТ Р 7.0.5-2008", + description="Markdown -> LaTeX конвертер по ГОСТ Р 7.0.5-2008", ) cli.add_argument("input", type=Path, help="путь к .md файлу") cli.add_argument( - "-o", "--output", type=Path, default=Path("."), + "-o", + "--output", + type=Path, + default=Path("."), help="выходная директория (по умолчанию текущая)", ) cli.add_argument( - "--no-pdf", action="store_true", + "--no-pdf", + action="store_true", help="только .tex, без компиляции в PDF", ) args = cli.parse_args() @@ -62,7 +72,7 @@ def main() -> None: final_tex = args.output / tex_name shutil.copy2(tex_path, final_tex) - print(f"→ {final_tex}") + print(f"-> {final_tex}") if not args.no_pdf: if not run_xelatex(tex_path, cache_dir): @@ -79,7 +89,7 @@ def main() -> None: pdf_path = cache_dir / pdf_name final_pdf = args.output / pdf_name shutil.copy2(pdf_path, final_pdf) - print(f"→ {final_pdf}") + print(f"-> {final_pdf}") if __name__ == "__main__": diff --git a/md2gost/render.py b/md2gost/render.py index 05764f9..f0c15fc 100644 --- a/md2gost/render.py +++ b/md2gost/render.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Callable + from md2gost.handlers import ( bullet_list, fence, @@ -15,8 +17,7 @@ from md2gost.latex import END_DOCUMENT, PREAMBLE, render_titlepage from markdown_it.token import Token - -_BLOCK = { +_BLOCK: dict[str, Callable[..., str]] = { "heading_open": heading, "paragraph_open": paragraph, "bullet_list_open": bullet_list, @@ -25,7 +26,7 @@ _BLOCK = { "hr": hr, } -_INLINE = { +_INLINE: dict[str, Callable[..., str]] = { "fence": fence, "image": image, "inline": render_inline, @@ -40,7 +41,11 @@ class Renderer: self.current_heading = "" def render(self) -> str: - parts: list[str] = [PREAMBLE, render_titlepage(self.fm), "\\setcounter{page}{2}"] + parts: list[str] = [ + PREAMBLE, + render_titlepage(self.fm), + "\\setcounter{page}{2}", + ] while self.pos < len(self.tokens): tok = self.tokens[self.pos]