@@ -0,0 +1,110 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user