[UPDATE] рефакторинг: Unicode -> ASCII, README на русском, форматирование

- Unicode стрелки (->) -> ASCII (->)
- Добавлен README.md на русском языке
- from __future__ import annotations во всех файлах
- Исправлены ошибки mypy (Callable[..., str], Token import)
- make format + make tidy проходят чисто
This commit is contained in:
Arseny
2026-07-16 09:00:24 +03:00
parent 59ba0555bb
commit c116d7d9af
12 changed files with 138 additions and 40 deletions
+77
View File
@@ -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)
```
+3 -3
View File
@@ -6,8 +6,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
usage() { usage() {
echo "Использование: md2gost file.md [-o dir]" echo "Использование: md2gost file.md [-o dir]"
echo " file.md исходный Markdown-файл" echo " file.md - исходный Markdown-файл"
echo " -o dir выходная директория (по умолчанию ./build)" echo " -o dir - выходная директория (по умолчанию ./build)"
exit 1 exit 1
} }
@@ -57,7 +57,7 @@ if ! docker image inspect "$IMAGE" &>/dev/null; then
docker build -t "$IMAGE" "$SCRIPT_DIR" docker build -t "$IMAGE" "$SCRIPT_DIR"
fi fi
echo " конвертация $INPUT_FILE $OUTPUT_DIR/" echo "-> конвертация $INPUT_FILE -> $OUTPUT_DIR/"
# Запуск контейнера: # Запуск контейнера:
# - вход: только .md файл (read-only) # - вход: только .md файл (read-only)
+2 -1
View File
@@ -2,8 +2,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from markdown_it.token import Token
if TYPE_CHECKING: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer from md2gost.render import Renderer
+2 -1
View File
@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from markdown_it.token import Token
from md2gost.handlers.inline import render_inline from md2gost.handlers.inline import render_inline
if TYPE_CHECKING: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer from md2gost.render import Renderer
UNNUMBERED = { UNNUMBERED = {
+2 -1
View File
@@ -2,8 +2,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from markdown_it.token import Token
if TYPE_CHECKING: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer from md2gost.render import Renderer
+2 -1
View File
@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from markdown_it.token import Token
from md2gost.handlers.inline import render_inline from md2gost.handlers.inline import render_inline
if TYPE_CHECKING: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer from md2gost.render import Renderer
+6 -7
View File
@@ -1,11 +1,9 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from markdown_it.token import Token
if TYPE_CHECKING: _LATEX_SPECIAL = str.maketrans(
from markdown_it.token import Token {
_LATEX_SPECIAL = str.maketrans({
"&": r"\&", "&": r"\&",
"%": r"\%", "%": r"\%",
"$": r"\$", "$": r"\$",
@@ -16,7 +14,8 @@ _LATEX_SPECIAL = str.maketrans({
"~": r"\textasciitilde{}", "~": r"\textasciitilde{}",
"^": r"\textasciicircum{}", "^": r"\textasciicircum{}",
"\\": r"\textbackslash{}", "\\": r"\textbackslash{}",
}) }
)
def escape_latex(text: str) -> str: def escape_latex(text: str) -> str:
@@ -45,7 +44,7 @@ def render_inline_tokens(tokens: list[Token]) -> str:
elif tok.type == "code_inline": elif tok.type == "code_inline":
parts.append(f"\\verb|{tok.content}|") parts.append(f"\\verb|{tok.content}|")
elif tok.type == "link_open": elif tok.type == "link_open":
href = tok.attrGet("href") or "" href = str(tok.attrGet("href") or "")
parts.append(f"\\href{{{escape_latex(href)}}}{{") parts.append(f"\\href{{{escape_latex(href)}}}{{")
elif tok.type == "link_close": elif tok.type == "link_close":
parts.append("}") parts.append("}")
+3 -2
View File
@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING 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: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer from md2gost.render import Renderer
+2 -1
View File
@@ -5,10 +5,11 @@ from typing import TYPE_CHECKING
from md2gost.handlers.inline import render_inline from md2gost.handlers.inline import render_inline
if TYPE_CHECKING: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer 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]] = [] rows: list[list[str]] = []
r.pos += 1 r.pos += 1
while r.pos < len(r.tokens): while r.pos < len(r.tokens):
+2 -1
View File
@@ -2,10 +2,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from markdown_it.token import Token
from md2gost.handlers.inline import render_inline from md2gost.handlers.inline import render_inline
if TYPE_CHECKING: if TYPE_CHECKING:
from markdown_it.token import Token
from md2gost.render import Renderer from md2gost.render import Renderer
+18 -8
View File
@@ -18,9 +18,15 @@ def _print_latex_errors(log_path: Path) -> None:
def run_xelatex(tex_path: Path, output_dir: Path) -> bool: def run_xelatex(tex_path: Path, output_dir: Path) -> bool:
result = subprocess.run( result = subprocess.run(
["xelatex", "-interaction=nonstopmode", [
"-output-directory", str(output_dir), str(tex_path)], "xelatex",
capture_output=True, text=True, "-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 pdf_path = output_dir / tex_path.with_suffix(".pdf").name
return pdf_path.exists() and result.returncode == 0 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: def main() -> None:
cli = argparse.ArgumentParser( cli = argparse.ArgumentParser(
prog="md2gost", 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("input", type=Path, help="путь к .md файлу")
cli.add_argument( cli.add_argument(
"-o", "--output", type=Path, default=Path("."), "-o",
"--output",
type=Path,
default=Path("."),
help="выходная директория (по умолчанию текущая)", help="выходная директория (по умолчанию текущая)",
) )
cli.add_argument( cli.add_argument(
"--no-pdf", action="store_true", "--no-pdf",
action="store_true",
help="только .tex, без компиляции в PDF", help="только .tex, без компиляции в PDF",
) )
args = cli.parse_args() args = cli.parse_args()
@@ -62,7 +72,7 @@ def main() -> None:
final_tex = args.output / tex_name final_tex = args.output / tex_name
shutil.copy2(tex_path, final_tex) shutil.copy2(tex_path, final_tex)
print(f" {final_tex}") print(f"-> {final_tex}")
if not args.no_pdf: if not args.no_pdf:
if not run_xelatex(tex_path, cache_dir): if not run_xelatex(tex_path, cache_dir):
@@ -79,7 +89,7 @@ def main() -> None:
pdf_path = cache_dir / pdf_name pdf_path = cache_dir / pdf_name
final_pdf = args.output / pdf_name final_pdf = args.output / pdf_name
shutil.copy2(pdf_path, final_pdf) shutil.copy2(pdf_path, final_pdf)
print(f" {final_pdf}") print(f"-> {final_pdf}")
if __name__ == "__main__": if __name__ == "__main__":
+9 -4
View File
@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Callable
from md2gost.handlers import ( from md2gost.handlers import (
bullet_list, bullet_list,
fence, fence,
@@ -15,8 +17,7 @@ from md2gost.latex import END_DOCUMENT, PREAMBLE, render_titlepage
from markdown_it.token import Token from markdown_it.token import Token
_BLOCK: dict[str, Callable[..., str]] = {
_BLOCK = {
"heading_open": heading, "heading_open": heading,
"paragraph_open": paragraph, "paragraph_open": paragraph,
"bullet_list_open": bullet_list, "bullet_list_open": bullet_list,
@@ -25,7 +26,7 @@ _BLOCK = {
"hr": hr, "hr": hr,
} }
_INLINE = { _INLINE: dict[str, Callable[..., str]] = {
"fence": fence, "fence": fence,
"image": image, "image": image,
"inline": render_inline, "inline": render_inline,
@@ -40,7 +41,11 @@ class Renderer:
self.current_heading = "" self.current_heading = ""
def render(self) -> str: 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): while self.pos < len(self.tokens):
tok = self.tokens[self.pos] tok = self.tokens[self.pos]