diff --git a/src/__main__.py b/src/__main__.py index 64ba54e..7d5b86c 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,16 +1,22 @@ +from typing import Optional + import typer from pathlib import Path from .core.converter import process_document app = typer.Typer() +import logging + @app.command() def convert( input_dir: Path = typer.Option(..., "--input", "-i", help="Папка с исходниками"), output_dir: Path = typer.Option(..., "--output", "-o", help="Папка для результата"), - style_config: Path = typer.Option("resources/style_config.yaml", "--style", "-s", help="Файл стилей") + style_config: Path = typer.Option("resources/style_config.yaml", "--style", "-s", help="Файл стилей"), + template: Optional[Path] = typer.Option(None, "--template", "-t", help="Шаблон .dotx"), + verbose: bool = typer.Option(False, "--verbose", "-v", help="Включить отладочные сообщения"), ): - process_document(input_dir, output_dir, style_config) + log_level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig(level=log_level, format='%(levelname)s: %(message)s') -if __name__ == "__main__": - app() \ No newline at end of file + process_document(input_dir, output_dir, style_config, template) \ No newline at end of file diff --git a/src/core/converter.py b/src/core/converter.py index e08c1de..82f564f 100644 --- a/src/core/converter.py +++ b/src/core/converter.py @@ -1,30 +1,43 @@ from pathlib import Path +from typing import Optional + from docx import Document from .style_registry import StyleRegistry from .renderer import render_markdown_to_docx -def process_document(input_dir: Path, output_dir: Path, style_config_path: Path): - style_reg = StyleRegistry(style_config_path) +import logging +logger = logging.getLogger(__name__) + +def process_document(input_dir: Path, output_dir: Path, style_config_path: Path, template_path: Optional[Path] = None): for doc_dir in input_dir.iterdir(): if not doc_dir.is_dir(): continue + # Пропускаем папку images + if doc_dir.name == "images": + continue + doc_name = doc_dir.name md_path = doc_dir / "document.md" images_dir = doc_dir / "images" if not md_path.exists(): - print(f"⚠️ Пропущено: {doc_name} — нет document.md") + logger.warning(f"Пропущено: {doc_name} — нет document.md") continue with open(md_path, encoding="utf-8") as f: md_content = f.read() - doc = Document() - style_reg.ensure_styles_in_doc(doc) # добавляем стили + if template_path and template_path.exists(): + doc = Document(template_path) + else: + doc = Document() + + style_reg = StyleRegistry(style_config_path) + style_reg.ensure_styles_in_doc(doc) render_markdown_to_docx(doc, md_content, images_dir, style_reg) output_path = output_dir / f"{doc_name}.docx" output_path.parent.mkdir(parents=True, exist_ok=True) doc.save(output_path) - print(f"✅ {doc_name} → {output_path}") \ No newline at end of file + logger.info(f"Создан документ: {output_path}") \ No newline at end of file diff --git a/src/core/renderer.py b/src/core/renderer.py index 53d674a..c4c38ee 100644 --- a/src/core/renderer.py +++ b/src/core/renderer.py @@ -7,11 +7,15 @@ def render_markdown_to_docx(doc, md_content: str, images_dir, style_reg: StyleRe ast = markdown(md_content) def walk(node): - handler = get_handler(node["type"]) - if handler: - handler(node, doc, images_dir, style_reg) - elif node.get("children"): - for child in node["children"]: - walk(child) + if isinstance(node, list): + for item in node: + walk(item) + elif isinstance(node, dict): + handler = get_handler(node["type"]) + if handler: + handler(node, doc, images_dir, style_reg) + elif node.get("children"): + for child in node["children"]: + walk(child) walk(ast) \ No newline at end of file diff --git a/src/handlers/builtin.py b/src/handlers/builtin.py index 8717897..29a777a 100644 --- a/src/handlers/builtin.py +++ b/src/handlers/builtin.py @@ -10,7 +10,10 @@ from docx.enum.text import WD_ALIGN_PARAGRAPH @register_handler("heading") def handle_heading(node: dict, doc, images_dir: Path, style_reg): - level = min(node["level"], 3) + # mistune v3: node["attrs"]["level"] + # тесты: node["level"] + level = node.get("attrs", {}).get("level") or node.get("level") + level = min(level, 3) text = node["children"][0]["raw"] if node["children"] else "" doc.add_heading(text, level=level) p = doc.paragraphs[-1] diff --git a/tests/test_converter.py b/tests/test_converter.py new file mode 100644 index 0000000..d58d77e --- /dev/null +++ b/tests/test_converter.py @@ -0,0 +1,48 @@ +import pytest +from pathlib import Path +from docx import Document +from src.core.converter import process_document + +@pytest.fixture +def create_example_doc(): + # Создаём inputs/example_doc/ + doc_dir = Path("tests/fixtures/inputs/example_doc") + doc_dir.mkdir(parents=True, exist_ok=True) + + # Создаём document.md + md_content = "# Заголовок\n\nПараграф." + (doc_dir / "document.md").write_text(md_content, encoding="utf-8") + + # Создаём images/ + (doc_dir / "images").mkdir(exist_ok=True) + + yield doc_dir.parent # возвращаем Path("tests/fixtures/inputs") + + # Удаляем после теста + import shutil + shutil.rmtree(doc_dir.parent, ignore_errors=True) + +def test_process_document_with_template(create_example_doc): + # create_example_doc = Path("tests/fixtures/inputs") + input_dir = create_example_doc + + # Создать пустой .dotx файл для теста + tpl_path = Path("tests/fixtures/template.dotx") + tpl_path.parent.mkdir(exist_ok=True) + doc = Document() + doc.save(tpl_path) + + # Запустить конвертацию с шаблоном + process_document( + input_dir=input_dir, + output_dir=Path("tests/output"), + style_config_path=Path("resources/style_config.yaml"), + template_path=tpl_path + ) + + output_path = Path("tests/output/example_doc.docx") + assert output_path.exists() + + # Удаляем файл после теста + output_path.unlink(missing_ok=True) + tpl_path.unlink(missing_ok=True) \ No newline at end of file diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 5c018ae..e2106b3 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -1,6 +1,8 @@ import pytest from pathlib import Path from docx import Document + +from src.core.converter import process_document from src.handlers.builtin import handle_heading, handle_paragraph, handle_image, handle_list, handle_table from src.core.style_registry import StyleRegistry @@ -149,3 +151,4 @@ def test_handle_table_with_br(): cell_text = table.cell(1, 0).text # "Первая строка\nВторая строка" assert "Первая строка" in cell_text assert "Вторая строка" in cell_text +