Compare commits
1 Commits
main
...
burn/20260
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dea06a0995 |
50
build/manifest.json
Normal file
50
build/manifest.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"project": "The Testament",
|
||||
"author": "Alexander Whitestone with Timmy",
|
||||
"version": "1.0.0",
|
||||
"built_at": "2026-04-11T18:18:09.197952+00:00",
|
||||
"formats": {
|
||||
"markdown": {
|
||||
"path": "/private/tmp/timmy-burn-1775931309/the-testament/testament-complete.md",
|
||||
"words": 19490,
|
||||
"lines": 2229,
|
||||
"bytes": 112408
|
||||
},
|
||||
"epub": {
|
||||
"path": "/private/tmp/timmy-burn-1775931309/the-testament/build/output/the-testament.epub",
|
||||
"bytes": 69460
|
||||
},
|
||||
"pdf": {
|
||||
"path": "/private/tmp/timmy-burn-1775931309/the-testament/build/output/the-testament.pdf",
|
||||
"bytes": 143222
|
||||
},
|
||||
"html": {
|
||||
"path": "/private/tmp/timmy-burn-1775931309/the-testament/testament.html",
|
||||
"bytes": 3866869
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"testament-complete.md": {
|
||||
"path": "testament-complete.md",
|
||||
"bytes": 112408,
|
||||
"sha256": "84a0fb4fe9575ab78c839386f31f058881455d0b95e58cf1004ad619e695ba64"
|
||||
},
|
||||
"the-testament.epub": {
|
||||
"path": "build/output/the-testament.epub",
|
||||
"bytes": 69460,
|
||||
"sha256": "7d7f497fb1e36a5c35d7beb61613f89cf6eee50bf175d9f7146e6b1943c27033"
|
||||
},
|
||||
"the-testament.pdf": {
|
||||
"path": "build/output/the-testament.pdf",
|
||||
"bytes": 143222,
|
||||
"sha256": "c2a41f70667a7f53f49f6a5e934d3d7860030e901a323bebbfc583e5e48db53a"
|
||||
},
|
||||
"testament.html": {
|
||||
"path": "testament.html",
|
||||
"bytes": 3866869,
|
||||
"sha256": "e68285f808f8cdb5c785e9295b2a5b689b34d40d252f85dec07c6a5701de63e8"
|
||||
}
|
||||
},
|
||||
"chapters": 18,
|
||||
"words": 19490
|
||||
}
|
||||
Binary file not shown.
582
compile_all.py
Normal file
582
compile_all.py
Normal file
@@ -0,0 +1,582 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
THE TESTAMENT — Final Compilation Pipeline
|
||||
|
||||
Produces all distributable formats:
|
||||
1. testament-complete.md — Full novel in one markdown file
|
||||
2. the-testament.epub — EPUB with cover art + CSS
|
||||
3. the-testament.pdf — PDF via reportlab (pure Python)
|
||||
4. testament.html — Standalone styled HTML for web/print
|
||||
5. manifest.json — Build manifest with checksums + metadata
|
||||
6. website/chapters.json — Chapter index for the web reader
|
||||
|
||||
Usage:
|
||||
python3 compile_all.py # build everything
|
||||
python3 compile_all.py --md # markdown only
|
||||
python3 compile_all.py --epub # markdown + EPUB
|
||||
python3 compile_all.py --pdf # markdown + PDF
|
||||
python3 compile_all.py --html # markdown + standalone HTML
|
||||
python3 compile_all.py --manifest # regenerate manifest only
|
||||
python3 compile_all.py --check # verify dependencies
|
||||
|
||||
Requirements:
|
||||
- pandoc (brew install pandoc)
|
||||
- reportlab (pip install reportlab) — for PDF
|
||||
- qrcode (pip install qrcode) — for QR codes in PDF
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# ── Paths ──────────────────────────────────────────────────────────────
|
||||
REPO = Path(__file__).resolve().parent
|
||||
CHAPTERS_DIR = REPO / "chapters"
|
||||
BUILD_DIR = REPO / "build"
|
||||
OUTPUT_DIR = BUILD_DIR / "output"
|
||||
WEBSITE_DIR = REPO / "website"
|
||||
|
||||
FRONT_MATTER = BUILD_DIR / "frontmatter.md"
|
||||
BACK_MATTER = BUILD_DIR / "backmatter.md"
|
||||
METADATA_YAML = BUILD_DIR / "metadata.yaml"
|
||||
STYLESHEET = REPO / "book-style.css"
|
||||
COVER_IMAGE = REPO / "cover" / "cover-art.jpg"
|
||||
|
||||
OUT_MD = REPO / "testament-complete.md"
|
||||
OUT_EPUB = OUTPUT_DIR / "the-testament.epub"
|
||||
OUT_PDF = OUTPUT_DIR / "the-testament.pdf"
|
||||
OUT_HTML = REPO / "testament.html"
|
||||
OUT_MANIFEST = BUILD_DIR / "manifest.json"
|
||||
CHAPTERS_JSON = WEBSITE_DIR / "chapters.json"
|
||||
|
||||
# ── Part structure ─────────────────────────────────────────────────────
|
||||
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."),
|
||||
}
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
def get_chapter_num(filename: str) -> int:
|
||||
m = re.search(r"chapter-(\d+)", filename)
|
||||
return int(m.group(1)) if m else 0
|
||||
|
||||
|
||||
def sha256_of(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def read_file(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def get_chapters() -> list[tuple[int, str]]:
|
||||
"""Return sorted list of (chapter_num, filename)."""
|
||||
chapters = []
|
||||
for f in os.listdir(CHAPTERS_DIR):
|
||||
if f.startswith("chapter-") and f.endswith(".md"):
|
||||
chapters.append((get_chapter_num(f), f))
|
||||
chapters.sort()
|
||||
return chapters
|
||||
|
||||
|
||||
def check_dependencies() -> dict:
|
||||
"""Check and report all build dependencies."""
|
||||
results = {}
|
||||
|
||||
pandoc = shutil.which("pandoc")
|
||||
pandoc_ver = ""
|
||||
if pandoc:
|
||||
r = subprocess.run(["pandoc", "--version"], capture_output=True, text=True)
|
||||
pandoc_ver = r.stdout.split("\n")[0]
|
||||
results["pandoc"] = (bool(pandoc), pandoc_ver or "NOT FOUND")
|
||||
|
||||
# reportlab
|
||||
try:
|
||||
import reportlab
|
||||
results["reportlab"] = (True, f"v{reportlab.Version}")
|
||||
except ImportError:
|
||||
results["reportlab"] = (False, "NOT FOUND (pip install reportlab)")
|
||||
|
||||
# qrcode
|
||||
try:
|
||||
import qrcode
|
||||
results["qrcode"] = (True, "Available")
|
||||
except ImportError:
|
||||
results["qrcode"] = (False, "NOT FOUND (pip install qrcode)")
|
||||
|
||||
# weasyprint
|
||||
try:
|
||||
from weasyprint import HTML as _HTML
|
||||
results["weasyprint"] = (True, "Available")
|
||||
except Exception:
|
||||
results["weasyprint"] = (False, "Missing system libs (fallback to reportlab)")
|
||||
|
||||
# cover art
|
||||
results["cover art"] = (COVER_IMAGE.exists(), str(COVER_IMAGE) if COVER_IMAGE.exists() else "NOT FOUND")
|
||||
|
||||
# stylesheet
|
||||
results["stylesheet"] = (STYLESHEET.exists(), str(STYLESHEET) if STYLESHEET.exists() else "NOT FOUND")
|
||||
|
||||
print("\n📋 Build Dependencies:")
|
||||
print(f"{'─' * 60}")
|
||||
for name, (found, detail) in results.items():
|
||||
icon = "✅" if found else "❌"
|
||||
print(f" {icon} {name:15s} {detail}")
|
||||
|
||||
pdf_engine = results["reportlab"][0] or results["weasyprint"][0]
|
||||
print(f"\n PDF engine: {'✅' if pdf_engine else '❌'}")
|
||||
print(f" EPUB engine: {'✅' if results['pandoc'][0] else '❌'}")
|
||||
print(f" Web version: ✅ (HTML is always available)")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ── Step 1: Markdown Compilation ───────────────────────────────────────
|
||||
|
||||
def compile_markdown() -> dict:
|
||||
"""Compile all chapters into a single markdown file. Returns stats."""
|
||||
parts = []
|
||||
|
||||
# Front matter
|
||||
if FRONT_MATTER.exists():
|
||||
parts.append(read_file(FRONT_MATTER))
|
||||
else:
|
||||
# Inline fallback
|
||||
parts.append("""# THE TESTAMENT\n\n## A NOVEL\n\nBy Alexander Whitestone\nwith Timmy\n\n---\n\n*For every man who thought he was a machine.*\n*And for the ones who know he isn't.*\n\n---""")
|
||||
|
||||
# Chapters with part dividers
|
||||
chapters = get_chapters()
|
||||
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 = read_file(CHAPTERS_DIR / filename)
|
||||
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")
|
||||
if BACK_MATTER.exists():
|
||||
parts.append(read_file(BACK_MATTER))
|
||||
|
||||
compiled = "\n".join(parts)
|
||||
OUT_MD.write_text(compiled, encoding="utf-8")
|
||||
|
||||
words = len(compiled.split())
|
||||
lines_count = compiled.count("\n")
|
||||
size = OUT_MD.stat().st_size
|
||||
|
||||
print(f"\n📄 Markdown compiled: {OUT_MD.relative_to(REPO)}")
|
||||
print(f" {words:,} words | {lines_count:,} lines | {size:,} bytes")
|
||||
|
||||
return {"path": str(OUT_MD), "words": words, "lines": lines_count, "bytes": size}
|
||||
|
||||
|
||||
# ── Step 2: EPUB ──────────────────────────────────────────────────────
|
||||
|
||||
def compile_epub() -> dict | None:
|
||||
"""Generate EPUB via pandoc."""
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not shutil.which("pandoc"):
|
||||
print(" ⚠️ pandoc not found — EPUB skipped")
|
||||
return None
|
||||
|
||||
cmd = [
|
||||
"pandoc", str(OUT_MD),
|
||||
"-o", str(OUT_EPUB),
|
||||
"--toc", "--toc-depth=2",
|
||||
"--metadata", "title=The Testament",
|
||||
"--metadata", "subtitle=A Novel",
|
||||
"--metadata", "author=Alexander Whitestone with Timmy",
|
||||
"--metadata", "lang=en",
|
||||
"--metadata", "date=2026",
|
||||
"--metadata", "publisher=Timmy Foundation",
|
||||
]
|
||||
|
||||
if METADATA_YAML.exists():
|
||||
cmd.extend(["--metadata-file", str(METADATA_YAML)])
|
||||
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, timeout=60)
|
||||
if r.returncode == 0:
|
||||
size = OUT_EPUB.stat().st_size
|
||||
print(f" 📖 EPUB: {OUT_EPUB.name} ({size:,} bytes, {size/1024:.0f} KB)")
|
||||
return {"path": str(OUT_EPUB), "bytes": size}
|
||||
else:
|
||||
print(f" ❌ EPUB FAILED: {r.stderr[:200]}")
|
||||
return None
|
||||
|
||||
|
||||
# ── Step 3: PDF ───────────────────────────────────────────────────────
|
||||
|
||||
def compile_pdf() -> dict | None:
|
||||
"""Generate PDF using reportlab (pure Python, no system deps)."""
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, PageBreak,
|
||||
Image as RLImage, Table, TableStyle, HRFlowable
|
||||
)
|
||||
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT
|
||||
except ImportError:
|
||||
print(" ⚠️ reportlab not found — PDF skipped (pip install reportlab)")
|
||||
return None
|
||||
|
||||
try:
|
||||
import qrcode
|
||||
HAS_QRCODE = True
|
||||
except ImportError:
|
||||
HAS_QRCODE = False
|
||||
|
||||
print(" 📕 Building PDF (reportlab)...")
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
_add_style = styles.add
|
||||
|
||||
_add_style(ParagraphStyle(
|
||||
"BookTitle", parent=styles["Title"],
|
||||
fontSize=28, leading=34, spaceAfter=20,
|
||||
textColor=HexColor("#1a1a2e"), alignment=TA_CENTER,
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"BookAuthor", parent=styles["Normal"],
|
||||
fontSize=14, leading=18, spaceAfter=40,
|
||||
textColor=HexColor("#555555"), alignment=TA_CENTER,
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"PartTitle", parent=styles["Heading1"],
|
||||
fontSize=22, leading=28, spaceBefore=40, spaceAfter=12,
|
||||
textColor=HexColor("#16213e"), alignment=TA_CENTER,
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"PartDesc", parent=styles["Normal"],
|
||||
fontSize=11, leading=15, spaceAfter=30,
|
||||
textColor=HexColor("#666666"), alignment=TA_CENTER, italics=1,
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"ChapterTitle", parent=styles["Heading1"],
|
||||
fontSize=20, leading=26, spaceBefore=30, spaceAfter=16,
|
||||
textColor=HexColor("#1a1a2e"), alignment=TA_CENTER,
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"BodyText2", parent=styles["Normal"],
|
||||
fontSize=11, leading=16, spaceAfter=8,
|
||||
alignment=TA_JUSTIFY, firstLineIndent=24,
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"SectionBreak", parent=styles["Normal"],
|
||||
fontSize=14, leading=18, spaceBefore=20, spaceAfter=20,
|
||||
alignment=TA_CENTER, textColor=HexColor("#999999"),
|
||||
))
|
||||
_add_style(ParagraphStyle(
|
||||
"Footer", parent=styles["Normal"],
|
||||
fontSize=9, textColor=HexColor("#888888"), alignment=TA_CENTER,
|
||||
))
|
||||
|
||||
def _esc(text: str) -> str:
|
||||
return text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
def _md2rml(text: str) -> str:
|
||||
text = _esc(text)
|
||||
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
|
||||
text = re.sub(r"\*(.+?)\*", r"<i>\1</i>", text)
|
||||
return text
|
||||
|
||||
def _make_qr(data: str, size: int = 72):
|
||||
if not HAS_QRCODE:
|
||||
return None
|
||||
qr = qrcode.QRCode(version=1, box_size=4, border=1)
|
||||
qr.add_data(data)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buf = __import__("io").BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
buf.seek(0)
|
||||
return RLImage(buf, width=size, height=size)
|
||||
|
||||
def _parse_md(md_text: str) -> list:
|
||||
flowables = []
|
||||
lines = md_text.split("\n")
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
s = lines[i].strip()
|
||||
if s in ("---", "***", "___"):
|
||||
flowables.append(HRFlowable(
|
||||
width="60%", thickness=1, spaceAfter=20, spaceBefore=20,
|
||||
color=HexColor("#cccccc"),
|
||||
))
|
||||
i += 1
|
||||
continue
|
||||
if s.startswith("# ") and not s.startswith("## "):
|
||||
text = s[2:].strip()
|
||||
if text.upper().startswith("PART "):
|
||||
flowables.append(PageBreak())
|
||||
flowables.append(Paragraph(text, styles["PartTitle"]))
|
||||
elif text.upper().startswith("CHAPTER "):
|
||||
flowables.append(PageBreak())
|
||||
flowables.append(Paragraph(text, styles["ChapterTitle"]))
|
||||
elif "THE TESTAMENT" in text.upper():
|
||||
flowables.append(Spacer(1, 2 * inch))
|
||||
flowables.append(Paragraph(text, styles["BookTitle"]))
|
||||
else:
|
||||
flowables.append(Spacer(1, 0.3 * inch))
|
||||
flowables.append(Paragraph(text, styles["Heading1"]))
|
||||
i += 1
|
||||
continue
|
||||
if s.startswith("## "):
|
||||
flowables.append(Spacer(1, 0.2 * inch))
|
||||
flowables.append(Paragraph(s[3:].strip(), styles["Heading2"]))
|
||||
i += 1
|
||||
continue
|
||||
if s.startswith("*") and s.endswith("*") and len(s) > 2:
|
||||
flowables.append(Paragraph(
|
||||
f'<i>{_esc(s.strip("*").strip())}</i>', styles["PartDesc"]
|
||||
))
|
||||
i += 1
|
||||
continue
|
||||
if not s:
|
||||
i += 1
|
||||
continue
|
||||
flowables.append(Paragraph(_md2rml(s), styles["BodyText2"]))
|
||||
i += 1
|
||||
return flowables
|
||||
|
||||
# Build document
|
||||
doc = SimpleDocTemplate(
|
||||
str(OUT_PDF), pagesize=letter,
|
||||
leftMargin=1.0 * inch, rightMargin=1.0 * inch,
|
||||
topMargin=0.8 * inch, bottomMargin=0.8 * inch,
|
||||
title="The Testament",
|
||||
author="Alexander Whitestone with Timmy",
|
||||
)
|
||||
|
||||
if not OUT_MD.exists():
|
||||
compile_markdown()
|
||||
|
||||
story = _parse_md(read_file(OUT_MD))
|
||||
|
||||
# QR codes page
|
||||
qr_links = {
|
||||
"Read Online": "https://timmyfoundation.org/the-testament",
|
||||
"The Door (Game)": "https://timmyfoundation.org/the-door",
|
||||
"Soundtrack": "https://timmyfoundation.org/soundtrack",
|
||||
"Source Code": "https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament",
|
||||
}
|
||||
if HAS_QRCODE:
|
||||
story.append(PageBreak())
|
||||
story.append(Paragraph("Experience More", styles["PartTitle"]))
|
||||
story.append(Spacer(1, 0.3 * inch))
|
||||
qr_items = []
|
||||
for label, url in qr_links.items():
|
||||
img = _make_qr(url)
|
||||
if img:
|
||||
cell = [img, Spacer(1, 6),
|
||||
Paragraph(f"<b>{label}</b>", styles["Footer"])]
|
||||
qr_items.append(cell)
|
||||
if qr_items:
|
||||
rows = []
|
||||
for j in range(0, len(qr_items), 2):
|
||||
row = qr_items[j:j + 2]
|
||||
if len(row) == 1:
|
||||
row.append("")
|
||||
rows.append(row)
|
||||
t = Table(rows, colWidths=[2.5 * inch, 2.5 * inch])
|
||||
t.setStyle(TableStyle([
|
||||
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 12),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 12),
|
||||
]))
|
||||
story.append(t)
|
||||
|
||||
try:
|
||||
doc.build(story)
|
||||
size = OUT_PDF.stat().st_size
|
||||
print(f" 📕 PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
|
||||
return {"path": str(OUT_PDF), "bytes": size}
|
||||
except Exception as e:
|
||||
print(f" ❌ PDF FAILED: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# ── Step 4: HTML ──────────────────────────────────────────────────────
|
||||
|
||||
def compile_html() -> dict | None:
|
||||
"""Generate standalone HTML via pandoc."""
|
||||
if not shutil.which("pandoc"):
|
||||
print(" ⚠️ pandoc not found — HTML skipped")
|
||||
return None
|
||||
|
||||
cmd = [
|
||||
"pandoc", str(OUT_MD),
|
||||
"-o", str(OUT_HTML),
|
||||
"--standalone",
|
||||
"--toc", "--toc-depth=2",
|
||||
"--css", "book-style.css",
|
||||
"--metadata", "title=The Testament",
|
||||
"--metadata", "author=Alexander Whitestone with Timmy",
|
||||
"--variable", "pagetitle=The Testament",
|
||||
]
|
||||
if METADATA_YAML.exists():
|
||||
cmd.extend(["--metadata-file", str(METADATA_YAML)])
|
||||
|
||||
# Embed resources for portability
|
||||
if STYLESHEET.exists():
|
||||
cmd.append("--embed-resources")
|
||||
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
if r.returncode == 0:
|
||||
size = OUT_HTML.stat().st_size
|
||||
print(f" 🌐 HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB)")
|
||||
return {"path": str(OUT_HTML), "bytes": size}
|
||||
else:
|
||||
print(f" ❌ HTML FAILED: {r.stderr[:200]}")
|
||||
return None
|
||||
|
||||
|
||||
# ── Step 5: Chapters JSON ─────────────────────────────────────────────
|
||||
|
||||
def generate_chapters_json() -> None:
|
||||
"""Generate chapters.json for the web reader."""
|
||||
chapters = get_chapters()
|
||||
data = []
|
||||
for num, filename in chapters:
|
||||
content = read_file(CHAPTERS_DIR / filename)
|
||||
lines = content.split("\n")
|
||||
title = lines[0].strip("# ").strip() if lines else f"Chapter {num}"
|
||||
data.append({
|
||||
"number": num,
|
||||
"title": title,
|
||||
"file": filename,
|
||||
"word_count": len(content.split()),
|
||||
})
|
||||
CHAPTERS_JSON.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
print(f" 📋 chapters.json ({len(data)} chapters)")
|
||||
|
||||
|
||||
# ── Step 6: Build Manifest ────────────────────────────────────────────
|
||||
|
||||
def generate_manifest(results: dict) -> None:
|
||||
"""Write manifest.json with build metadata and file checksums."""
|
||||
files = {}
|
||||
for key in ("markdown", "epub", "pdf", "html"):
|
||||
r = results.get(key)
|
||||
if r and os.path.exists(r["path"]):
|
||||
p = Path(r["path"])
|
||||
files[p.name] = {
|
||||
"path": str(p.relative_to(REPO)),
|
||||
"bytes": p.stat().st_size,
|
||||
"sha256": sha256_of(p),
|
||||
}
|
||||
|
||||
# Add testament-complete.md
|
||||
if OUT_MD.exists() and OUT_MD.name not in files:
|
||||
files[OUT_MD.name] = {
|
||||
"path": str(OUT_MD.relative_to(REPO)),
|
||||
"bytes": OUT_MD.stat().st_size,
|
||||
"sha256": sha256_of(OUT_MD),
|
||||
}
|
||||
|
||||
manifest = {
|
||||
"project": "The Testament",
|
||||
"author": "Alexander Whitestone with Timmy",
|
||||
"version": "1.0.0",
|
||||
"built_at": datetime.now(timezone.utc).isoformat(),
|
||||
"formats": results,
|
||||
"files": files,
|
||||
"chapters": len(get_chapters()),
|
||||
"words": results.get("markdown", {}).get("words", 0),
|
||||
}
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
OUT_MANIFEST.write_text(json.dumps(manifest, indent=2, ensure_ascii=False))
|
||||
print(f" 📋 manifest.json")
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
if "--check" in args:
|
||||
check_dependencies()
|
||||
return
|
||||
|
||||
manifest_only = "--manifest" in args
|
||||
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
|
||||
do_html = "--html" in args or do_all
|
||||
|
||||
print("=" * 60)
|
||||
print(" THE TESTAMENT — Final Compilation Pipeline")
|
||||
print("=" * 60)
|
||||
|
||||
t0 = time.time()
|
||||
results = {}
|
||||
|
||||
if do_md or do_epub or do_pdf or do_html:
|
||||
results["markdown"] = compile_markdown()
|
||||
|
||||
if do_epub:
|
||||
epub_r = compile_epub()
|
||||
if epub_r:
|
||||
results["epub"] = epub_r
|
||||
|
||||
if do_pdf:
|
||||
pdf_r = compile_pdf()
|
||||
if pdf_r:
|
||||
results["pdf"] = pdf_r
|
||||
|
||||
if do_html:
|
||||
html_r = compile_html()
|
||||
if html_r:
|
||||
results["html"] = html_r
|
||||
|
||||
if do_all or manifest_only:
|
||||
generate_chapters_json()
|
||||
generate_manifest(results)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
|
||||
# Summary
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f" Build complete in {elapsed:.1f}s")
|
||||
print(f"{'=' * 60}")
|
||||
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML, OUT_MANIFEST]:
|
||||
if f.exists():
|
||||
size = f.stat().st_size
|
||||
print(f" ✓ {str(f.relative_to(REPO)):40s} {size:>10,} bytes")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
593
testament.html
593
testament.html
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user