MVP готов
This commit is contained in:
Mikan
2025-12-08 17:48:35 +03:00
parent 5b08f6792a
commit bba1e8ca7e
8 changed files with 242 additions and 16 deletions

60
DEVELOPER.md Normal file
View File

@@ -0,0 +1,60 @@
# Документация разработчика
## Архитектура
```
src/
├── __main__.py # CLI (typer)
├── core/
│ ├── converter.py # основная логика
│ ├── renderer.py # парсинг и диспетчеризация
│ └── style_registry.py # стили
├── handlers/
│ ├── __init__.py # реестр обработчиков
│ └── builtin.py # встроенные обработчики
├── plugins/ # плагины (пользовательские)
└── utils.py # вспомогательные функции
```
## Добавление нового обработчика
1. Создайте функцию:
```python
@register_handler("new_element")
def handle_new_element(node, doc, images_dir, style_reg):
# ваш код
pass
```
2. Регистрируйте через `@register_handler("type")`
3. В `mistune` должен быть соответствующий плагин (если нужен кастомный синтаксис)
## Стили
Стили определяются в YAML и применяются через `StyleRegistry`.
## Тестирование
```bash
pytest tests/
pytest tests/ -v -s # с логами
```
Тесты покрывают:
- Обработчики
- Стили
- CLI
- Плагины
## Docker
Сборка: `docker build -t md2docx .`
Запуск: `docker run -v ./inputs:/app/inputs -v ./outputs:/app/outputs md2docx ...`
## CI/CD (опционально)
- Запуск тестов
- Сборка Docker
- Публикация в PyPI (опционально)

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
FROM python:3.11-slim
WORKDIR /app
# Копируем pyproject.toml, чтобы получить версию
COPY pyproject.toml .
# Устанавливаем toml, чтобы получить версию
RUN pip install toml
RUN python -c "import toml; print('VERSION=' + toml.load('pyproject.toml')['project']['version'])" > version.txt
RUN echo "VERSION=$(cat version.txt | cut -d= -f2)" >> /etc/environment
# Установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копируем исходники
COPY src/ ./src/
COPY resources/ ./resources/
# Папки для входных и выходных данных
VOLUME ["/app/inputs", "/app/outputs"]
# Точка входа
ENTRYPOINT ["python", "-m", "src"]
# Пример запуска:
# docker run -v ./my_inputs:/app/inputs -v ./my_outputs:/app/outputs md2docx --input inputs --output outputs

101
README.md Normal file
View File

@@ -0,0 +1,101 @@
# md-to-docx
Конвертер Markdown в Word (.docx) с поддержкой стилей, тем и плагинов.
## Установка
```bash
pip install -r requirements.txt
```
## Использование
### Структура входных данных:
```
inputs/
├── my_doc/
│ ├── document.md
│ └── images/
│ ├── img1.png
│ └── img2.jpg
```
### Простой запуск:
```bash
python -m src --input inputs --output outputs
```
### С использованием темы:
```bash
python -m src --input inputs --output outputs --theme academic
```
### С шаблоном .dotx:
```bash
python -m src --input inputs --output outputs --template template.dotx
```
### С плагинами:
```bash
python -m src --input inputs --output outputs --plugins my_plugins/
```
### Дополнительные опции:
- `--verbose` / `-v` — подробный лог
- `--quiet` / `-q` — без логов
- `--dry-run` — показать, что будет сделано
- `--style path/to/style.yaml` — использовать свой стиль
## Поддерживаемые элементы
- Заголовки `#`, `##`, `###`
- Параграфы
- Списки: `-`, `1.`
- **Жирный**, *курсив*, [ссылки](https://example.com)
- Изображения: `![alt](images/file.png)`
- Таблицы
- `<br>` внутри таблиц и параграфов
## Стили
Стили задаются в `resources/style_config.yaml` или через `--theme`.
Пример:
```yaml
styles:
Heading 1:
font:
name: "Times New Roman"
size: 16
bold: true
```
## Плагины
Можно создать свой обработчик:
```python
# plugins/my_block.py
from src.handlers import register_handler
@register_handler("my_block")
def handle_my_block(node, doc, images_dir, style_reg):
# ваш код
pass
```
И использовать: `--plugins plugins/`
## Docker
```bash
docker build -t md2docx .
docker run -v ./inputs:/app/inputs -v ./outputs:/app/outputs md2docx --input inputs --output outputs
```

22
pyproject.toml Normal file
View File

@@ -0,0 +1,22 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "md-to-docx"
version = "0.1.0"
description = "Конвертер Markdown в Word (.docx) с поддержкой стилей, плагинов и тем"
authors = [{name = "Mikan", email = "MikanDrawChannel@gmail.com"}]
license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"mistune>=3.0.0",
"python-docx",
"typer[all]",
"PyYAML",
"Pillow"
]
[project.scripts]
md2docx = "src.__main__:app"

View File

@@ -2,3 +2,4 @@ mistune>=3.0.0
python-docx
typer[all]
PyYAML
Pillow

View File

@@ -8,6 +8,8 @@ app = typer.Typer()
import logging
logger = logging.getLogger(__name__)
@app.command()
def convert(
input_dir: Path = typer.Option(..., "--input", "-i", help="Папка с исходниками"),
@@ -16,9 +18,24 @@ def convert(
template: Optional[Path] = typer.Option(None, "--template", "-t", help="Шаблон .dotx"),
theme: Optional[str] = typer.Option(None, "--theme", "-T", help="Название темы (gost, academic...)"),
plugins_dir: Optional[Path] = typer.Option(None, "--plugins", "-p", help="Папка с плагинами"),
dry_run: bool = typer.Option(False, "--dry-run", help="Только показать, что будет сделано"),
quiet: bool = typer.Option(False, "--quiet", "-q", help="Не выводить логи"),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Включить отладочные сообщения"),
):
log_level = logging.DEBUG if verbose else logging.INFO
if quiet:
log_level = logging.WARNING
elif verbose:
log_level = logging.DEBUG
else:
log_level = logging.INFO
logging.basicConfig(level=log_level, format='%(levelname)s: %(message)s')
if dry_run:
logger.info("Режим --dry-run: файлы не будут созданы")
for doc_dir in input_dir.iterdir():
if doc_dir.is_dir() and (doc_dir / "document.md").exists():
logger.info(f"Будет обработан: {doc_dir.name}")
return
process_document(input_dir, output_dir, style_config, template, theme, plugins_dir)

View File

@@ -12,23 +12,14 @@ logger = logging.getLogger(__name__)
def process_document(
input_dir: Path, output_dir: Path, style_config_path: Path,
template_path: Optional[Path] = None, theme_name: Optional[str] = None,
plugins_dir: Optional[Path] = None
plugins_dir: Optional[Path] = None, dry_run: bool = False
):
if plugins_dir and plugins_dir.exists():
load_plugins_from_path(plugins_dir)
if theme_name:
theme_path = Path("resources/themes") / f"{theme_name}.yaml"
if theme_path.exists():
style_config_path = theme_path
else:
logger.warning(f"Тема {theme_name} не найдена, используем стандартный стиль")
for doc_dir in input_dir.iterdir():
if not doc_dir.is_dir():
continue
# Пропускаем папку images
if doc_dir.name == "images":
continue
for doc_dir in input_dir.iterdir():
if not doc_dir.is_dir() or doc_dir.name == "images":
continue
doc_name = doc_dir.name
md_path = doc_dir / "document.md"
images_dir = doc_dir / "images"
@@ -37,6 +28,10 @@ def process_document(
logger.warning(f"Пропущено: {doc_name} — нет document.md")
continue
if dry_run:
logger.info(f"[DRY-RUN] Будет создан: {output_dir}/{doc_name}.docx")
continue
with open(md_path, encoding="utf-8") as f:
md_content = f.read()

View File

@@ -1,6 +1,6 @@
import logging
logger = logging.getLogger(__name__)
from pathlib import Path
@@ -8,6 +8,8 @@ from src.handlers import register_handler
from docx.shared import Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
logger = logging.getLogger(__name__)
@register_handler("heading")
def handle_heading(node: dict, doc, images_dir: Path, style_reg):
# mistune v3: node["attrs"]["level"]