- build/build.py: Clean compilation script (md, epub, pdf via xelatex) - build/metadata.yaml: Pandoc metadata (fonts, page size, formatting) - build/frontmatter.md: Enhanced front matter with chapter guide table - build/backmatter.md: Acknowledgments, sovereignty note, author bio - Makefile: make all/pdf/epub/md/clean targets - Updated .gitignore for build artifacts Verified: markdown (19,490 words) and EPUB (213 KB) build successfully. Closes #18
189 lines
5.1 KiB
Python
189 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
THE TESTAMENT — Build System
|
|
|
|
Compiles the complete novel into distributable formats:
|
|
1. Combined markdown (testament-complete.md)
|
|
2. EPUB (the-testament.epub)
|
|
3. PDF via xelatex (the-testament.pdf)
|
|
|
|
Usage:
|
|
python3 build/build.py # all formats
|
|
python3 build/build.py --md # markdown only
|
|
python3 build/build.py --epub # EPUB only
|
|
python3 build/build.py --pdf # PDF only (requires xelatex)
|
|
|
|
Requirements:
|
|
- pandoc (brew install pandoc)
|
|
- xelatex (install MacTeX or TinyTeX) — for PDF
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
# Paths relative to repo root
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
BUILD = REPO / "build"
|
|
OUTPUT_DIR = BUILD / "output"
|
|
CHAPTERS_DIR = REPO / "chapters"
|
|
FRONT_MATTER = BUILD / "frontmatter.md"
|
|
BACK_MATTER = BUILD / "backmatter.md"
|
|
METADATA = BUILD / "metadata.yaml"
|
|
STYLESHEET = REPO / "book-style.css"
|
|
COVER_IMAGE = REPO / "cover" / "cover-art.jpg"
|
|
|
|
# Output files
|
|
OUT_MD = REPO / "testament-complete.md"
|
|
OUT_EPUB = OUTPUT_DIR / "the-testament.epub"
|
|
OUT_PDF = OUTPUT_DIR / "the-testament.pdf"
|
|
|
|
# Part divisions
|
|
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 get_chapter_num(filename):
|
|
m = re.search(r'chapter-(\d+)', filename)
|
|
return int(m.group(1)) if m else 0
|
|
|
|
|
|
def compile_markdown():
|
|
"""Combine front matter + 18 chapters + back matter into one markdown file."""
|
|
parts = []
|
|
|
|
# Front matter
|
|
parts.append(FRONT_MATTER.read_text())
|
|
|
|
# Chapters
|
|
chapters = sorted(
|
|
[(get_chapter_num(f), f) for f in os.listdir(CHAPTERS_DIR)
|
|
if f.startswith("chapter-") and f.endswith(".md")]
|
|
)
|
|
|
|
current_part = 0
|
|
for num, filename in chapters:
|
|
if num in PARTS:
|
|
current_part += 1
|
|
name, desc = PARTS[num]
|
|
parts.append(f"\n---\n\n# PART {current_part}: {name}\n\n*{desc}*\n\n---\n")
|
|
|
|
content = (CHAPTERS_DIR / filename).read_text()
|
|
lines = content.split('\n')
|
|
body = '\n'.join(lines[1:]).strip()
|
|
parts.append(f"\n{lines[0]}\n\n{body}\n")
|
|
|
|
# Back matter
|
|
parts.append("\n---\n")
|
|
parts.append(BACK_MATTER.read_text())
|
|
|
|
compiled = '\n'.join(parts)
|
|
OUT_MD.write_text(compiled)
|
|
|
|
words = len(compiled.split())
|
|
size = OUT_MD.stat().st_size
|
|
print(f" Markdown: {OUT_MD.name} ({words:,} words, {size:,} bytes)")
|
|
return True
|
|
|
|
|
|
def compile_epub():
|
|
"""Generate EPUB via pandoc."""
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
cmd = [
|
|
"pandoc", str(OUT_MD),
|
|
"-o", str(OUT_EPUB),
|
|
"--toc", "--toc-depth=2",
|
|
"--metadata", "title=The Testament",
|
|
"--metadata", "author=Alexander Whitestone with Timmy",
|
|
"--metadata", "lang=en",
|
|
"--metadata", "date=2026",
|
|
]
|
|
|
|
if METADATA.exists():
|
|
cmd.extend(["--metadata-file", str(METADATA)])
|
|
if STYLESHEET.exists():
|
|
cmd.extend(["--css", str(STYLESHEET)])
|
|
if COVER_IMAGE.exists():
|
|
cmd.extend(["--epub-cover-image", str(COVER_IMAGE)])
|
|
|
|
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
if r.returncode == 0:
|
|
size = OUT_EPUB.stat().st_size
|
|
print(f" EPUB: {OUT_EPUB.name} ({size:,} bytes, {size/1024:.0f} KB)")
|
|
return True
|
|
else:
|
|
print(f" EPUB FAILED: {r.stderr[:200]}")
|
|
return False
|
|
|
|
|
|
def compile_pdf():
|
|
"""Generate PDF via pandoc + xelatex."""
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
if not shutil.which("xelatex"):
|
|
print(" PDF SKIPPED: xelatex not found (install MacTeX)")
|
|
return False
|
|
|
|
cmd = [
|
|
"pandoc", str(OUT_MD),
|
|
"-o", str(OUT_PDF),
|
|
"--pdf-engine=xelatex",
|
|
"--toc", "--toc-depth=2",
|
|
]
|
|
|
|
if METADATA.exists():
|
|
cmd.extend(["--metadata-file", str(METADATA)])
|
|
|
|
print(" Building PDF (xelatex)... this takes a minute")
|
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
if r.returncode == 0:
|
|
size = OUT_PDF.stat().st_size
|
|
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
|
|
return True
|
|
else:
|
|
print(f" PDF FAILED: {r.stderr[:300]}")
|
|
return False
|
|
|
|
|
|
def main():
|
|
args = sys.argv[1:]
|
|
do_all = not any(a.startswith("--") and a != "--check" for a in args)
|
|
do_md = "--md" in args or do_all
|
|
do_epub = "--epub" in args or do_all
|
|
do_pdf = "--pdf" in args or do_all
|
|
|
|
print("=" * 50)
|
|
print(" THE TESTAMENT — Build System")
|
|
print("=" * 50)
|
|
|
|
# Step 1: Always compile markdown first
|
|
if do_md or do_epub or do_pdf:
|
|
compile_markdown()
|
|
|
|
# Step 2: EPUB
|
|
if do_epub:
|
|
compile_epub()
|
|
|
|
# Step 3: PDF
|
|
if do_pdf:
|
|
compile_pdf()
|
|
|
|
print("=" * 50)
|
|
print(" Build complete.")
|
|
print("=" * 50)
|
|
|
|
for f in [OUT_MD, OUT_EPUB, OUT_PDF]:
|
|
if f.exists():
|
|
print(f" ✓ {f.relative_to(REPO)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|