"""Builders for small synthetic EPUB files used throughout the tests.""" from __future__ import annotations import json import sys import zipfile from dataclasses import dataclass, field from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) from epub2md.cli import main # noqa: E402 # 1x1 transparent PNG PNG = bytes.fromhex( "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489" "0000000d4944415478da63f8ffff3f0005fe02fea7d6a5b60000000049454e44ae426082" ) SVG = b'' XHTML = """ {title} {body} """ @dataclass class Doc: name: str # file name inside OEBPS/ body: str title: str = "" linear: bool = True @dataclass class Book: docs: list[Doc] version: str = "3.0" title: str = "Testbuch" authors: list[str] = field(default_factory=lambda: ["Erika Musterfrau"]) language: str = "de" identifier: str = "urn:isbn:9783161484100" media: dict[str, bytes] = field(default_factory=dict) # OEBPS-relative path -> bytes nav: list[tuple[str, str]] | None = None # (label, href) – default: one entry per doc ncx: bool = False extra_entries: dict[str, bytes] = field(default_factory=dict) # raw ZIP entries (any name) container: str | None = None opf_override: str | None = None spine_extra: str = "" cover: str | None = None MEDIA_TYPES = {".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", ".gif": "image/gif", ".mp3": "audio/mpeg", ".css": "text/css", ".otf": "font/otf"} def build_epub(path: Path, book: Book) -> Path: manifest = [] spine = [] for i, d in enumerate(book.docs): manifest.append(f'') spine.append(f'") for i, (name, _) in enumerate(book.media.items()): ext = "." + name.rsplit(".", 1)[-1].lower() props = ' properties="cover-image"' if book.cover == name else "" manifest.append(f'') nav_entries = book.nav if book.nav is not None else [(d.title or d.name, d.name) for d in book.docs] epub3 = book.version.startswith("3") if epub3: manifest.append('') if book.ncx: manifest.append('') creators = "".join(f"{a}" for a in book.authors) opf = book.opf_override or f""" {book.title}{creators} {book.language} {book.identifier} Testverlag 2024-01-01 {''.join(manifest)} {''.join(spine)}{book.spine_extra} """ nav_xhtml = XHTML.format( title="Inhalt", body='", ) ncx = ( '' + "".join( f'{lbl}' for i, (lbl, h) in enumerate(nav_entries) ) + "" ) container = book.container or ( '' '' ) with zipfile.ZipFile(path, "w") as z: z.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED) z.writestr("META-INF/container.xml", container) z.writestr("OEBPS/content.opf", opf) if epub3: z.writestr("OEBPS/nav.xhtml", nav_xhtml) if book.ncx: z.writestr("OEBPS/toc.ncx", ncx) for d in book.docs: z.writestr(f"OEBPS/{d.name}", XHTML.format(title=d.title, body=d.body)) for name, data in book.media.items(): z.writestr(f"OEBPS/{name}", data) for name, data in book.extra_entries.items(): z.writestr(zipfile.ZipInfo(name), data) return path @dataclass class Run: rc: int out: Path stdout: str stderr: str def chapter(self, prefix: str) -> str: files = sorted((self.out / "chapters").glob(f"{prefix}*.md")) assert files, f"no chapter {prefix} in {sorted(p.name for p in (self.out / 'chapters').glob('*.md'))}" return files[0].read_text(encoding="utf-8") def chapters(self) -> list[Path]: return sorted((self.out / "chapters").glob("*.md")) def all_md(self) -> str: return "\n".join(p.read_text(encoding="utf-8") for p in self.chapters()) @property def report(self) -> dict: return json.loads((self.out / "conversion-report.json").read_text(encoding="utf-8")) @pytest.fixture def convert(tmp_path, capsys): counter = {"n": 0} def run(book_or_path, *args: str, out: Path | None = None) -> Run: counter["n"] += 1 if isinstance(book_or_path, Book): epub = build_epub(tmp_path / f"book{counter['n']}.epub", book_or_path) else: epub = Path(book_or_path) target = out or tmp_path / f"out{counter['n']}" rc = main([str(epub), str(target), *args]) captured = capsys.readouterr() return Run(rc, target, captured.out, captured.err) return run