Artifex/control_plane/authoring/epub.py
2026-08-28 23:51:02 +07:00

88 lines
4.2 KiB
Python

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 = [
'<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>',
'<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>',
'<item id="css" href="style.css" media-type="text/css"/>',
'<item id="title" href="title.xhtml" media-type="application/xhtml+xml"/>',
]
spine = ['<itemref idref="title"/>']
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",
'<?xml version="1.0"?><container version="1.0" '
'xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles>'
'<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>'
"</rootfiles></container>",
)
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'<div class="title"><h1>{html.escape(title)}</h1><p>{html.escape(series)}</p></div>'),
)
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'<item id="{item_id}" href="{filename}" media-type="application/xhtml+xml"/>'
)
spine.append(f'<itemref idref="{item_id}"/>')
navigation.append(
f'<li><a href="{filename}">{html.escape(chapter_title)}</a></li>'
)
ncx.append(
f'<navPoint id="n{index}" playOrder="{index}"><navLabel><text>{html.escape(chapter_title)}</text></navLabel>'
f'<content src="{filename}"/></navPoint>'
)
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"<p{class_name}>{html.escape(paragraph)}</p>")
archive.writestr(
f"OEBPS/{filename}",
_xhtml(chapter_title, f"<h1>{html.escape(chapter_title)}</h1>{''.join(paragraphs)}"),
)
archive.writestr(
"OEBPS/nav.xhtml",
_xhtml(title, f'<nav xmlns:epub="http://www.idpf.org/2007/ops" epub:type="toc"><ol>{"".join(navigation)}</ol></nav>'),
)
archive.writestr(
"OEBPS/toc.ncx",
f'<?xml version="1.0"?><ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">'
f"<docTitle><text>{html.escape(title)}</text></docTitle><navMap>{''.join(ncx)}</navMap></ncx>",
)
archive.writestr(
"OEBPS/content.opf",
f'<?xml version="1.0"?><package xmlns="http://www.idpf.org/2007/opf" unique-identifier="bookid" version="3.0">'
f'<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:identifier id="bookid">artifex-{html.escape(title)}</dc:identifier>'
f"<dc:title>{html.escape(title)}</dc:title><dc:language>en</dc:language></metadata>"
f'<manifest>{"".join(manifest)}</manifest><spine toc="ncx">{"".join(spine)}</spine></package>',
)
return destination
def _xhtml(title: str, body: str) -> str:
return (
'<?xml version="1.0" encoding="UTF-8"?>'
'<html xmlns="http://www.w3.org/1999/xhtml"><head>'
f"<title>{html.escape(title)}</title><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"/>"
f"</head><body>{body}</body></html>"
)