diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000..6e826eb --- /dev/null +++ b/DEVELOPER.md @@ -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 (опционально) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1d58e24 --- /dev/null +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ca6170 --- /dev/null +++ b/README.md @@ -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)` +- Таблицы +- `
` внутри таблиц и параграфов + +## Стили + +Стили задаются в `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 +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3225138 --- /dev/null +++ b/pyproject.toml @@ -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" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6f24a94..c7f7c84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ mistune>=3.0.0 python-docx typer[all] -PyYAML \ No newline at end of file +PyYAML +Pillow \ No newline at end of file diff --git a/src/__main__.py b/src/__main__.py index 7b6f602..5f874c4 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -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) \ No newline at end of file diff --git a/src/core/converter.py b/src/core/converter.py index fc60d9f..b9ac7ed 100644 --- a/src/core/converter.py +++ b/src/core/converter.py @@ -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() diff --git a/src/handlers/builtin.py b/src/handlers/builtin.py index 29a777a..e2704df 100644 --- a/src/handlers/builtin.py +++ b/src/handlers/builtin.py @@ -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"]