"""Builders for small synthetic EPUB files used throughout the tests."""
from __future__ import annotations
import json
import posixpath
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}{head}
{body}
"""
@dataclass
class Doc:
name: str # file name inside OEBPS/
body: str
title: str = ""
linear: bool = True
css: list[str] | None = None # linked stylesheets (OEBPS-relative) – default: every .css in Book.media
@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", head="",
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:
css = d.css if d.css is not None else [n for n in book.media if n.lower().endswith(".css")]
head = "".join(f''
for c in css)
z.writestr(f"OEBPS/{d.name}", XHTML.format(title=d.title, head=head, 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