Lines 288-290 had literal \n characters instead of actual newlines, causing the main() function to have no body. Fixed formatting and removed duplicate args assignment.
344 lines
10 KiB
Python
344 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():
|
|
print("Generating index...")
|
|
os.system("python3 scripts/index_generator.py")
|
|
|
|
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()
|