111 lines
3.4 KiB
Python
111 lines
3.4 KiB
Python
"""DOCX → md2gost Markdown pipeline."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import traceback
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from docx import Document
|
|
|
|
from .emit import emit_markdown
|
|
from .media import MediaExtractor
|
|
from .refs import apply_reference_pass
|
|
from .walker import DocxWalker, postprocess_blocks
|
|
|
|
LogFn = Callable[[str], None]
|
|
|
|
|
|
@dataclass
|
|
class ImportRequest:
|
|
filename: str = ""
|
|
output: str | None = None
|
|
media_dir: str | None = None
|
|
keep_toc_pages: bool = False
|
|
emit_page_breaks: bool = False
|
|
|
|
|
|
@dataclass
|
|
class ImportResult:
|
|
ok: bool
|
|
exit_code: int = 0
|
|
output_path: str | None = None
|
|
media_dir: str | None = None
|
|
message: str = ""
|
|
|
|
|
|
def default_output_path(docx_path: str | Path) -> Path:
|
|
p = Path(docx_path)
|
|
return p.with_suffix(".md")
|
|
|
|
|
|
def default_media_dir(md_path: Path) -> Path:
|
|
return md_path.parent / f"{md_path.stem}_media"
|
|
|
|
|
|
def convert_docx(req: ImportRequest, log: LogFn | None = None) -> ImportResult:
|
|
"""Convert DOCX to md2gost dialect Markdown. Errors go into ImportResult."""
|
|
emit: LogFn = log if log is not None else print
|
|
|
|
def fail(code: int, message: str) -> ImportResult:
|
|
emit(message)
|
|
return ImportResult(False, code, message=message)
|
|
|
|
filename = (req.filename or "").strip()
|
|
if not filename:
|
|
return fail(2, "Укажите исходный .docx")
|
|
path = Path(filename)
|
|
if not path.is_file():
|
|
return fail(2, f"Файл не найден: {path}")
|
|
if path.suffix.lower() != ".docx":
|
|
return fail(1, "Исходный файл должен быть в формате .docx")
|
|
|
|
out = Path(req.output) if req.output else default_output_path(path)
|
|
if out.suffix.lower() != ".md":
|
|
out = out.with_suffix(".md")
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
media_path = Path(req.media_dir) if req.media_dir else default_media_dir(out)
|
|
rel_prefix = media_path.name
|
|
# If media dir is not sibling of md, use relative path from md parent
|
|
try:
|
|
rel_prefix = str(media_path.resolve().relative_to(out.parent.resolve())).replace("\\", "/")
|
|
except ValueError:
|
|
rel_prefix = str(media_path).replace("\\", "/")
|
|
|
|
try:
|
|
document = Document(str(path))
|
|
except Exception as exc:
|
|
return fail(1, f"Не удалось открыть DOCX: {exc}")
|
|
|
|
try:
|
|
with MediaExtractor(path, media_path, rel_prefix=rel_prefix) as media:
|
|
walker = DocxWalker(
|
|
document,
|
|
media,
|
|
keep_toc_pages=req.keep_toc_pages,
|
|
emit_page_breaks=req.emit_page_breaks,
|
|
log=emit,
|
|
)
|
|
blocks = walker.walk()
|
|
blocks = postprocess_blocks(blocks)
|
|
blocks = apply_reference_pass(blocks)
|
|
md_text = emit_markdown(blocks, emit_page_breaks=req.emit_page_breaks)
|
|
out.write_text(md_text, encoding="utf-8")
|
|
except Exception as exc:
|
|
emit(traceback.format_exc())
|
|
return fail(1, f"Ошибка импорта: {exc}")
|
|
|
|
msg = f"Markdown: {out}"
|
|
emit(msg)
|
|
if media_path.is_dir() and any(media_path.iterdir()):
|
|
emit(f"Медиа: {media_path}")
|
|
return ImportResult(
|
|
True,
|
|
0,
|
|
output_path=str(out.resolve()),
|
|
media_dir=str(media_path.resolve()) if media_path.exists() else None,
|
|
message=msg,
|
|
)
|