[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:
@@ -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
@@ -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,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,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,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,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
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,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
|
||||
|
||||
_LATEX_SPECIAL = str.maketrans({
|
||||
_LATEX_SPECIAL = str.maketrans(
|
||||
{
|
||||
"&": r"\&",
|
||||
"%": r"\%",
|
||||
"$": r"\$",
|
||||
@@ -16,7 +14,8 @@ _LATEX_SPECIAL = str.maketrans({
|
||||
"~": 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("}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user