[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() {
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)
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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 = {
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+16 -17
View File
@@ -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("}")
+3 -2
View File
@@ -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
+2 -1
View File
@@ -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):
+2 -1
View File
@@ -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
+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:
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__":
+9 -4
View File
@@ -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]