работа с шаблонами
This commit is contained in:
@@ -1,16 +1,22 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .core.converter import process_document
|
from .core.converter import process_document
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer()
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def convert(
|
def convert(
|
||||||
input_dir: Path = typer.Option(..., "--input", "-i", help="Папка с исходниками"),
|
input_dir: Path = typer.Option(..., "--input", "-i", help="Папка с исходниками"),
|
||||||
output_dir: Path = typer.Option(..., "--output", "-o", 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__":
|
process_document(input_dir, output_dir, style_config, template)
|
||||||
app()
|
|
||||||
@@ -1,30 +1,43 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from .style_registry import StyleRegistry
|
from .style_registry import StyleRegistry
|
||||||
from .renderer import render_markdown_to_docx
|
from .renderer import render_markdown_to_docx
|
||||||
|
|
||||||
def process_document(input_dir: Path, output_dir: Path, style_config_path: Path):
|
import logging
|
||||||
style_reg = StyleRegistry(style_config_path)
|
|
||||||
|
|
||||||
|
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():
|
for doc_dir in input_dir.iterdir():
|
||||||
if not doc_dir.is_dir():
|
if not doc_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
|
# Пропускаем папку images
|
||||||
|
if 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"
|
||||||
|
|
||||||
if not md_path.exists():
|
if not md_path.exists():
|
||||||
print(f"⚠️ Пропущено: {doc_name} — нет document.md")
|
logger.warning(f"Пропущено: {doc_name} — нет document.md")
|
||||||
continue
|
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()
|
||||||
|
|
||||||
|
if template_path and template_path.exists():
|
||||||
|
doc = Document(template_path)
|
||||||
|
else:
|
||||||
doc = Document()
|
doc = Document()
|
||||||
style_reg.ensure_styles_in_doc(doc) # добавляем стили
|
|
||||||
|
style_reg = StyleRegistry(style_config_path)
|
||||||
|
style_reg.ensure_styles_in_doc(doc)
|
||||||
render_markdown_to_docx(doc, md_content, images_dir, style_reg)
|
render_markdown_to_docx(doc, md_content, images_dir, style_reg)
|
||||||
|
|
||||||
output_path = output_dir / f"{doc_name}.docx"
|
output_path = output_dir / f"{doc_name}.docx"
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
doc.save(output_path)
|
doc.save(output_path)
|
||||||
print(f"✅ {doc_name} → {output_path}")
|
logger.info(f"Создан документ: {output_path}")
|
||||||
@@ -7,6 +7,10 @@ def render_markdown_to_docx(doc, md_content: str, images_dir, style_reg: StyleRe
|
|||||||
ast = markdown(md_content)
|
ast = markdown(md_content)
|
||||||
|
|
||||||
def walk(node):
|
def walk(node):
|
||||||
|
if isinstance(node, list):
|
||||||
|
for item in node:
|
||||||
|
walk(item)
|
||||||
|
elif isinstance(node, dict):
|
||||||
handler = get_handler(node["type"])
|
handler = get_handler(node["type"])
|
||||||
if handler:
|
if handler:
|
||||||
handler(node, doc, images_dir, style_reg)
|
handler(node, doc, images_dir, style_reg)
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ from docx.enum.text import WD_ALIGN_PARAGRAPH
|
|||||||
|
|
||||||
@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):
|
||||||
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 ""
|
text = node["children"][0]["raw"] if node["children"] else ""
|
||||||
doc.add_heading(text, level=level)
|
doc.add_heading(text, level=level)
|
||||||
p = doc.paragraphs[-1]
|
p = doc.paragraphs[-1]
|
||||||
|
|||||||
48
tests/test_converter.py
Normal file
48
tests/test_converter.py
Normal file
@@ -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)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from docx import Document
|
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.handlers.builtin import handle_heading, handle_paragraph, handle_image, handle_list, handle_table
|
||||||
from src.core.style_registry import StyleRegistry
|
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Вторая строка"
|
cell_text = table.cell(1, 0).text # "Первая строка\nВторая строка"
|
||||||
assert "Первая строка" in cell_text
|
assert "Первая строка" in cell_text
|
||||||
assert "Вторая строка" in cell_text
|
assert "Вторая строка" in cell_text
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user