from __future__ import annotations import html import re import zipfile from pathlib import Path def write_epub(*, title: str, series: str, chapters: list[dict[str, str]], destination: Path) -> Path: destination.parent.mkdir(parents=True, exist_ok=True) manifest = [ '', '', '', '', ] spine = [''] navigation = [] ncx = [] with zipfile.ZipFile(destination, "w") as archive: archive.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED) archive.writestr( "META-INF/container.xml", '' '' "", ) archive.writestr( "OEBPS/style.css", "body{font-family:serif;line-height:1.45;margin:5%}h1{text-align:center}" "p{text-indent:1.2em;margin:0 0 .35em}.first{text-indent:0}.title{text-align:center;margin-top:30%}", ) archive.writestr( "OEBPS/title.xhtml", _xhtml(title, f'

{html.escape(title)}

{html.escape(series)}

'), ) for index, chapter in enumerate(chapters, start=1): filename = f"chapter-{index}.xhtml" item_id = f"chapter-{index}" chapter_title = chapter.get("title") or f"Chapter {index}" manifest.append( f'' ) spine.append(f'') navigation.append( f'
  • {html.escape(chapter_title)}
  • ' ) ncx.append( f'{html.escape(chapter_title)}' f'' ) paragraphs = [] for paragraph_index, paragraph in enumerate( part.strip() for part in re.split(r"\n\s*\n", chapter.get("content", "")) if part.strip() ): class_name = ' class="first"' if paragraph_index == 0 else "" paragraphs.append(f"{html.escape(paragraph)}

    ") archive.writestr( f"OEBPS/{filename}", _xhtml(chapter_title, f"

    {html.escape(chapter_title)}

    {''.join(paragraphs)}"), ) archive.writestr( "OEBPS/nav.xhtml", _xhtml(title, f''), ) archive.writestr( "OEBPS/toc.ncx", f'' f"{html.escape(title)}{''.join(ncx)}", ) archive.writestr( "OEBPS/content.opf", f'' f'artifex-{html.escape(title)}' f"{html.escape(title)}en" f'{"".join(manifest)}{"".join(spine)}', ) return destination def _xhtml(title: str, body: str) -> str: return ( '' '' f"{html.escape(title)}" f"{body}" )