89 lines
4.2 KiB
Python
89 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>"
|
||
|
|
)
|