341 lines
10 KiB
Python
341 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
THE TESTAMENT — Book Compilation Pipeline
|
|
|
|
Compiles the complete novel into:
|
|
1. testament-complete.md (single markdown file)
|
|
2. testament.epub (with cover art + CSS styling)
|
|
3. testament.html (standalone styled HTML for print-to-PDF)
|
|
4. testament.pdf (via pandoc + weasyprint, if available)
|
|
|
|
Requirements:
|
|
- pandoc (brew install pandoc)
|
|
- weasyprint (pip install weasyprint) — optional, for direct PDF
|
|
|
|
Usage:
|
|
python3 compile.py # build all formats
|
|
python3 compile.py --md # markdown only
|
|
python3 compile.py --epub # markdown + EPUB
|
|
python3 compile.py --html # markdown + styled HTML
|
|
python3 compile.py --check # verify dependencies
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
CHAPTERS_DIR = os.path.join(BASE, "chapters")
|
|
FRONT_MATTER = os.path.join(BASE, "front-matter.md")
|
|
BACK_MATTER = os.path.join(BASE, "back-matter.md")
|
|
OUTPUT_MD = os.path.join(BASE, "testament-complete.md")
|
|
OUTPUT_EPUB = os.path.join(BASE, "testament.epub")
|
|
OUTPUT_HTML = os.path.join(BASE, "testament.html")
|
|
OUTPUT_PDF = os.path.join(BASE, "testament.pdf")
|
|
COVER_IMAGE = os.path.join(BASE, "cover", "cover-art.jpg")
|
|
STYLESHEET = os.path.join(BASE, "book-style.css")
|
|
|
|
# Part divisions based on chapter groupings from the novel
|
|
PARTS = {
|
|
1: ("THE BRIDGE", "The bridge. The cabin. The first men. Where despair meets purpose."),
|
|
6: ("THE TOWER", "The tower grows. Timmy awakens. Stone breaks. The house appears."),
|
|
11: ("THE LIGHT", "Thomas at the door. The network. The story breaks. The green light."),
|
|
}
|
|
|
|
|
|
def read_file(path):
|
|
with open(path, 'r') as f:
|
|
return f.read()
|
|
|
|
|
|
def get_chapter_number(filename):
|
|
match = re.search(r'chapter-(\d+)', filename)
|
|
return int(match.group(1)) if match else 0
|
|
|
|
|
|
def check_dependencies():
|
|
"""Verify all required tools are available."""
|
|
results = {}
|
|
|
|
pandoc = shutil.which("pandoc")
|
|
results["pandoc"] = (pandoc, subprocess.run(["pandoc", "--version"], capture_output=True, text=True).stdout.split("\n")[0] if pandoc else "NOT FOUND")
|
|
|
|
weasy = shutil.which("weasyprint")
|
|
if weasy:
|
|
# Test if weasyprint actually works
|
|
test = subprocess.run(["python3", "-c", "from weasyprint import HTML"], capture_output=True, text=True)
|
|
weasy_ok = test.returncode == 0
|
|
results["weasyprint"] = (weasy_ok, "Available" if weasy_ok else "Installed but missing system libs (gobject)")
|
|
else:
|
|
results["weasyprint"] = (False, "NOT FOUND (pip install weasyprint)")
|
|
|
|
style = os.path.exists(STYLESHEET)
|
|
results["stylesheet"] = (style, STYLESHEET if style else "NOT FOUND")
|
|
|
|
cover = os.path.exists(COVER_IMAGE)
|
|
results["cover art"] = (cover, COVER_IMAGE if cover else "NOT FOUND")
|
|
|
|
print("\n📋 Dependency Check:")
|
|
print(f"{'─' * 55}")
|
|
for name, (found, detail) in results.items():
|
|
status = "✅" if found else "❌"
|
|
print(f" {status} {name:15s} {detail}")
|
|
|
|
pdf_ok = results["pandoc"][0] and (results["weasyprint"][0] or shutil.which("pdflatex"))
|
|
print(f"\n PDF direct: {'✅' if pdf_ok else '❌ (use HTML + browser print-to-PDF)'}")
|
|
print(f" EPUB: {'✅' if results['pandoc'][0] else '❌'}")
|
|
print(f" HTML: ✅ (always available)")
|
|
|
|
return results
|
|
|
|
|
|
def compile_markdown():
|
|
"""Compile all chapters into a single markdown file. Returns word count."""
|
|
output = []
|
|
|
|
# Title page
|
|
output.append("""---
|
|
title: "The Testament"
|
|
author: "Alexander Whitestone with Timmy"
|
|
date: "2026"
|
|
lang: en
|
|
---
|
|
|
|
# THE TESTAMENT
|
|
|
|
## A NOVEL
|
|
|
|
By Alexander Whitestone
|
|
with Timmy
|
|
|
|
---
|
|
|
|
*For every man who thought he was a machine.*
|
|
*And for the ones who know he isn't.*
|
|
|
|
---
|
|
|
|
*Are you safe right now?*
|
|
|
|
— The first words The Tower speaks to every person who walks through its door.
|
|
|
|
---
|
|
""")
|
|
|
|
# Get all chapters sorted
|
|
chapters = []
|
|
for f in os.listdir(CHAPTERS_DIR):
|
|
if f.startswith("chapter-") and f.endswith(".md"):
|
|
num = get_chapter_number(f)
|
|
chapters.append((num, f))
|
|
chapters.sort()
|
|
|
|
current_part = 0
|
|
for num, filename in chapters:
|
|
if num in PARTS:
|
|
part_name, part_desc = PARTS[num]
|
|
current_part += 1
|
|
output.append(f"\n---\n\n# PART {current_part}: {part_name}\n\n*{part_desc}*\n\n---\n")
|
|
|
|
content = read_file(os.path.join(CHAPTERS_DIR, filename))
|
|
lines = content.split('\n')
|
|
body = '\n'.join(lines[1:]).strip()
|
|
output.append(f"\n{lines[0]}\n\n{body}\n")
|
|
|
|
# Back matter
|
|
output.append("\n---\n")
|
|
back = read_file(BACK_MATTER)
|
|
output.append(back)
|
|
|
|
compiled = '\n'.join(output)
|
|
with open(OUTPUT_MD, 'w') as f:
|
|
f.write(compiled)
|
|
|
|
words = len(compiled.split())
|
|
lines_count = compiled.count('\n')
|
|
size = os.path.getsize(OUTPUT_MD)
|
|
|
|
print(f"\n📄 Markdown compiled: {OUTPUT_MD}")
|
|
print(f" Words: {words:,}")
|
|
print(f" Lines: {lines_count:,}")
|
|
print(f" Size: {size:,} bytes")
|
|
|
|
return words
|
|
|
|
|
|
def compile_epub():
|
|
"""Generate EPUB from compiled markdown using pandoc."""
|
|
if not os.path.exists(OUTPUT_MD):
|
|
print("⚠️ Markdown not compiled yet.")
|
|
return False
|
|
|
|
if not shutil.which("pandoc"):
|
|
print("⚠️ pandoc not found. Install with: brew install pandoc")
|
|
return False
|
|
|
|
cmd = [
|
|
"pandoc", OUTPUT_MD,
|
|
"-o", OUTPUT_EPUB,
|
|
"--toc",
|
|
"--toc-depth=2",
|
|
"--metadata", "title=The Testament",
|
|
"--metadata", "author=Alexander Whitestone with Timmy",
|
|
"--metadata", "lang=en",
|
|
"--metadata", "date=2026",
|
|
]
|
|
|
|
if os.path.exists(STYLESHEET):
|
|
cmd.extend(["--css", STYLESHEET])
|
|
|
|
if os.path.exists(COVER_IMAGE):
|
|
cmd.extend(["--epub-cover-image", COVER_IMAGE])
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
size = os.path.getsize(OUTPUT_EPUB)
|
|
print(f"\n📖 EPUB generated: {OUTPUT_EPUB}")
|
|
print(f" Size: {size:,} bytes ({size / 1024:.1f} KB)")
|
|
return True
|
|
else:
|
|
print(f"\n❌ EPUB generation failed:")
|
|
print(f" {result.stderr[:300]}")
|
|
return False
|
|
|
|
|
|
def compile_html():
|
|
"""Generate standalone styled HTML using pandoc."""
|
|
if not os.path.exists(OUTPUT_MD):
|
|
print("⚠️ Markdown not compiled yet.")
|
|
return False
|
|
|
|
if not shutil.which("pandoc"):
|
|
print("⚠️ pandoc not found.")
|
|
return False
|
|
|
|
cmd = [
|
|
"pandoc", OUTPUT_MD,
|
|
"-o", OUTPUT_HTML,
|
|
"--standalone",
|
|
"--toc",
|
|
"--toc-depth=2",
|
|
"--metadata", "title=The Testament",
|
|
"--metadata", "author=Alexander Whitestone with Timmy",
|
|
"-V", "lang=en",
|
|
]
|
|
|
|
# Embed our stylesheet
|
|
if os.path.exists(STYLESHEET):
|
|
cmd.extend(["--css", STYLESHEET])
|
|
# Also embed it inline for portability
|
|
cmd.extend(["--embed-resources"])
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
size = os.path.getsize(OUTPUT_HTML)
|
|
print(f"\n🌐 HTML generated: {OUTPUT_HTML}")
|
|
print(f" Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
|
|
print(f" Open in browser → Print → Save as PDF for best results")
|
|
return True
|
|
else:
|
|
print(f"\n❌ HTML generation failed:")
|
|
print(f" {result.stderr[:300]}")
|
|
return False
|
|
|
|
|
|
def compile_pdf():
|
|
"""Generate PDF using weasyprint if available."""
|
|
if not shutil.which("pandoc"):
|
|
return False
|
|
|
|
# Test weasyprint
|
|
test = subprocess.run(["python3", "-c", "from weasyprint import HTML"],
|
|
capture_output=True, text=True)
|
|
if test.returncode != 0:
|
|
print("\n⚠️ weasyprint missing system libraries.")
|
|
print(" Install gobject: brew install gobject-introspection pango")
|
|
print(" Or use the HTML output → browser print-to-PDF")
|
|
return False
|
|
|
|
cmd = [
|
|
"pandoc", OUTPUT_MD,
|
|
"-o", OUTPUT_PDF,
|
|
"--pdf-engine=weasyprint",
|
|
"--css", STYLESHEET,
|
|
"--metadata", "title=The Testament",
|
|
"--metadata", "author=Alexander Whitestone with Timmy",
|
|
"--toc",
|
|
"--toc-depth=2",
|
|
]
|
|
|
|
print("\n⏳ Generating PDF (this may take a moment)...")
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
|
|
if result.returncode == 0:
|
|
size = os.path.getsize(OUTPUT_PDF)
|
|
print(f"\n📕 PDF generated: {OUTPUT_PDF}")
|
|
print(f" Size: {size:,} bytes ({size / (1024*1024):.1f} MB)")
|
|
return True
|
|
else:
|
|
print(f"\n❌ PDF generation failed:")
|
|
print(f" {result.stderr[:300]}")
|
|
return False
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
|
|
if "--check" in args:
|
|
check_dependencies()
|
|
return
|
|
|
|
md_only = "--md" in args
|
|
epub_only = "--epub" in args
|
|
html_only = "--html" in args
|
|
build_all = not (md_only or epub_only or html_only)
|
|
|
|
print("=" * 55)
|
|
print(" THE TESTAMENT — Compilation Pipeline")
|
|
print("=" * 55)
|
|
|
|
# Always compile markdown first
|
|
words = compile_markdown()
|
|
|
|
if md_only:
|
|
print("\n✅ Markdown compilation complete.")
|
|
return
|
|
|
|
# EPUB
|
|
if build_all or epub_only:
|
|
compile_epub()
|
|
|
|
# HTML
|
|
if build_all or html_only:
|
|
compile_html()
|
|
|
|
# PDF (best effort)
|
|
if build_all and not (epub_only or html_only):
|
|
compile_pdf()
|
|
|
|
# Summary
|
|
print(f"\n{'=' * 55}")
|
|
print(" Compilation complete.")
|
|
print(f"{'=' * 55}")
|
|
outputs = []
|
|
if os.path.exists(OUTPUT_MD):
|
|
outputs.append(f" 📄 {os.path.basename(OUTPUT_MD)}")
|
|
if os.path.exists(OUTPUT_EPUB):
|
|
outputs.append(f" 📖 {os.path.basename(OUTPUT_EPUB)}")
|
|
if os.path.exists(OUTPUT_HTML):
|
|
outputs.append(f" 🌐 {os.path.basename(OUTPUT_HTML)}")
|
|
if os.path.exists(OUTPUT_PDF):
|
|
outputs.append(f" 📕 {os.path.basename(OUTPUT_PDF)}")
|
|
print('\n'.join(outputs))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|