Spaces:
Runtime error
Runtime error
Update FairDinkumPublishing ebook pipeline
Browse files- requirements.txt +0 -1
- src/__pycache__/pipeline_runner.cpython-313.pyc +0 -0
- src/pipeline_runner.py +162 -80
requirements.txt
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
gradio>=5.0
|
| 2 |
-
ebooklib>=0.20
|
| 3 |
fpdf2>=2.8
|
| 4 |
Pillow>=10.0
|
|
|
|
| 1 |
gradio>=5.0
|
|
|
|
| 2 |
fpdf2>=2.8
|
| 3 |
Pillow>=10.0
|
src/__pycache__/pipeline_runner.cpython-313.pyc
ADDED
|
Binary file (74.8 kB). View file
|
|
|
src/pipeline_runner.py
CHANGED
|
@@ -8,7 +8,7 @@ Agents:
|
|
| 8 |
2. Outline Architect β part/chapter/section structure
|
| 9 |
3. Chapter Writer β full long-form chapters
|
| 10 |
4. Cover Generator β Pillow front/back covers
|
| 11 |
-
5. Publisher β EPUB (
|
| 12 |
|
| 13 |
Usage:
|
| 14 |
python3 pipeline_runner.py "Port Noarlunga jetty fishing South Australia"
|
|
@@ -19,14 +19,15 @@ from pathlib import Path
|
|
| 19 |
# ββ pip install inline (Space env) ββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
def ensure_deps():
|
| 21 |
try:
|
| 22 |
-
import
|
| 23 |
except ImportError:
|
| 24 |
subprocess.check_call([sys.executable, "-m", "pip", "install", "--quiet",
|
| 25 |
-
"
|
| 26 |
ensure_deps()
|
| 27 |
|
| 28 |
-
import
|
| 29 |
-
from
|
|
|
|
| 30 |
from fpdf import FPDF
|
| 31 |
from PIL import Image, ImageDraw, ImageFont
|
| 32 |
|
|
@@ -527,116 +528,197 @@ def _md_to_xhtml(md_text: str) -> str:
|
|
| 527 |
|
| 528 |
|
| 529 |
def build_epub(concept: dict, outline: dict, out_path: str) -> str:
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
| 557 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 558 |
<title>{title}</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 559 |
<body>{body_xhtml}</body></html>"""
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
|
| 565 |
# Title page
|
| 566 |
-
|
| 567 |
-
tp.content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 568 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 569 |
<title>Title</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 570 |
<body><div class="cover-page">
|
| 571 |
<h1>{concept['title']}</h1><h2>{concept['subtitle']}</h2>
|
| 572 |
-
<p><em>by {concept.get('author', '
|
| 573 |
<p class='noindent'>{concept['primary_keyword']} β {', '.join(concept['geo_keywords'])}</p>
|
| 574 |
</div></body></html>"""
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
# Cover
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
media_type="image/jpeg", content=front_jpg.read_bytes()))
|
| 583 |
-
cov = epub.EpubHtml(title="Cover", file_name="text/cover.xhtml", lang="en-AU")
|
| 584 |
-
cov.content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 585 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 586 |
<title>Cover</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 587 |
<body><div class="cover-page"><img class="cover" src="../images/cover.jpg" alt="Cover"/></div></body></html>"""
|
| 588 |
-
|
|
|
|
|
|
|
| 589 |
|
| 590 |
-
# TOC page
|
| 591 |
-
|
| 592 |
-
toc_html = ["<nav epub:type='toc'><h2>Table of Contents</h2><ul>"]
|
| 593 |
for part in outline["parts"]:
|
| 594 |
-
|
| 595 |
for ch in part["chapters"]:
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
|
|
|
|
|
|
| 600 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 601 |
<title>Contents</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 602 |
-
<body>{chr(10).join(
|
| 603 |
-
|
|
|
|
|
|
|
| 604 |
|
| 605 |
# Chapters
|
| 606 |
for part in outline["parts"]:
|
| 607 |
for ch in part["chapters"]:
|
| 608 |
-
|
|
|
|
| 609 |
if md_path.exists():
|
| 610 |
body = _md_to_xhtml(md_path.read_text(encoding="utf-8"))
|
| 611 |
else:
|
| 612 |
body = f"<h1>{ch['title']}</h1><p>Content coming soon.</p>"
|
| 613 |
-
|
| 614 |
|
| 615 |
# Back cover
|
| 616 |
-
|
| 617 |
-
back_jpg = COVERS / "back_cover.jpg"
|
| 618 |
-
if back_jpg.exists():
|
| 619 |
-
book.add_item(epub.EpubItem(uid="back_img", file_name="images/back_cover.jpg",
|
| 620 |
-
media_type="image/jpeg", content=back_jpg.read_bytes()))
|
| 621 |
-
back.content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 622 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 623 |
<title>Back Cover</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 624 |
-
<body><div class="back-cover">
|
| 625 |
-
|
| 626 |
-
|
|
|
|
|
|
|
| 627 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 628 |
<title>Back Cover</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 629 |
-
<body><div class="back-cover"><
|
| 630 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
|
|
|
|
|
|
|
|
|
| 640 |
|
| 641 |
|
| 642 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 8 |
2. Outline Architect β part/chapter/section structure
|
| 9 |
3. Chapter Writer β full long-form chapters
|
| 10 |
4. Cover Generator β Pillow front/back covers
|
| 11 |
+
5. Publisher β EPUB (zipfile) + PDF (fpdf2) + Obsidian vault
|
| 12 |
|
| 13 |
Usage:
|
| 14 |
python3 pipeline_runner.py "Port Noarlunga jetty fishing South Australia"
|
|
|
|
| 19 |
# ββ pip install inline (Space env) ββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
def ensure_deps():
|
| 21 |
try:
|
| 22 |
+
import fpdf, PIL # noqa: F401
|
| 23 |
except ImportError:
|
| 24 |
subprocess.check_call([sys.executable, "-m", "pip", "install", "--quiet",
|
| 25 |
+
"fpdf2", "Pillow"])
|
| 26 |
ensure_deps()
|
| 27 |
|
| 28 |
+
import zipfile
|
| 29 |
+
from fpdf import FPDF
|
| 30 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 31 |
from fpdf import FPDF
|
| 32 |
from PIL import Image, ImageDraw, ImageFont
|
| 33 |
|
|
|
|
| 528 |
|
| 529 |
|
| 530 |
def build_epub(concept: dict, outline: dict, out_path: str) -> str:
|
| 531 |
+
"""Build a valid EPUB 3 package using zipfile (no fpdf2 dependency)."""
|
| 532 |
+
out = Path(out_path)
|
| 533 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 534 |
+
|
| 535 |
+
def slugify_toc(text):
|
| 536 |
+
text = text.lower()
|
| 537 |
+
text = re.sub(r"[^a-z0-9]+", "_", text)
|
| 538 |
+
text = re.sub(r"_+", "_", text).strip("_")
|
| 539 |
+
return text[:60]
|
| 540 |
+
|
| 541 |
+
chapters = []
|
| 542 |
+
for part in outline["parts"]:
|
| 543 |
+
for ch in part["chapters"]:
|
| 544 |
+
chapters.append(ch)
|
| 545 |
+
|
| 546 |
+
# ββ OPF manifest entries ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 547 |
+
manifest_items = [
|
| 548 |
+
'<item id="style" href="style/nav.css" media-type="text/css"/>',
|
| 549 |
+
]
|
| 550 |
+
if (COVERS / "front_cover.jpg").exists():
|
| 551 |
+
manifest_items.append('<item id="cover" href="images/cover.jpg" media-type="image/jpeg"/>')
|
| 552 |
+
if (COVERS / "back_cover.jpg").exists():
|
| 553 |
+
manifest_items.append('<item id="back" href="images/back_cover.jpg" media-type="image/jpeg"/>')
|
| 554 |
+
|
| 555 |
+
spine_ids = ["titlepage"]
|
| 556 |
+
toc_entries = []
|
| 557 |
+
|
| 558 |
+
def add_chapter_xhtml(ch_num, title, body_xhtml, spine_id):
|
| 559 |
+
fname = f"ch{ch_num:02d}_{slugify_toc(title)}.xhtml"
|
| 560 |
+
xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 561 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 562 |
<title>{title}</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 563 |
<body>{body_xhtml}</body></html>"""
|
| 564 |
+
files[f"text/{fname}"] = xhtml.encode("utf-8")
|
| 565 |
+
manifest_items.append(f'<item id="{spine_id}" href="text/{fname}" media-type="application/xhtml+xml"/>')
|
| 566 |
+
spine_ids.append(spine_id)
|
| 567 |
+
toc_entries.append((spine_id, title, f"text/{fname}"))
|
| 568 |
|
| 569 |
# Title page
|
| 570 |
+
tp_xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
| 571 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 572 |
<title>Title</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 573 |
<body><div class="cover-page">
|
| 574 |
<h1>{concept['title']}</h1><h2>{concept['subtitle']}</h2>
|
| 575 |
+
<p><em>by {concept.get('author', 'Brett Anthony Sjoberg')}</em></p>
|
| 576 |
<p class='noindent'>{concept['primary_keyword']} β {', '.join(concept['geo_keywords'])}</p>
|
| 577 |
</div></body></html>"""
|
| 578 |
+
files["text/title_page.xhtml"] = tp_xhtml.encode("utf-8")
|
| 579 |
+
manifest_items.append('<item id="titlepage" href="text/title_page.xhtml" media-type="application/xhtml+xml"/>')
|
| 580 |
+
|
| 581 |
+
# Cover page
|
| 582 |
+
if (COVERS / "front_cover.jpg").exists():
|
| 583 |
+
files["images/cover.jpg"] = (COVERS / "front_cover.jpg").read_bytes()
|
| 584 |
+
cov_xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
|
|
|
|
|
| 585 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 586 |
<title>Cover</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 587 |
<body><div class="cover-page"><img class="cover" src="../images/cover.jpg" alt="Cover"/></div></body></html>"""
|
| 588 |
+
files["text/cover.xhtml"] = cov_xhtml.encode("utf-8")
|
| 589 |
+
manifest_items.append('<item id="coverpage" href="text/cover.xhtml" media-type="application/xhtml+xml"/>')
|
| 590 |
+
spine_ids.insert(1, "coverpage")
|
| 591 |
|
| 592 |
+
# TOC page
|
| 593 |
+
toc_xhtml_lines = ["<nav epub:type='toc'><h2>Table of Contents</h2><ul>"]
|
|
|
|
| 594 |
for part in outline["parts"]:
|
| 595 |
+
toc_xhtml_lines.append(f"<li><span class='toc-part'>{part['name']}</span><ul>")
|
| 596 |
for ch in part["chapters"]:
|
| 597 |
+
ch_num = int(ch["id"][2:])
|
| 598 |
+
href = f"text/ch{ch_num:02d}_{slugify_toc(ch['title'])}.xhtml"
|
| 599 |
+
toc_xhtml_lines.append(f"<li><a class='toc-entry' href='../{href}'>{ch['title']}</a></li>")
|
| 600 |
+
toc_xhtml_lines.append("</ul></li>")
|
| 601 |
+
toc_xhtml_lines.append("</ul></nav>")
|
| 602 |
+
toc_xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 603 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 604 |
<title>Contents</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 605 |
+
<body>{chr(10).join(toc_xhtml_lines)}</body></html>"""
|
| 606 |
+
files["text/toc_page.xhtml"] = toc_xhtml.encode("utf-8")
|
| 607 |
+
manifest_items.append('<item id="tocpage" href="text/toc_page.xhtml" media-type="application/xhtml+xml"/>')
|
| 608 |
+
spine_ids.append("tocpage")
|
| 609 |
|
| 610 |
# Chapters
|
| 611 |
for part in outline["parts"]:
|
| 612 |
for ch in part["chapters"]:
|
| 613 |
+
ch_num = int(ch["id"][2:])
|
| 614 |
+
md_path = CHAPTERS / f"{ch['id']}_{slugify_toc(ch['title'])}.md"
|
| 615 |
if md_path.exists():
|
| 616 |
body = _md_to_xhtml(md_path.read_text(encoding="utf-8"))
|
| 617 |
else:
|
| 618 |
body = f"<h1>{ch['title']}</h1><p>Content coming soon.</p>"
|
| 619 |
+
add_chapter_xhtml(ch_num, ch["title"], body, f"ch{ch_num:02d}")
|
| 620 |
|
| 621 |
# Back cover
|
| 622 |
+
back_xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 624 |
<title>Back Cover</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 625 |
+
<body><div class="back-cover">
|
| 626 |
+
<h2>About This Book</h2><p>{concept['description']}</p></div></body></html>"""
|
| 627 |
+
if (COVERS / "back_cover.jpg").exists():
|
| 628 |
+
files["images/back_cover.jpg"] = (COVERS / "back_cover.jpg").read_bytes()
|
| 629 |
+
back_xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 630 |
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 631 |
<title>Back Cover</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 632 |
+
<body><div class="back-cover"><img class="cover" src="../images/back_cover.jpg" alt="Back Cover"/></div></body></html>"""
|
| 633 |
+
files["text/back_cover.xhtml"] = back_xhtml.encode("utf-8")
|
| 634 |
+
manifest_items.append('<item id="backcover" href="text/back_cover.xhtml" media-type="application/xhtml+xml"/>')
|
| 635 |
+
spine_ids.append("backcover")
|
| 636 |
+
|
| 637 |
+
# ββ NCX βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 638 |
+
ncx_nav_points = []
|
| 639 |
+
for spine_id, title, href in toc_entries:
|
| 640 |
+
ncx_nav_points.append(f"""<navPoint id="{spine_id}" playOrder="{spine_ids.index(spine_id) + 1}">
|
| 641 |
+
<navLabel><text>{title}</text></navLabel>
|
| 642 |
+
<content src="{href}"/>
|
| 643 |
+
</navPoint>""")
|
| 644 |
+
toc_ncx = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 645 |
+
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
|
| 646 |
+
<head><meta name="dtb:uid" content="{concept['id']}"/>
|
| 647 |
+
<meta name="dtb:depth" content="1"/><meta name="dtb:totalPageCount" content="0"/>
|
| 648 |
+
<meta name="dtb:maxPageNumber" content="0"/></head>
|
| 649 |
+
<docTitle><text>{concept['title']}</text></docTitle>
|
| 650 |
+
<navMap>{chr(10).join(ncx_nav_points)}</navMap>
|
| 651 |
+
</ncx>"""
|
| 652 |
+
|
| 653 |
+
# ββ Container ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 654 |
+
container_xml = """<?xml version="1.0" encoding="UTF-8"?>
|
| 655 |
+
<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
|
| 656 |
+
<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles>
|
| 657 |
+
</container>"""
|
| 658 |
+
|
| 659 |
+
# ββ OPF ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 660 |
+
opf = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 661 |
+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
|
| 662 |
+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
| 663 |
+
<dc:identifier id="uid">{concept['id']}</dc:identifier>
|
| 664 |
+
<dc:title>{concept['title']}</dc:title>
|
| 665 |
+
<dc:language>en-AU</dc:language>
|
| 666 |
+
<dc:creator>{concept.get('author', 'Brett Anthony Sjoberg')}</dc:creator>
|
| 667 |
+
<dc:publisher>{concept.get('publisher', 'Brett Anthony Sjoberg')}</dc:publisher>
|
| 668 |
+
<dc:description>{concept['description']}</dc:description>
|
| 669 |
+
<dc:subject>{', '.join([concept['primary_keyword']] + concept['secondary_keywords'])}</dc:subject>
|
| 670 |
+
<dc:date>{concept.get('created_at', '')[:10]}</dc:date>
|
| 671 |
+
<dc:rights>Β© {datetime.date.today().year} {concept.get('author', 'Brett Anthony Sjoberg')} β ABN {concept.get('abn', '')}</dc:rights>
|
| 672 |
+
<meta name="geo" content="Australia"/>
|
| 673 |
+
<meta name="keywords" content="{', '.join(concept['geo_keywords'])}"/>
|
| 674 |
+
</metadata>
|
| 675 |
+
<manifest>
|
| 676 |
+
{chr(10).join(manifest_items)}
|
| 677 |
+
</manifest>
|
| 678 |
+
<spine toc="toc">
|
| 679 |
+
{chr(10).join(f'<itemref idref="{sid}"/>' for sid in spine_ids)}
|
| 680 |
+
</spine>
|
| 681 |
+
</package>"""
|
| 682 |
+
|
| 683 |
+
# ββ Assemble ZIP βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 684 |
+
files = {
|
| 685 |
+
"mimetype": b"application/epub+zip",
|
| 686 |
+
"META-INF/container.xml": container_xml.encode("utf-8"),
|
| 687 |
+
"OEBPS/content.opf": opf.encode("utf-8"),
|
| 688 |
+
"OEBPS/toc.ncx": toc_ncx.encode("utf-8"),
|
| 689 |
+
"OEBPS/style/nav.css": EPUB_CSS.encode("utf-8"),
|
| 690 |
+
"OEBPS/text/title_page.xhtml": tp_xhtml.encode("utf-8"),
|
| 691 |
+
"OEBPS/text/toc_page.xhtml": toc_xhtml.encode("utf-8"),
|
| 692 |
+
"OEBPS/text/back_cover.xhtml": back_xhtml.encode("utf-8"),
|
| 693 |
+
}
|
| 694 |
+
if (COVERS / "front_cover.jpg").exists():
|
| 695 |
+
files["OEBPS/images/cover.jpg"] = (COVERS / "front_cover.jpg").read_bytes()
|
| 696 |
+
if (COVERS / "back_cover.jpg").exists():
|
| 697 |
+
files["OEBPS/images/back_cover.jpg"] = (COVERS / "back_cover.jpg").read_bytes()
|
| 698 |
|
| 699 |
+
# Add chapters
|
| 700 |
+
for part in outline["parts"]:
|
| 701 |
+
for ch in part["chapters"]:
|
| 702 |
+
ch_num = int(ch["id"][2:])
|
| 703 |
+
md_path = CHAPTERS / f"{ch['id']}_{slugify_toc(ch['title'])}.md"
|
| 704 |
+
if md_path.exists():
|
| 705 |
+
body = _md_to_xhtml(md_path.read_text(encoding="utf-8"))
|
| 706 |
+
else:
|
| 707 |
+
body = f"<h1>{ch['title']}</h1><p>Content coming soon.</p>"
|
| 708 |
+
fname = f"OEBPS/text/ch{ch_num:02d}_{slugify_toc(ch['title'])}.xhtml"
|
| 709 |
+
xhtml = f"""<?xml version="1.0" encoding="UTF-8"?>
|
| 710 |
+
<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>
|
| 711 |
+
<title>{ch['title']}</title><link rel="stylesheet" href="../style/nav.css"/></head>
|
| 712 |
+
<body>{body}</body></html>"""
|
| 713 |
+
files[fname] = xhtml.encode("utf-8")
|
| 714 |
|
| 715 |
+
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 716 |
+
# mimetype must be first and uncompressed
|
| 717 |
+
zf.writestr("mimetype", files.pop("mimetype"), zipfile.ZIP_STORED)
|
| 718 |
+
for arcname, data in sorted(files.items()):
|
| 719 |
+
zf.writestr(arcname, data)
|
| 720 |
+
|
| 721 |
+
return str(out)
|
| 722 |
|
| 723 |
|
| 724 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|