[AI]
MVP готов
This commit is contained in:
60
DEVELOPER.md
Normal file
60
DEVELOPER.md
Normal 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
28
Dockerfile
Normal 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
101
README.md
Normal 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)
|
||||||
|
- Изображения: ``
|
||||||
|
- Таблицы
|
||||||
|
- `<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
22
pyproject.toml
Normal 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"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mistune>=3.0.0
|
mistune>=3.0.0
|
||||||
python-docx
|
python-docx
|
||||||
typer[all]
|
typer[all]
|
||||||
PyYAML
|
PyYAML
|
||||||
|
Pillow
|
||||||
@@ -8,6 +8,8 @@ app = typer.Typer()
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def convert(
|
def convert(
|
||||||
input_dir: Path = typer.Option(..., "--input", "-i", help="Папка с исходниками"),
|
input_dir: Path = typer.Option(..., "--input", "-i", help="Папка с исходниками"),
|
||||||
@@ -16,9 +18,24 @@ def convert(
|
|||||||
template: Optional[Path] = typer.Option(None, "--template", "-t", help="Шаблон .dotx"),
|
template: Optional[Path] = typer.Option(None, "--template", "-t", help="Шаблон .dotx"),
|
||||||
theme: Optional[str] = typer.Option(None, "--theme", "-T", help="Название темы (gost, academic...)"),
|
theme: Optional[str] = typer.Option(None, "--theme", "-T", help="Название темы (gost, academic...)"),
|
||||||
plugins_dir: Optional[Path] = typer.Option(None, "--plugins", "-p", help="Папка с плагинами"),
|
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="Включить отладочные сообщения"),
|
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')
|
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)
|
process_document(input_dir, output_dir, style_config, template, theme, plugins_dir)
|
||||||
@@ -12,23 +12,14 @@ logger = logging.getLogger(__name__)
|
|||||||
def process_document(
|
def process_document(
|
||||||
input_dir: Path, output_dir: Path, style_config_path: Path,
|
input_dir: Path, output_dir: Path, style_config_path: Path,
|
||||||
template_path: Optional[Path] = None, theme_name: Optional[str] = None,
|
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():
|
if plugins_dir and plugins_dir.exists():
|
||||||
load_plugins_from_path(plugins_dir)
|
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
|
doc_name = doc_dir.name
|
||||||
md_path = doc_dir / "document.md"
|
md_path = doc_dir / "document.md"
|
||||||
images_dir = doc_dir / "images"
|
images_dir = doc_dir / "images"
|
||||||
@@ -37,6 +28,10 @@ def process_document(
|
|||||||
logger.warning(f"Пропущено: {doc_name} — нет document.md")
|
logger.warning(f"Пропущено: {doc_name} — нет document.md")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
logger.info(f"[DRY-RUN] Будет создан: {output_dir}/{doc_name}.docx")
|
||||||
|
continue
|
||||||
|
|
||||||
with open(md_path, encoding="utf-8") as f:
|
with open(md_path, encoding="utf-8") as f:
|
||||||
md_content = f.read()
|
md_content = f.read()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ from src.handlers import register_handler
|
|||||||
from docx.shared import Inches
|
from docx.shared import Inches
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@register_handler("heading")
|
@register_handler("heading")
|
||||||
def handle_heading(node: dict, doc, images_dir: Path, style_reg):
|
def handle_heading(node: dict, doc, images_dir: Path, style_reg):
|
||||||
# mistune v3: node["attrs"]["level"]
|
# mistune v3: node["attrs"]["level"]
|
||||||
|
|||||||
Reference in New Issue
Block a user