[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() {
|
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,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,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,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,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-17
@@ -1,22 +1,21 @@
|
|||||||
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
|
{
|
||||||
|
"&": r"\&",
|
||||||
_LATEX_SPECIAL = str.maketrans({
|
"%": r"\%",
|
||||||
"&": r"\&",
|
"$": r"\$",
|
||||||
"%": r"\%",
|
"#": r"\#",
|
||||||
"$": r"\$",
|
"_": r"\_",
|
||||||
"#": r"\#",
|
"{": r"\{",
|
||||||
"_": r"\_",
|
"}": r"\}",
|
||||||
"{": r"\{",
|
"~": r"\textasciitilde{}",
|
||||||
"}": r"\}",
|
"^": r"\textasciicircum{}",
|
||||||
"~": r"\textasciitilde{}",
|
"\\": r"\textbackslash{}",
|
||||||
"^": r"\textasciicircum{}",
|
}
|
||||||
"\\": 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("}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user