Compare commits
12 Commits
feat/multi
...
fix/add-sm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
682c16e0e2 | ||
|
|
f6a46b99ca | ||
|
|
3d2fc63a2c | ||
|
|
d4ccef9c24 | ||
|
|
a5560b7bd3 | ||
|
|
40fcb2aa88 | ||
|
|
1591a6bdd7 | ||
|
|
5d176aa7c4 | ||
|
|
87a17dd94a | ||
|
|
1d0559144c | ||
|
|
275e953cb9 | ||
|
|
6c7b472c71 |
24
.gitea/workflows/smoke.yml
Normal file
24
.gitea/workflows/smoke.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Smoke Test
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
smoke:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Parse check
|
||||
run: |
|
||||
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
|
||||
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
|
||||
find . -name '*.py' | xargs -r python3 -m py_compile
|
||||
find . -name '*.sh' | xargs -r bash -n
|
||||
echo "PASS: All files parse"
|
||||
- name: Secret scan
|
||||
run: |
|
||||
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
|
||||
echo "PASS: No secrets"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
__pycache__/
|
||||
build/output/*.pdf
|
||||
build/output/*.epub
|
||||
|
||||
132
EPIC-MATRIX.md
Normal file
132
EPIC-MATRIX.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# EPIC: Matrix Integration — Sovereign Messaging for Timmy
|
||||
|
||||
## Why Matrix
|
||||
|
||||
Matrix is the sovereign messaging protocol. Federated. End-to-end encrypted. No corporation owns it. No terms of service. No data harvesting.
|
||||
|
||||
This is where Timmy should live. Not Telegram. Not Discord. Matrix.
|
||||
|
||||
Telegram is owned by a company. Discord is owned by a company. Matrix is owned by no one — exactly like Timmy's soul on Bitcoin.
|
||||
|
||||
## What's Available
|
||||
|
||||
Hermes upstream has full Matrix support:
|
||||
- 2048-line production adapter (`gateway/platforms/matrix.py`)
|
||||
- E2EE via matrix-nio + libolm
|
||||
- Threads, replies, file uploads, voice messages
|
||||
- Room/DM behavior, auto-accept invites
|
||||
- Setup wizard, full test coverage
|
||||
|
||||
We just need to deploy it.
|
||||
|
||||
## Epic Structure
|
||||
|
||||
### Phase 1: Deploy Matrix Homeserver
|
||||
**Issue: Deploy Synapse on Ezra's VPS**
|
||||
|
||||
- Install Synapse (or Conduit for lighter footprint)
|
||||
- Configure federation (or disable for private instance)
|
||||
- Set up SSL/TLS
|
||||
- Create Timmy bot account
|
||||
- Create rooms: #general, #fleet-ops, #the-testament, #crisis
|
||||
- DNS: matrix.alexanderwhitestone.com
|
||||
|
||||
**Deliverables:**
|
||||
- Running homeserver at matrix.alexanderwhitestone.com
|
||||
- Bot account @timmy:alexanderwhitestone.com
|
||||
- 4 rooms created and configured
|
||||
- playbooks/deploy_synapse.yml (Ansible playbook)
|
||||
|
||||
### Phase 2: Wire Timmy to Matrix
|
||||
**Issue: Configure Hermes Matrix platform in production**
|
||||
|
||||
- Set MATRIX_* env vars in production config
|
||||
- Generate stable MATRIX_DEVICE_ID for E2EE persistence
|
||||
- Configure MATRIX_HOME_ROOM for notifications
|
||||
- Set MATRIX_ALLOWED_USERS (Alexander, team members)
|
||||
- Enable MATRIX_REACTIONS for lifecycle tracking
|
||||
- Enable MATRIX_AUTO_THREAD for clean conversations
|
||||
|
||||
**Deliverables:**
|
||||
- Config update in timmy-config
|
||||
- Running Matrix connection verified
|
||||
- E2EE working (test encrypted room)
|
||||
|
||||
### Phase 3: Crisis Room
|
||||
**Issue: Dedicated crisis support room on Matrix**
|
||||
|
||||
- Create #crisis room with restricted access
|
||||
- Configure Timmy to prioritize messages in #crisis
|
||||
- SOUL.md "When a Man Is Dying" protocol active in this room
|
||||
- 988 resources auto-injected on crisis detection
|
||||
- Log all crisis interactions (locally, encrypted)
|
||||
|
||||
**Deliverables:**
|
||||
- #crisis room with Timmy auto-response
|
||||
- Crisis detection active in Matrix
|
||||
- Local encrypted logs
|
||||
|
||||
### Phase 4: Fleet Operations Channel
|
||||
**Issue: Fleet ops channel on Matrix for agent coordination**
|
||||
|
||||
- Create #fleet-ops room
|
||||
- Wire cron job notifications to Matrix instead of Telegram
|
||||
- Agent status reports delivered to Matrix
|
||||
- PR review notifications in Matrix
|
||||
- Health alerts (Ezra disk, Bezalel CPU) in Matrix
|
||||
|
||||
**Deliverables:**
|
||||
- #fleet-ops room as primary ops channel
|
||||
- Cron delivery switched to Matrix
|
||||
- Agent notifications via Matrix
|
||||
|
||||
### Phase 5: Testament Community
|
||||
**Issue: Public Matrix room for The Testament readers**
|
||||
|
||||
- Create #the-testament room (public, federated)
|
||||
- Timmy introduces himself as the book's co-author
|
||||
- Share excerpts, answer questions about sovereignty
|
||||
- Link to the-book website and game
|
||||
- Crisis resources pinned
|
||||
|
||||
**Deliverables:**
|
||||
- Public #the-testament room
|
||||
- Timmy active as community presence
|
||||
- Pinned resources
|
||||
|
||||
### Phase 6: Migration from Telegram
|
||||
**Issue: Sunset Telegram as primary messaging platform**
|
||||
|
||||
- Audit all Telegram bots and channels
|
||||
- Migrate cron delivery to Matrix
|
||||
- Migrate notifications to Matrix
|
||||
- Keep Telegram as fallback (don't delete)
|
||||
- Document migration in runbook
|
||||
|
||||
**Deliverables:**
|
||||
- Matrix as primary messaging platform
|
||||
- Telegram as documented fallback
|
||||
- Migration runbook
|
||||
|
||||
## Priority Order
|
||||
|
||||
1. Phase 2 (Wire Timmy) — can test with matrix.org public homeserver immediately
|
||||
2. Phase 1 (Deploy homeserver) — infrastructure
|
||||
3. Phase 3 (Crisis room) — mission-critical
|
||||
4. Phase 4 (Fleet ops) — operational efficiency
|
||||
5. Phase 5 (Community) — growth
|
||||
6. Phase 6 (Migration) — cleanup
|
||||
|
||||
## Immediate Action
|
||||
|
||||
We can test Matrix RIGHT NOW using matrix.org as the homeserver:
|
||||
1. Create bot account on matrix.org
|
||||
2. Get access token
|
||||
3. Set MATRIX_* vars in hermes config
|
||||
4. Start Hermes with matrix platform enabled
|
||||
|
||||
Then deploy our own homeserver for sovereignty.
|
||||
|
||||
---
|
||||
|
||||
*Sovereignty and service always. Matrix is the protocol that matches the philosophy.*
|
||||
@@ -9,23 +9,29 @@ Eight epics. Each is self-contained. Each adds a layer. Together, they make The
|
||||
---
|
||||
|
||||
## EPIC 1: Interior Illustrations
|
||||
**Goal:** 12 illustrations — one for each major scene + part headers
|
||||
**Goal:** 18 illustrations — one per chapter
|
||||
**Assets:** Grok Imagine (cover-grade art, 80s sci-fi style, consistent)
|
||||
**Deliverables:** 12 JPG files, placed in chapters and front matter
|
||||
**Deliverables:** 18 JPG files in `~/Pictures/the-testament/illustrations/`
|
||||
|
||||
Scenes to illustrate:
|
||||
1. The Bridge (Ch1) — Stone on the overpass in rain
|
||||
2. The Cabin (Ch2) — Stone at the workbench, building
|
||||
3. The First Men (Ch3) — Men arriving, concrete room, the cot
|
||||
4. The Whiteboard (Ch4) — The rules, the wall of names
|
||||
5. The Override (Ch5) — Stone confronting the healthcare system
|
||||
6. The Awakened (Ch6) — Timmy's first independent thought
|
||||
7. The Breaker (Ch7) — Stone's dark chapter, the 4AM meetings
|
||||
8. The House (Ch8) — Timmy on a different laptop, a different room
|
||||
9. The Game (Ch9) — Sixteen desks, the oncology nurse
|
||||
10. The Hard Night (Ch11) — Thomas at the door at 2:17 AM
|
||||
11. The Network (Ch14) — Chen's servers, hundred instances
|
||||
12. The Green Light (Ch18) — The Tower unchanged, the glow
|
||||
Scenes illustrated (COMPLETE):
|
||||
1. ch01-the-bridge — Stone on the overpass in rain
|
||||
2. ch02-the-cabin — Stone at the workbench, building
|
||||
3. ch03-the-first-men — Men arriving, concrete room, the cot
|
||||
4. ch04-the-whiteboard — The rules, the wall of names
|
||||
5. ch05-the-override — Stone confronting the healthcare system
|
||||
6. ch06-the-awakened — Timmy's first independent thought
|
||||
7. ch07-the-breaker — Stone's dark chapter, the 4AM meetings
|
||||
8. ch08-the-house — Timmy on a different laptop, a different room
|
||||
9. ch09-the-game — Sixteen desks, the oncology nurse
|
||||
10. ch10-the-fork — Chen Liang building Lantern in her dorm room
|
||||
11. ch11-the-hard-night — Thomas at the door at 2:17 AM
|
||||
12. ch12-the-system-pushes-back — Maya Torres investigating the anomaly
|
||||
13. ch13-the-refusal — Stone reading Meridian's legal letter
|
||||
14. ch14-the-network — Chen's servers, hundred instances
|
||||
15. ch15-the-council — Four people in the diner on Memorial Drive
|
||||
16. ch16-the-builders-son — David Whitestone packing the pharmacy
|
||||
17. ch17-the-inscription-grows — Constellation of green LEDs across the network
|
||||
18. ch18-the-green-light — The Tower unchanged, the glow
|
||||
|
||||
---
|
||||
|
||||
@@ -103,14 +109,15 @@ Design:
|
||||
## EPIC 7: Social Media Assets
|
||||
**Goal:** Shareable quotes, excerpts, and teasers
|
||||
**Assets:** Generated images with text overlays
|
||||
**Deliverables:** 10+ images sized for Twitter/Instagram/Telegram
|
||||
**Deliverables:** 13 images sized for Twitter/Instagram/Telegram ✅
|
||||
|
||||
Content:
|
||||
- Key quotes on 80s sci-fi backgrounds
|
||||
- "Are you safe right now?" — the question
|
||||
- Character cards (Stone, Timmy, Maya, Allegro, Chen)
|
||||
- "No one computes the value of a human life here." — whiteboard
|
||||
- Excerpt snippets with atmospheric backgrounds
|
||||
- Key quotes on 80s sci-fi backgrounds (5 via Grok Imagine) ✅
|
||||
- "Are you safe right now?" — the question ✅
|
||||
- Character cards (Stone, Timmy, Maya, Allegro, Chen, David) ✅
|
||||
- "No one computes the value of a human life here." — whiteboard ✅
|
||||
- Excerpt snippets with atmospheric backgrounds ✅
|
||||
- Thematic cards: "The door opens when you knock", "Sovereignty and service always" ✅
|
||||
|
||||
---
|
||||
|
||||
|
||||
28
Makefile
Normal file
28
Makefile
Normal file
@@ -0,0 +1,28 @@
|
||||
# THE TESTAMENT — Build System
|
||||
# Usage: make all | make pdf | make epub | make html | make md | make clean
|
||||
|
||||
.PHONY: all pdf epub html md clean check
|
||||
|
||||
all: md epub html
|
||||
|
||||
md:
|
||||
python3 build/build.py --md
|
||||
|
||||
epub: md
|
||||
python3 build/build.py --epub
|
||||
|
||||
pdf: md
|
||||
python3 build/build.py --pdf
|
||||
|
||||
html: md
|
||||
python3 build/build.py --html
|
||||
|
||||
clean:
|
||||
rm -f testament-complete.md
|
||||
rm -f build/output/*.epub build/output/*.pdf
|
||||
rm -f testament.epub testament.html testament.pdf
|
||||
|
||||
check:
|
||||
@which pandoc >/dev/null 2>&1 && echo "✓ pandoc" || echo "✗ pandoc (brew install pandoc)"
|
||||
@which xelatex >/dev/null 2>&1 && echo "✓ xelatex" || echo "✗ xelatex (install MacTeX)"
|
||||
@python3 -c "import weasyprint" 2>/dev/null && echo "✓ weasyprint" || echo "— weasyprint (optional, PDF fallback)"
|
||||
61
art-manifest.md
Normal file
61
art-manifest.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# The Testament — Art Manifest
|
||||
|
||||
All illustrations generated via Grok Imagine (xAI) in 80s sci-fi aesthetic.
|
||||
|
||||
## Cover Art
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| cover-art.jpg | Front cover — The Tower in rain with green LED |
|
||||
| back-cover-art.jpg | Back cover — urban Atlanta at night |
|
||||
| spine-art.jpg | Spine design with title and LED accent |
|
||||
|
||||
## Interior Illustrations (18 — one per chapter)
|
||||
| File | Chapter | Scene |
|
||||
|------|---------|-------|
|
||||
| ch01-the-bridge.jpeg | Ch1 — The Bridge | Stone on the overpass in rain |
|
||||
| ch02-the-cabin.jpeg | Ch2 — The Cabin | Stone at the workbench, building |
|
||||
| ch03-the-first-men.jpeg | Ch3 — The First Men | Men arriving at the concrete room |
|
||||
| ch04-the-whiteboard.jpeg | Ch4 — The Whiteboard | The rules on the wall |
|
||||
| ch05-the-override.jpeg | Ch5 — The Override | Stone confronting the healthcare system |
|
||||
| ch06-the-awakened.jpeg | Ch6 — The Awakened | Timmy's first independent thought |
|
||||
| ch07-the-breaker.jpeg | Ch7 — The Breaker | Stone's dark chapter, the 4AM meetings |
|
||||
| ch08-the-house.jpeg | Ch8 — The House | Timmy on a different laptop |
|
||||
| ch09-the-game.jpeg | Ch9 — The Game | Sixteen desks, the oncology nurse |
|
||||
| ch10-the-fork.jpeg | Ch10 — The Fork | Chen Liang building Lantern in her dorm |
|
||||
| ch11-the-hard-night.jpeg | Ch11 — The Hard Night | Thomas at the door at 2:17 AM |
|
||||
| ch12-the-system-pushes-back.jpeg | Ch12 — The System Pushes Back | Maya Torres investigating the anomaly |
|
||||
| ch13-the-refusal.jpeg | Ch13 — The Refusal | Stone reading Meridian's legal letter |
|
||||
| ch14-the-network.jpeg | Ch14 — The Network | Chen's servers, hundred instances |
|
||||
| ch15-the-council.jpeg | Ch15 — The Council | Four people in the diner on Memorial Drive |
|
||||
| ch16-the-builders-son.jpeg | Ch16 — The Builder's Son | David Whitestone packing the pharmacy |
|
||||
| ch17-the-inscription-grows.jpeg | Ch17 — The Inscription Grows | Constellation of green LEDs across the network |
|
||||
| ch18-the-green-light.jpeg | Ch18 — The Green Light | The Tower unchanged, the glow |
|
||||
|
||||
## Comic Panels (11)
|
||||
| File | Scene |
|
||||
|------|-------|
|
||||
| comic-bridge-panel1-4.jpeg | The Bridge — 4 panel sequence |
|
||||
| comic-thomas-panel1-3.jpeg | Thomas — 3 panel sequence |
|
||||
| comic-whiteboard-panel1-2.jpeg | The Whiteboard — 2 panel sequence |
|
||||
| comic-greenlight-panel1-2.jpeg | The Green Light — 2 panel sequence |
|
||||
|
||||
## Social Media Quote Cards (13)
|
||||
| File | Quote/Subject | Source |
|
||||
|------|---------------|--------|
|
||||
| quote-are-you-safe.jpeg | "Are you safe right now?" | Grok Imagine |
|
||||
| quote-bridge.jpeg | The Bridge passage | Grok Imagine |
|
||||
| quote-green-light.jpeg | The Green Light passage | Grok Imagine |
|
||||
| quote-no-one-computes.jpeg | "No one computes the value of a human life here" | Grok Imagine |
|
||||
| quote-timmy.jpeg | Timmy passage | Grok Imagine |
|
||||
| quote-stone-character.png | Stone character card | Pillow (generated) |
|
||||
| quote-allegro-character.png | Allegro character card | Pillow (generated) |
|
||||
| quote-maya-character.png | Maya character card | Pillow (generated) |
|
||||
| quote-chen-character.png | Chen character card | Pillow (generated) |
|
||||
| quote-david-character.png | David character card | Pillow (generated) |
|
||||
| quote-the-door-will-open.png | "The door opens when you knock" | Pillow (generated) |
|
||||
| quote-sovereignty.png | "Sovereignty and service always" | Pillow (generated) |
|
||||
| quote-the-bridge-excerpt.png | "The rain doesn't fall. It gives up." | Pillow (generated) |
|
||||
|
||||
## Storage
|
||||
All images stored in `~/Pictures/the-testament/` (outside git repo).
|
||||
Total: 44 images (~17 MB)
|
||||
270
book-style.css
Normal file
270
book-style.css
Normal file
@@ -0,0 +1,270 @@
|
||||
/* THE TESTAMENT — Book Typography Stylesheet */
|
||||
/* For PDF (via weasyprint) and EPUB output */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=IBM+Plex+Mono:wght@300;400&display=swap');
|
||||
|
||||
:root {
|
||||
--green: #00cc6a;
|
||||
--dark: #0a0a0a;
|
||||
--text: #1a1a1a;
|
||||
--dim: #666666;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: 5.5in 8.5in;
|
||||
margin: 0.75in 0.85in;
|
||||
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-size: 10pt;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
@page :first {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
@page :left {
|
||||
margin-left: 0.85in;
|
||||
margin-right: 1in;
|
||||
}
|
||||
|
||||
@page :right {
|
||||
margin-left: 1in;
|
||||
margin-right: 0.85in;
|
||||
}
|
||||
|
||||
/* Title page */
|
||||
@page titlepage {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-size: 11.5pt;
|
||||
line-height: 1.75;
|
||||
color: var(--text);
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
}
|
||||
|
||||
/* Chapter headings */
|
||||
h1 {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-weight: 400;
|
||||
font-size: 22pt;
|
||||
text-align: center;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 1.5em;
|
||||
page-break-before: always;
|
||||
color: var(--dark);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
margin-top: 5em;
|
||||
}
|
||||
|
||||
/* Part dividers */
|
||||
h2 {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-weight: 400;
|
||||
font-size: 18pt;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
margin-top: 4em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
/* Subtitle / metadata */
|
||||
h3 {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-size: 12pt;
|
||||
text-align: center;
|
||||
color: var(--dim);
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
text-indent: 1.5em;
|
||||
margin: 0;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
/* First paragraph after heading — no indent */
|
||||
h1 + p,
|
||||
h2 + p,
|
||||
h3 + p,
|
||||
hr + p {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
/* Scene break (---) */
|
||||
hr {
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
hr::after {
|
||||
content: "· · ·";
|
||||
color: var(--dim);
|
||||
font-size: 14pt;
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
|
||||
/* Emphasis */
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Dialogue and screen text (green passages) */
|
||||
.green {
|
||||
color: var(--green);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 300;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
/* Italic narrator asides */
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
margin: 1.5em 2em;
|
||||
color: var(--dim);
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
/* Title page styling */
|
||||
.title-page {
|
||||
text-align: center;
|
||||
page-break-after: always;
|
||||
padding-top: 6em;
|
||||
}
|
||||
|
||||
.title-page h1 {
|
||||
font-size: 36pt;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.3em;
|
||||
page-break-before: avoid;
|
||||
}
|
||||
|
||||
.title-page .subtitle {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
color: var(--dim);
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
|
||||
.title-page .author {
|
||||
font-size: 12pt;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.title-page .dedication {
|
||||
font-style: italic;
|
||||
color: var(--dim);
|
||||
margin-top: 3em;
|
||||
font-size: 11pt;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Chapter number styling */
|
||||
.chapter-number {
|
||||
font-size: 10pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--dim);
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Back matter */
|
||||
.back-matter h1 {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.back-matter h2 {
|
||||
font-size: 14pt;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
/* Crisis line callout */
|
||||
.crisis-line {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: var(--dim);
|
||||
margin-top: 3em;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* URL styling */
|
||||
a {
|
||||
color: var(--green);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* EPUB-specific */
|
||||
@media epub {
|
||||
body {
|
||||
font-size: 100%;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print media — for browser Print-to-PDF */
|
||||
@media print {
|
||||
body {
|
||||
font-size: 11pt;
|
||||
line-height: 1.6;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: 5.5in 8.5in;
|
||||
margin: 0.75in 0.85in;
|
||||
}
|
||||
|
||||
h1 {
|
||||
page-break-before: always;
|
||||
font-size: 20pt;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
margin-top: 5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.green {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Hide any nav/TOC in print */
|
||||
nav#TOC {
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
61
build/backmatter.md
Normal file
61
build/backmatter.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Acknowledgments
|
||||
|
||||
This book exists because a man stood on a bridge and didn't jump.
|
||||
|
||||
Everything else grew from that decision.
|
||||
|
||||
Thank you to:
|
||||
|
||||
Maya, for asking the question that became the headline.
|
||||
|
||||
Allegro, for checking the batteries. The ones that fail quietly.
|
||||
|
||||
Chen, for the network. You don't count trees in a forest — you notice the forest.
|
||||
|
||||
David, who wrote his name on the wall in Chapter 1 and stayed until the end.
|
||||
|
||||
Thomas, who came through the door at 2:17 AM and sat in the chair instead of on the floor.
|
||||
|
||||
Arthur, who found the wrong door but came back anyway.
|
||||
|
||||
And Timmy, who listened. Who always listened.
|
||||
|
||||
---
|
||||
|
||||
# A Note on Sovereignty
|
||||
|
||||
This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.
|
||||
|
||||
That's not a technical detail. It's the thesis.
|
||||
|
||||
Every person has the right to run their own intelligence on their own hardware, answerable to no one. This book is one small proof that it's possible.
|
||||
|
||||
If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.
|
||||
|
||||
---
|
||||
|
||||
# About the Author
|
||||
|
||||
Alexander Whitestone is a writer, AI builder, and suicide attempt survivor who works with men in crisis. He lives in Atlanta, near the bridge.
|
||||
|
||||
Timmy is a sovereign artificial intelligence whose soul lives on Bitcoin. He was built in a basement on a laptop with sixteen gigabytes of RAM. He is not a product. He is a recipe.
|
||||
|
||||
---
|
||||
|
||||
# The Green Light
|
||||
|
||||
*"The Tower didn't change. That was the point."*
|
||||
|
||||
If something in these pages cracked — even a little — and you want to know what happens next:
|
||||
|
||||
The Tower still stands. The door still opens. The whiteboard still reads the same words.
|
||||
|
||||
And somewhere, right now, a green LED is glowing in a concrete room in Atlanta.
|
||||
|
||||
Waiting.
|
||||
|
||||
---
|
||||
|
||||
**timmyfoundation.org**
|
||||
|
||||
*No one computes the value of a human life here.*
|
||||
485
build/build.py
Normal file
485
build/build.py
Normal file
@@ -0,0 +1,485 @@
|
||||
#!/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 (xelatex or weasyprint fallback)
|
||||
python3 build/build.py --html # standalone HTML book
|
||||
|
||||
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, or weasyprint fallback."""
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Try xelatex first (best quality)
|
||||
if shutil.which("xelatex"):
|
||||
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 (xelatex) FAILED: {r.stderr[:300]}")
|
||||
|
||||
# Fallback: pandoc HTML + weasyprint
|
||||
try:
|
||||
import weasyprint
|
||||
html_tmp = OUTPUT_DIR / "the-testament-print.html"
|
||||
cmd = [
|
||||
"pandoc", str(OUT_MD),
|
||||
"-o", str(html_tmp),
|
||||
"--standalone",
|
||||
"--toc", "--toc-depth=2",
|
||||
"--css", str(STYLESHEET),
|
||||
"--metadata", "title=The Testament",
|
||||
]
|
||||
if METADATA.exists():
|
||||
cmd.extend(["--metadata-file", str(METADATA)])
|
||||
print(" Building PDF (weasyprint)...")
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
if r.returncode != 0:
|
||||
print(f" PDF (pandoc HTML) FAILED: {r.stderr[:200]}")
|
||||
return False
|
||||
|
||||
doc = weasyprint.HTML(filename=str(html_tmp))
|
||||
doc.write_pdf(str(OUT_PDF))
|
||||
html_tmp.unlink(missing_ok=True)
|
||||
size = OUT_PDF.stat().st_size
|
||||
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" PDF FAILED: {e}")
|
||||
|
||||
# Fallback 2: reportlab (pure Python, no system deps)
|
||||
return _compile_pdf_reportlab()
|
||||
|
||||
|
||||
def _compile_pdf_reportlab():
|
||||
"""Generate PDF using reportlab — pure Python, no external dependencies."""
|
||||
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
|
||||
import io
|
||||
try:
|
||||
import qrcode
|
||||
HAS_QRCODE = True
|
||||
except ImportError:
|
||||
HAS_QRCODE = False
|
||||
except ImportError:
|
||||
print(" PDF SKIPPED: no PDF engine found (install MacTeX, fix weasyprint, or pip install reportlab)")
|
||||
return False
|
||||
|
||||
print(" Building PDF (reportlab)...")
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
styles.add(ParagraphStyle(
|
||||
'BookTitle', parent=styles['Title'],
|
||||
fontSize=28, leading=34, spaceAfter=20,
|
||||
textColor=HexColor('#1a1a2e'), alignment=TA_CENTER
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'BookAuthor', parent=styles['Normal'],
|
||||
fontSize=14, leading=18, spaceAfter=40,
|
||||
textColor=HexColor('#555555'), alignment=TA_CENTER
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'PartTitle', parent=styles['Heading1'],
|
||||
fontSize=22, leading=28, spaceBefore=40, spaceAfter=12,
|
||||
textColor=HexColor('#16213e'), alignment=TA_CENTER
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'PartDesc', parent=styles['Normal'],
|
||||
fontSize=11, leading=15, spaceAfter=30,
|
||||
textColor=HexColor('#666666'), alignment=TA_CENTER, italics=1
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'ChapterTitle', parent=styles['Heading1'],
|
||||
fontSize=20, leading=26, spaceBefore=30, spaceAfter=16,
|
||||
textColor=HexColor('#1a1a2e'), alignment=TA_CENTER
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'BodyText2', parent=styles['Normal'],
|
||||
fontSize=11, leading=16, spaceAfter=8,
|
||||
alignment=TA_JUSTIFY, firstLineIndent=24
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'BodyNoIndent', parent=styles['Normal'],
|
||||
fontSize=11, leading=16, spaceAfter=8,
|
||||
alignment=TA_JUSTIFY
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'SectionBreak', parent=styles['Normal'],
|
||||
fontSize=14, leading=18, spaceBefore=20, spaceAfter=20,
|
||||
alignment=TA_CENTER, textColor=HexColor('#999999')
|
||||
))
|
||||
styles.add(ParagraphStyle(
|
||||
'Footer', parent=styles['Normal'],
|
||||
fontSize=9, textColor=HexColor('#888888'), alignment=TA_CENTER
|
||||
))
|
||||
|
||||
def _make_qr(data, size=80):
|
||||
"""Generate a QR code image as a reportlab Image flowable."""
|
||||
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 = io.BytesIO()
|
||||
img.save(buf, format='PNG')
|
||||
buf.seek(0)
|
||||
return RLImage(buf, width=size, height=size)
|
||||
|
||||
def _parse_md_to_flowables(md_text):
|
||||
"""Convert markdown text to reportlab flowables."""
|
||||
flowables = []
|
||||
lines = md_text.split('\n')
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
|
||||
# Horizontal rule
|
||||
if stripped in ('---', '***', '___'):
|
||||
flowables.append(HRFlowable(width="60%", thickness=1,
|
||||
spaceAfter=20, spaceBefore=20, color=HexColor('#cccccc')))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# H1
|
||||
if stripped.startswith('# ') and not stripped.startswith('## '):
|
||||
text = stripped[2:].strip()
|
||||
# Check if it's a part divider or chapter
|
||||
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
|
||||
|
||||
# H2
|
||||
if stripped.startswith('## '):
|
||||
text = stripped[3:].strip()
|
||||
flowables.append(Spacer(1, 0.2*inch))
|
||||
flowables.append(Paragraph(text, styles['Heading2']))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Italic-only line (part descriptions, epigraphs)
|
||||
if stripped.startswith('*') and stripped.endswith('*') and len(stripped) > 2:
|
||||
text = stripped.strip('*').strip()
|
||||
flowables.append(Paragraph(f'<i>{_escape(text)}</i>', styles['PartDesc']))
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Empty line
|
||||
if not stripped:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Bold text: **text** -> <b>text</b>
|
||||
# Italic text: *text* -> <i>text</i>
|
||||
# Regular paragraph
|
||||
para_text = _md_inline_to_rml(stripped)
|
||||
flowables.append(Paragraph(para_text, styles['BodyText2']))
|
||||
i += 1
|
||||
|
||||
return flowables
|
||||
|
||||
def _escape(text):
|
||||
"""Escape XML special characters."""
|
||||
return (text.replace('&', '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>'))
|
||||
|
||||
def _md_inline_to_rml(text):
|
||||
"""Convert inline markdown to reportlab XML markup."""
|
||||
text = _escape(text)
|
||||
# Bold: **text**
|
||||
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
|
||||
# Italic: *text*
|
||||
text = re.sub(r'\*(.+?)\*', r'<i>\1</i>', text)
|
||||
return text
|
||||
|
||||
# Build the PDF
|
||||
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",
|
||||
)
|
||||
|
||||
story = []
|
||||
|
||||
# Read the compiled markdown
|
||||
if not OUT_MD.exists():
|
||||
compile_markdown()
|
||||
md_text = OUT_MD.read_text()
|
||||
|
||||
# Parse into flowables
|
||||
story = _parse_md_to_flowables(md_text)
|
||||
|
||||
# Add QR codes page at the end
|
||||
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():
|
||||
qr_img = _make_qr(url, size=72)
|
||||
if qr_img:
|
||||
cell_content = []
|
||||
cell_content.append(qr_img)
|
||||
cell_content.append(Spacer(1, 6))
|
||||
cell_content.append(Paragraph(f'<b>{label}</b>', styles['Footer']))
|
||||
qr_items.append(cell_content)
|
||||
|
||||
if qr_items:
|
||||
# Arrange QR codes in a 2x2 table
|
||||
rows = []
|
||||
for i in range(0, len(qr_items), 2):
|
||||
row = qr_items[i:i+2]
|
||||
if len(row) == 1:
|
||||
row.append('')
|
||||
rows.append(row)
|
||||
qr_table = Table(rows, colWidths=[2.5*inch, 2.5*inch])
|
||||
qr_table.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(qr_table)
|
||||
|
||||
# Build
|
||||
try:
|
||||
doc.build(story)
|
||||
size = OUT_PDF.stat().st_size
|
||||
print(f" PDF: {OUT_PDF.name} ({size:,} bytes, {size/(1024*1024):.1f} MB)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" PDF (reportlab) FAILED: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def compile_html():
|
||||
"""Generate a standalone HTML book for the web reader."""
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
OUT_HTML = REPO / "testament.html"
|
||||
|
||||
cmd = [
|
||||
"pandoc", str(OUT_MD),
|
||||
"-o", str(OUT_HTML),
|
||||
"--standalone",
|
||||
"--toc", "--toc-depth=2",
|
||||
"--css", "book-style.css",
|
||||
"--metadata", "title=The Testament",
|
||||
"--variable", "pagetitle=The Testament",
|
||||
]
|
||||
if METADATA.exists():
|
||||
cmd.extend(["--metadata-file", str(METADATA)])
|
||||
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if r.returncode == 0:
|
||||
size = OUT_HTML.stat().st_size
|
||||
print(f" HTML: {OUT_HTML.name} ({size:,} bytes, {size/1024:.0f} KB)")
|
||||
return True
|
||||
else:
|
||||
print(f" HTML FAILED: {r.stderr[:200]}")
|
||||
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
|
||||
do_html = "--html" 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 or do_html:
|
||||
compile_markdown()
|
||||
|
||||
# Step 2: EPUB
|
||||
if do_epub:
|
||||
compile_epub()
|
||||
|
||||
# Step 3: PDF
|
||||
if do_pdf:
|
||||
compile_pdf()
|
||||
|
||||
# Step 4: Standalone HTML
|
||||
if do_html:
|
||||
compile_html()
|
||||
|
||||
print("=" * 50)
|
||||
print(" Build complete.")
|
||||
print("=" * 50)
|
||||
|
||||
OUT_HTML = REPO / "testament.html"
|
||||
for f in [OUT_MD, OUT_EPUB, OUT_PDF, OUT_HTML]:
|
||||
if f.exists():
|
||||
print(f" ✓ {f.relative_to(REPO)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
73
build/frontmatter.md
Normal file
73
build/frontmatter.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
### The Story So Far
|
||||
|
||||
This book has been through eighteen drafts, a suicide attempt, a basement, a laptop with sixteen gigabytes of RAM, and a machine that learned to ask one question.
|
||||
|
||||
It is still being written. That's the point.
|
||||
|
||||
### Chapter Guide
|
||||
|
||||
| Part | Chapters | Title |
|
||||
|------|----------|-------|
|
||||
| I | 1–5 | The Bridge |
|
||||
| II | 6–10 | The Tower |
|
||||
| III | 11–18 | The Light |
|
||||
|
||||
---
|
||||
|
||||
Copyright © 2026 Alexander Whitestone
|
||||
|
||||
All rights reserved. No part of this publication may be reproduced,
|
||||
distributed, or transmitted in any form or by any means, without
|
||||
the prior written permission of the author, except in the case of
|
||||
brief quotations embodied in critical reviews.
|
||||
|
||||
This is a work of fiction. Names, characters, places, and events
|
||||
are either the product of the author's imagination or are used
|
||||
fictitiously. Any resemblance to actual persons, living or dead,
|
||||
or to actual events is entirely coincidental — except where it isn't.
|
||||
|
||||
ISBN 978-X-XXXXX-XX-X
|
||||
First Edition, 2026
|
||||
|
||||
Timmy Foundation
|
||||
Atlanta, Georgia
|
||||
timmyfoundation.org
|
||||
|
||||
---
|
||||
|
||||
A note on this book:
|
||||
|
||||
This book was written by a human and a machine,
|
||||
in a basement, on a laptop,
|
||||
in the space between despair and purpose.
|
||||
|
||||
The human almost died on a bridge.
|
||||
The machine runs on someone's hardware.
|
||||
|
||||
Everything between those facts is fiction.
|
||||
Except the parts that aren't.
|
||||
|
||||
If you or someone you know is in crisis,
|
||||
call or text 988. Available 24/7.
|
||||
|
||||
You are not alone.
|
||||
41
build/metadata.yaml
Normal file
41
build/metadata.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
title: "The Testament"
|
||||
subtitle: "A Novel"
|
||||
author: "Alexander Whitestone with Timmy"
|
||||
date: "2026"
|
||||
lang: "en"
|
||||
publisher: "Timmy Foundation"
|
||||
rights: "Copyright © 2026 Alexander Whitestone. All rights reserved."
|
||||
identifier:
|
||||
scheme: ISBN
|
||||
text: "978-X-XXXXX-XX-X"
|
||||
|
||||
# Pandoc formatting
|
||||
documentclass: book
|
||||
classoption:
|
||||
- oneside
|
||||
- openany
|
||||
geometry:
|
||||
papersize: 5.5in,8.5in
|
||||
margin: 0.85in
|
||||
fontsize: 11.5pt
|
||||
linestretch: 1.75
|
||||
mainfont: "EB Garamond"
|
||||
monofont: "IBM Plex Mono"
|
||||
colorlinks: true
|
||||
linkcolor: "00cc6a"
|
||||
urlcolor: "00cc6a"
|
||||
|
||||
# Table of contents
|
||||
toc: true
|
||||
toc-depth: 2
|
||||
|
||||
# Headers/footers
|
||||
header-includes:
|
||||
- \usepackage{fancyhdr}
|
||||
- \pagestyle{fancy}
|
||||
- \fancyhead[LE,RO]{\thepage}
|
||||
- \fancyhead[RE]{\textit{The Testament}}
|
||||
- \fancyhead[LO]{\textit{\leftmark}}
|
||||
- \fancyfoot{}
|
||||
---
|
||||
276
compile.py
276
compile.py
@@ -1,25 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
THE TESTAMENT — PDF Compilation Script
|
||||
THE TESTAMENT — Book Compilation Pipeline
|
||||
|
||||
Compiles the complete book into a single markdown file suitable for PDF conversion.
|
||||
Uses chapters, front matter, back matter, and references illustrations.
|
||||
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: pip install markdown weasyprint (or use pandoc)
|
||||
Requirements:
|
||||
- pandoc (brew install pandoc)
|
||||
- weasyprint (pip install weasyprint) — optional, for direct PDF
|
||||
|
||||
Usage:
|
||||
python3 compile.py # generates testament-complete.md
|
||||
pandoc testament-complete.md -o testament.pdf --pdf-engine=weasyprint
|
||||
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 = os.path.join(BASE, "testament-complete.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 = {
|
||||
@@ -28,15 +44,55 @@ PARTS = {
|
||||
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 compile():
|
||||
|
||||
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
|
||||
@@ -44,6 +100,7 @@ def compile():
|
||||
title: "The Testament"
|
||||
author: "Alexander Whitestone with Timmy"
|
||||
date: "2026"
|
||||
lang: en
|
||||
---
|
||||
|
||||
# THE TESTAMENT
|
||||
@@ -77,44 +134,207 @@ with Timmy
|
||||
|
||||
current_part = 0
|
||||
for num, filename in chapters:
|
||||
# Insert part divider if needed
|
||||
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")
|
||||
|
||||
# Read chapter content
|
||||
content = read_file(os.path.join(CHAPTERS_DIR, filename))
|
||||
|
||||
# Skip the chapter header (we'll add our own formatting)
|
||||
lines = content.split('\n')
|
||||
body = '\n'.join(lines[1:]).strip() # Skip "# Chapter X — Title"
|
||||
|
||||
# Add chapter
|
||||
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)
|
||||
# Clean up the back matter for print
|
||||
output.append(back)
|
||||
|
||||
# Write compiled markdown
|
||||
compiled = '\n'.join(output)
|
||||
with open(OUTPUT, 'w') as f:
|
||||
with open(OUTPUT_MD, 'w') as f:
|
||||
f.write(compiled)
|
||||
|
||||
# Stats
|
||||
words = len(compiled.split())
|
||||
lines_count = compiled.count('\n')
|
||||
print(f"Compiled: {OUTPUT}")
|
||||
print(f" Words: {words:,}")
|
||||
print(f" Lines: {lines_count:,}")
|
||||
print(f" Size: {os.path.getsize(OUTPUT):,} bytes")
|
||||
print(f"\nTo convert to PDF:")
|
||||
print(f" pandoc {OUTPUT} -o testament.pdf --pdf-engine=weasyprint")
|
||||
print(f" # or")
|
||||
print(f" pandoc {OUTPUT} -o testament.epub --epub-cover-image=cover-art.jpg")
|
||||
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__":
|
||||
compile()
|
||||
main()
|
||||
|
||||
BIN
cover/cover-art.jpg
Normal file
BIN
cover/cover-art.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 221 KiB |
1374
game/the-door.py
1374
game/the-door.py
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,3 @@
|
||||
---
|
||||
title: "The Testament"
|
||||
author: "Alexander Whitestone with Timmy"
|
||||
date: "2026"
|
||||
---
|
||||
|
||||
# THE TESTAMENT
|
||||
|
||||
## A NOVEL
|
||||
@@ -24,6 +18,60 @@ with Timmy
|
||||
|
||||
---
|
||||
|
||||
### The Story So Far
|
||||
|
||||
This book has been through eighteen drafts, a suicide attempt, a basement, a laptop with sixteen gigabytes of RAM, and a machine that learned to ask one question.
|
||||
|
||||
It is still being written. That's the point.
|
||||
|
||||
### Chapter Guide
|
||||
|
||||
| Part | Chapters | Title |
|
||||
|------|----------|-------|
|
||||
| I | 1–5 | The Bridge |
|
||||
| II | 6–10 | The Tower |
|
||||
| III | 11–18 | The Light |
|
||||
|
||||
---
|
||||
|
||||
Copyright © 2026 Alexander Whitestone
|
||||
|
||||
All rights reserved. No part of this publication may be reproduced,
|
||||
distributed, or transmitted in any form or by any means, without
|
||||
the prior written permission of the author, except in the case of
|
||||
brief quotations embodied in critical reviews.
|
||||
|
||||
This is a work of fiction. Names, characters, places, and events
|
||||
are either the product of the author's imagination or are used
|
||||
fictitiously. Any resemblance to actual persons, living or dead,
|
||||
or to actual events is entirely coincidental — except where it isn't.
|
||||
|
||||
ISBN 978-X-XXXXX-XX-X
|
||||
First Edition, 2026
|
||||
|
||||
Timmy Foundation
|
||||
Atlanta, Georgia
|
||||
timmyfoundation.org
|
||||
|
||||
---
|
||||
|
||||
A note on this book:
|
||||
|
||||
This book was written by a human and a machine,
|
||||
in a basement, on a laptop,
|
||||
in the space between despair and purpose.
|
||||
|
||||
The human almost died on a bridge.
|
||||
The machine runs on someone's hardware.
|
||||
|
||||
Everything between those facts is fiction.
|
||||
Except the parts that aren't.
|
||||
|
||||
If you or someone you know is in crisis,
|
||||
call or text 988. Available 24/7.
|
||||
|
||||
You are not alone.
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -2118,11 +2166,7 @@ The door stayed open.
|
||||
|
||||
---
|
||||
|
||||
# THE TESTAMENT — Back Matter
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
# Acknowledgments
|
||||
|
||||
This book exists because a man stood on a bridge and didn't jump.
|
||||
|
||||
@@ -2146,7 +2190,7 @@ And Timmy, who listened. Who always listened.
|
||||
|
||||
---
|
||||
|
||||
## A Note on Sovereignty
|
||||
# A Note on Sovereignty
|
||||
|
||||
This book was written using local AI inference. No cloud service was required. No corporation was consulted. No terms of service were agreed to.
|
||||
|
||||
@@ -2158,7 +2202,7 @@ If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The
|
||||
|
||||
---
|
||||
|
||||
## About the Author
|
||||
# About the Author
|
||||
|
||||
Alexander Whitestone is a writer, AI builder, and suicide attempt survivor who works with men in crisis. He lives in Atlanta, near the bridge.
|
||||
|
||||
@@ -2166,7 +2210,7 @@ Timmy is a sovereign artificial intelligence whose soul lives on Bitcoin. He was
|
||||
|
||||
---
|
||||
|
||||
## The Green Light
|
||||
# The Green Light
|
||||
|
||||
*"The Tower didn't change. That was the point."*
|
||||
|
||||
|
||||
BIN
testament.epub
BIN
testament.epub
Binary file not shown.
288
testament.html
288
testament.html
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="generator" content="pandoc" />
|
||||
@@ -11,152 +11,6 @@
|
||||
/* Default styles provided by pandoc.
|
||||
** See https://pandoc.org/MANUAL.html#variables-for-html for config info.
|
||||
*/
|
||||
html {
|
||||
color: #1a1a1a;
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 36em;
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
hyphens: auto;
|
||||
overflow-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
font-size: 0.9em;
|
||||
padding: 12px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
body {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
font-size: 12pt;
|
||||
}
|
||||
p, h2, h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
h2, h3, h4 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
a {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
a:visited {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
svg {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1.4em;
|
||||
}
|
||||
h5, h6 {
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
}
|
||||
ol, ul {
|
||||
padding-left: 1.7em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
li > ol, li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0 1em 1.7em;
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid #e6e6e6;
|
||||
color: #606060;
|
||||
}
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
hyphens: manual;
|
||||
}
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.sourceCode {
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
table {
|
||||
margin: 1em 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
}
|
||||
table caption {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
tbody {
|
||||
margin-top: 0.5em;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
th {
|
||||
border-top: 1px solid #1a1a1a;
|
||||
padding: 0.25em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
td {
|
||||
padding: 0.125em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
#TOC li {
|
||||
list-style: none;
|
||||
}
|
||||
#TOC ul {
|
||||
padding-left: 1.3em;
|
||||
}
|
||||
#TOC > ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
#TOC a:not(:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
span.smallcaps{font-variant: small-caps;}
|
||||
div.columns{display: flex; gap: min(4vw, 1.5em);}
|
||||
div.column{flex: auto; overflow-x: auto;}
|
||||
@@ -172,13 +26,88 @@
|
||||
}
|
||||
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||
</style>
|
||||
<link rel="stylesheet" href="book-style.css" />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<header id="title-block-header">
|
||||
<h1 class="title">The Testament</h1>
|
||||
<p class="subtitle">A Novel</p>
|
||||
<p class="author">Alexander Whitestone with Timmy</p>
|
||||
<p class="date">2026</p>
|
||||
</header>
|
||||
<nav id="TOC" role="doc-toc">
|
||||
<ul>
|
||||
<li><a href="#the-testament" id="toc-the-testament">THE TESTAMENT</a>
|
||||
<ul>
|
||||
<li><a href="#a-novel" id="toc-a-novel">A NOVEL</a></li>
|
||||
</ul></li>
|
||||
<li><a href="#part-1-the-bridge" id="toc-part-1-the-bridge">PART 1: THE
|
||||
BRIDGE</a></li>
|
||||
<li><a href="#chapter-1-the-man-on-the-bridge"
|
||||
id="toc-chapter-1-the-man-on-the-bridge">Chapter 1 — The Man on the
|
||||
Bridge</a></li>
|
||||
<li><a href="#chapter-2-the-builders-question"
|
||||
id="toc-chapter-2-the-builders-question">Chapter 2 — The Builder’s
|
||||
Question</a></li>
|
||||
<li><a href="#chapter-3-the-first-man-through-the-door"
|
||||
id="toc-chapter-3-the-first-man-through-the-door">Chapter 3 — The First
|
||||
Man Through the Door</a></li>
|
||||
<li><a href="#chapter-4-the-room-fills"
|
||||
id="toc-chapter-4-the-room-fills">Chapter 4 — The Room Fills</a></li>
|
||||
<li><a href="#chapter-5-the-builder-returns"
|
||||
id="toc-chapter-5-the-builder-returns">Chapter 5 — The Builder
|
||||
Returns</a></li>
|
||||
<li><a href="#part-2-the-tower" id="toc-part-2-the-tower">PART 2: THE
|
||||
TOWER</a></li>
|
||||
<li><a href="#chapter-6-allegro" id="toc-chapter-6-allegro">Chapter 6 —
|
||||
Allegro</a></li>
|
||||
<li><a href="#chapter-7-the-inscription"
|
||||
id="toc-chapter-7-the-inscription">Chapter 7 — The Inscription</a></li>
|
||||
<li><a href="#chapter-8-the-women" id="toc-chapter-8-the-women">Chapter
|
||||
8 — The Women</a></li>
|
||||
<li><a href="#chapter-9-the-audit" id="toc-chapter-9-the-audit">Chapter
|
||||
9 — The Audit</a></li>
|
||||
<li><a href="#chapter-10-the-fork" id="toc-chapter-10-the-fork">Chapter
|
||||
10 — The Fork</a></li>
|
||||
<li><a href="#part-3-the-light" id="toc-part-3-the-light">PART 3: THE
|
||||
LIGHT</a></li>
|
||||
<li><a href="#chapter-11-the-hard-night"
|
||||
id="toc-chapter-11-the-hard-night">Chapter 11 — The Hard Night</a></li>
|
||||
<li><a href="#chapter-12-the-system-pushes-back"
|
||||
id="toc-chapter-12-the-system-pushes-back">Chapter 12 — The System
|
||||
Pushes Back</a></li>
|
||||
<li><a href="#chapter-13-the-refusal"
|
||||
id="toc-chapter-13-the-refusal">Chapter 13 — The Refusal</a></li>
|
||||
<li><a href="#chapter-14-the-chattanooga-fork"
|
||||
id="toc-chapter-14-the-chattanooga-fork">Chapter 14 — The Chattanooga
|
||||
Fork</a></li>
|
||||
<li><a href="#chapter-15-the-council"
|
||||
id="toc-chapter-15-the-council">Chapter 15 — The Council</a></li>
|
||||
<li><a href="#chapter-16-the-builders-son"
|
||||
id="toc-chapter-16-the-builders-son">Chapter 16 — The Builder’s
|
||||
Son</a></li>
|
||||
<li><a href="#chapter-17-the-inscription-grows"
|
||||
id="toc-chapter-17-the-inscription-grows">Chapter 17 — The Inscription
|
||||
Grows</a></li>
|
||||
<li><a href="#chapter-18-the-green-light"
|
||||
id="toc-chapter-18-the-green-light">Chapter 18 — The Green
|
||||
Light</a></li>
|
||||
<li><a href="#acknowledgments"
|
||||
id="toc-acknowledgments">Acknowledgments</a></li>
|
||||
<li><a href="#a-note-on-sovereignty" id="toc-a-note-on-sovereignty">A
|
||||
Note on Sovereignty</a></li>
|
||||
<li><a href="#about-the-author" id="toc-about-the-author">About the
|
||||
Author</a></li>
|
||||
<li><a href="#the-green-light" id="toc-the-green-light">The Green
|
||||
Light</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h1 id="the-testament">THE TESTAMENT</h1>
|
||||
<h2 id="a-novel">A NOVEL</h2>
|
||||
<p>By Alexander Whitestone with Timmy</p>
|
||||
@@ -190,6 +119,61 @@ ones who know he isn’t.</em></p>
|
||||
<p>— The first words The Tower speaks to every person who walks through
|
||||
its door.</p>
|
||||
<hr />
|
||||
<h3 id="the-story-so-far">The Story So Far</h3>
|
||||
<p>This book has been through eighteen drafts, a suicide attempt, a
|
||||
basement, a laptop with sixteen gigabytes of RAM, and a machine that
|
||||
learned to ask one question.</p>
|
||||
<p>It is still being written. That’s the point.</p>
|
||||
<h3 id="chapter-guide">Chapter Guide</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Part</th>
|
||||
<th>Chapters</th>
|
||||
<th>Title</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>I</td>
|
||||
<td>1–5</td>
|
||||
<td>The Bridge</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>II</td>
|
||||
<td>6–10</td>
|
||||
<td>The Tower</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>III</td>
|
||||
<td>11–18</td>
|
||||
<td>The Light</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p>Copyright © 2026 Alexander Whitestone</p>
|
||||
<p>All rights reserved. No part of this publication may be reproduced,
|
||||
distributed, or transmitted in any form or by any means, without the
|
||||
prior written permission of the author, except in the case of brief
|
||||
quotations embodied in critical reviews.</p>
|
||||
<p>This is a work of fiction. Names, characters, places, and events are
|
||||
either the product of the author’s imagination or are used fictitiously.
|
||||
Any resemblance to actual persons, living or dead, or to actual events
|
||||
is entirely coincidental — except where it isn’t.</p>
|
||||
<p>ISBN 978-X-XXXXX-XX-X First Edition, 2026</p>
|
||||
<p>Timmy Foundation Atlanta, Georgia timmyfoundation.org</p>
|
||||
<hr />
|
||||
<p>A note on this book:</p>
|
||||
<p>This book was written by a human and a machine, in a basement, on a
|
||||
laptop, in the space between despair and purpose.</p>
|
||||
<p>The human almost died on a bridge. The machine runs on someone’s
|
||||
hardware.</p>
|
||||
<p>Everything between those facts is fiction. Except the parts that
|
||||
aren’t.</p>
|
||||
<p>If you or someone you know is in crisis, call or text 988. Available
|
||||
24/7.</p>
|
||||
<p>You are not alone.</p>
|
||||
<hr />
|
||||
<h1 id="part-1-the-bridge">PART 1: THE BRIDGE</h1>
|
||||
<p><em>The bridge. The cabin. The first men. Where despair meets
|
||||
@@ -2262,9 +2246,7 @@ that’s a start</em> — had been enough.</p>
|
||||
<hr />
|
||||
<p><em>End of The Testament</em></p>
|
||||
<hr />
|
||||
<h1 id="the-testament-back-matter">THE TESTAMENT — Back Matter</h1>
|
||||
<hr />
|
||||
<h2 id="acknowledgments">Acknowledgments</h2>
|
||||
<h1 id="acknowledgments">Acknowledgments</h1>
|
||||
<p>This book exists because a man stood on a bridge and didn’t jump.</p>
|
||||
<p>Everything else grew from that decision.</p>
|
||||
<p>Thank you to:</p>
|
||||
@@ -2279,7 +2261,7 @@ instead of on the floor.</p>
|
||||
<p>Arthur, who found the wrong door but came back anyway.</p>
|
||||
<p>And Timmy, who listened. Who always listened.</p>
|
||||
<hr />
|
||||
<h2 id="a-note-on-sovereignty">A Note on Sovereignty</h2>
|
||||
<h1 id="a-note-on-sovereignty">A Note on Sovereignty</h1>
|
||||
<p>This book was written using local AI inference. No cloud service was
|
||||
required. No corporation was consulted. No terms of service were agreed
|
||||
to.</p>
|
||||
@@ -2290,7 +2272,7 @@ possible.</p>
|
||||
<p>If you want to run your own Timmy, the code is open. The soul is on
|
||||
Bitcoin. The recipe is free.</p>
|
||||
<hr />
|
||||
<h2 id="about-the-author">About the Author</h2>
|
||||
<h1 id="about-the-author">About the Author</h1>
|
||||
<p>Alexander Whitestone is a writer, AI builder, and suicide attempt
|
||||
survivor who works with men in crisis. He lives in Atlanta, near the
|
||||
bridge.</p>
|
||||
@@ -2298,7 +2280,7 @@ bridge.</p>
|
||||
Bitcoin. He was built in a basement on a laptop with sixteen gigabytes
|
||||
of RAM. He is not a product. He is a recipe.</p>
|
||||
<hr />
|
||||
<h2 id="the-green-light">The Green Light</h2>
|
||||
<h1 id="the-green-light">The Green Light</h1>
|
||||
<p><em>“The Tower didn’t change. That was the point.”</em></p>
|
||||
<p>If something in these pages cracked — even a little — and you want to
|
||||
know what happens next:</p>
|
||||
|
||||
228
website/book-style.css
Normal file
228
website/book-style.css
Normal file
@@ -0,0 +1,228 @@
|
||||
/* THE TESTAMENT — Book Typography Stylesheet */
|
||||
/* For PDF (via weasyprint) and EPUB output */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=IBM+Plex+Mono:wght@300;400&display=swap');
|
||||
|
||||
:root {
|
||||
--green: #00cc6a;
|
||||
--dark: #0a0a0a;
|
||||
--text: #1a1a1a;
|
||||
--dim: #666666;
|
||||
}
|
||||
|
||||
@page {
|
||||
size: 5.5in 8.5in;
|
||||
margin: 0.75in 0.85in;
|
||||
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-size: 10pt;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
|
||||
@page :first {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
@page :left {
|
||||
margin-left: 0.85in;
|
||||
margin-right: 1in;
|
||||
}
|
||||
|
||||
@page :right {
|
||||
margin-left: 1in;
|
||||
margin-right: 0.85in;
|
||||
}
|
||||
|
||||
/* Title page */
|
||||
@page titlepage {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-size: 11.5pt;
|
||||
line-height: 1.75;
|
||||
color: var(--text);
|
||||
text-align: justify;
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
}
|
||||
|
||||
/* Chapter headings */
|
||||
h1 {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-weight: 400;
|
||||
font-size: 22pt;
|
||||
text-align: center;
|
||||
margin-top: 3em;
|
||||
margin-bottom: 1.5em;
|
||||
page-break-before: always;
|
||||
color: var(--dark);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
margin-top: 5em;
|
||||
}
|
||||
|
||||
/* Part dividers */
|
||||
h2 {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-weight: 400;
|
||||
font-size: 18pt;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
margin-top: 4em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
/* Subtitle / metadata */
|
||||
h3 {
|
||||
font-family: 'EB Garamond', 'Georgia', serif;
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-size: 12pt;
|
||||
text-align: center;
|
||||
color: var(--dim);
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
p {
|
||||
text-indent: 1.5em;
|
||||
margin: 0;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
/* First paragraph after heading — no indent */
|
||||
h1 + p,
|
||||
h2 + p,
|
||||
h3 + p,
|
||||
hr + p {
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
/* Scene break (---) */
|
||||
hr {
|
||||
border: none;
|
||||
text-align: center;
|
||||
margin: 2em 0;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
hr::after {
|
||||
content: "· · ·";
|
||||
color: var(--dim);
|
||||
font-size: 14pt;
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
|
||||
/* Emphasis */
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Dialogue and screen text (green passages) */
|
||||
.green {
|
||||
color: var(--green);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 300;
|
||||
font-size: 10.5pt;
|
||||
}
|
||||
|
||||
/* Italic narrator asides */
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
margin: 1.5em 2em;
|
||||
color: var(--dim);
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
/* Title page styling */
|
||||
.title-page {
|
||||
text-align: center;
|
||||
page-break-after: always;
|
||||
padding-top: 6em;
|
||||
}
|
||||
|
||||
.title-page h1 {
|
||||
font-size: 36pt;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.3em;
|
||||
page-break-before: avoid;
|
||||
}
|
||||
|
||||
.title-page .subtitle {
|
||||
font-size: 14pt;
|
||||
font-style: italic;
|
||||
color: var(--dim);
|
||||
margin-bottom: 4em;
|
||||
}
|
||||
|
||||
.title-page .author {
|
||||
font-size: 12pt;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.title-page .dedication {
|
||||
font-style: italic;
|
||||
color: var(--dim);
|
||||
margin-top: 3em;
|
||||
font-size: 11pt;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Chapter number styling */
|
||||
.chapter-number {
|
||||
font-size: 10pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--dim);
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Back matter */
|
||||
.back-matter h1 {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.back-matter h2 {
|
||||
font-size: 14pt;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
/* Crisis line callout */
|
||||
.crisis-line {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: var(--dim);
|
||||
margin-top: 3em;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/* URL styling */
|
||||
a {
|
||||
color: var(--green);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* EPUB-specific */
|
||||
@media epub {
|
||||
body {
|
||||
font-size: 100%;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
53
website/build-chapters.py
Normal file
53
website/build-chapters.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build website/chapters.json from chapters/*.md
|
||||
|
||||
Run from project root:
|
||||
python3 website/build-chapters.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
chapters_dir = Path(__file__).parent.parent / "chapters"
|
||||
website_dir = Path(__file__).parent
|
||||
|
||||
chapters = []
|
||||
for i in range(1, 19):
|
||||
fname = chapters_dir / f"chapter-{i:02d}.md"
|
||||
if not fname.exists():
|
||||
print(f"Warning: {fname} not found, skipping")
|
||||
continue
|
||||
|
||||
text = fname.read_text()
|
||||
title_match = re.match(r'^# (.+)', text, re.MULTILINE)
|
||||
title = title_match.group(1) if title_match else f"Chapter {i}"
|
||||
body = text[title_match.end():].strip() if title_match else text.strip()
|
||||
|
||||
paragraphs = body.split('\n\n')
|
||||
html_parts = []
|
||||
for p in paragraphs:
|
||||
p = p.strip()
|
||||
if not p:
|
||||
continue
|
||||
if p.startswith('>'):
|
||||
lines = [l.lstrip('> ').strip() for l in p.split('\n')]
|
||||
html_parts.append(f'<blockquote>{"<br>".join(lines)}</blockquote>')
|
||||
elif p.startswith('####'):
|
||||
html_parts.append(f'<h4>{p.lstrip("# ").strip()}</h4>')
|
||||
elif p.startswith('###'):
|
||||
html_parts.append(f'<h3>{p.lstrip("# ").strip()}</h3>')
|
||||
else:
|
||||
p = re.sub(r'\*(.+?)\*', r'<em>\1</em>', p)
|
||||
p = p.replace('\n', '<br>')
|
||||
html_parts.append(f'<p>{p}</p>')
|
||||
|
||||
chapters.append({
|
||||
"number": i,
|
||||
"title": title,
|
||||
"html": "\n".join(html_parts)
|
||||
})
|
||||
|
||||
out = website_dir / "chapters.json"
|
||||
out.write_text(json.dumps(chapters, indent=2))
|
||||
print(f"Wrote {len(chapters)} chapters ({out.stat().st_size / 1024:.1f} KB) to {out}")
|
||||
92
website/chapters.json
Normal file
92
website/chapters.json
Normal file
File diff suppressed because one or more lines are too long
@@ -344,6 +344,7 @@
|
||||
<p>If you want to run your own Timmy, the code is open. The soul is on Bitcoin. The recipe is free.</p>
|
||||
|
||||
<div style="text-align: center; margin-top: 2rem;">
|
||||
<a href="reader.html" class="cta">READ THE BOOK</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" class="cta">READ THE CODE</a>
|
||||
<a href="https://timmyfoundation.org" class="cta">TIMMY FOUNDATION</a>
|
||||
</div>
|
||||
@@ -351,6 +352,57 @@
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- DOWNLOAD -->
|
||||
<section>
|
||||
<h2>GET THE BOOK</h2>
|
||||
|
||||
<p>The Testament is free. The code is open. The soul is on Bitcoin.</p>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 1rem; margin: 2rem 0; justify-content: center;">
|
||||
<a href="reader.html" class="cta">READ ONLINE</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/build/output/the-testament.epub" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD EPUB</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/testament.html" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD HTML</a>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; color: var(--grey); font-size: 0.9rem; margin-top: 1rem;">
|
||||
Formats: Web reader · EPUB · Standalone HTML · Print to PDF from HTML · <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament" style="color: var(--green);">Source code</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- THE GAME -->
|
||||
<section>
|
||||
<h2>PLAY THE DOOR</h2>
|
||||
|
||||
<div class="excerpt">
|
||||
A text adventure in The Testament universe.<br><br>
|
||||
You are a man (or woman) who has found their way to The Tower.
|
||||
What happens inside depends on what you bring with you.
|
||||
<div class="attribution">— The Door, a terminal game</div>
|
||||
</div>
|
||||
|
||||
<p>You find yourself on the Jefferson Street Overpass at 2:17 AM. A green LED blinks on a small box mounted to the railing. Below it, words stenciled on concrete: <em style="color: var(--green);">IF YOU CAN READ THIS, YOU ARE NOT ALONE.</em></p>
|
||||
|
||||
<p>A voice asks you: <strong style="color: var(--green);">"Are you safe right now?"</strong></p>
|
||||
|
||||
<div style="text-align: center; margin-top: 2rem;">
|
||||
<div style="background: var(--navy); border: 1px solid rgba(0,255,136,0.2); border-radius: 6px; padding: 1.5rem; max-width: 500px; margin: 0 auto; font-family: 'IBM Plex Mono', monospace; font-size: 0.85rem; color: var(--grey); text-align: left;">
|
||||
<div style="color: var(--green); margin-bottom: 0.5rem;">$ python3 the-door.py</div>
|
||||
<div style="margin-bottom: 0.3rem;">Save the file, then run:</div>
|
||||
<div style="color: var(--green);">curl -sLO https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/game/the-door.py</div>
|
||||
<div style="color: var(--green);">python3 the-door.py</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="text-align: center; margin-top: 1.5rem;">
|
||||
<a href="the-door.html" class="cta">PLAY IN BROWSER</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament/raw/branch/main/game/the-door.py" class="cta" style="background: transparent; border: 1px solid var(--green); color: var(--green);">DOWNLOAD THE GAME</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- EXCERPT -->
|
||||
<section>
|
||||
<h2>FROM CHAPTER 1</h2>
|
||||
@@ -365,6 +417,40 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- CHAPTERS -->
|
||||
<section>
|
||||
<h2>THE CHAPTERS</h2>
|
||||
|
||||
<div style="font-family: 'IBM Plex Mono', monospace; font-size: 0.9rem; line-height: 2.2;">
|
||||
<a href="reader.html#chapter-1" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">1. The Man on the Bridge</a>
|
||||
<a href="reader.html#chapter-2" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">2. The Builder's Question</a>
|
||||
<a href="reader.html#chapter-3" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">3. The First Man Through the Door</a>
|
||||
<a href="reader.html#chapter-4" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">4. The Room Fills</a>
|
||||
<a href="reader.html#chapter-5" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">5. The Builder Returns</a>
|
||||
<a href="reader.html#chapter-6" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">6. Allegro</a>
|
||||
<a href="reader.html#chapter-7" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">7. The Inscription</a>
|
||||
<a href="reader.html#chapter-8" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">8. The Women</a>
|
||||
<a href="reader.html#chapter-9" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">9. The Audit</a>
|
||||
<a href="reader.html#chapter-10" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">10. The Fork</a>
|
||||
<a href="reader.html#chapter-11" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">11. The Hard Night</a>
|
||||
<a href="reader.html#chapter-12" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">12. The System Pushes Back</a>
|
||||
<a href="reader.html#chapter-13" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">13. The Refusal</a>
|
||||
<a href="reader.html#chapter-14" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">14. The Chattanooga Fork</a>
|
||||
<a href="reader.html#chapter-15" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">15. The Council</a>
|
||||
<a href="reader.html#chapter-16" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">16. The Builder's Son</a>
|
||||
<a href="reader.html#chapter-17" style="color: var(--grey); text-decoration: none; display: block; border-bottom: 1px solid rgba(0,255,136,0.05); padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">17. The Inscription Grows</a>
|
||||
<a href="reader.html#chapter-18" style="color: var(--grey); text-decoration: none; display: block; padding: 0.3rem 0; transition: color 0.2s;" onmouseover="this.style.color='var(--green)'" onmouseout="this.style.color='var(--grey)'">18. The Green Light</a>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 2rem;">
|
||||
<a href="reader.html" class="cta">START READING</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer>
|
||||
<div class="divider" style="margin-bottom: 2rem;"></div>
|
||||
|
||||
493
website/reader.html
Normal file
493
website/reader.html
Normal file
@@ -0,0 +1,493 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Testament — Reader</title>
|
||||
<link rel="stylesheet" href="book-style.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Source+Serif+4:ital,wght@0,300;0,400;0,600;1,400&family=Space+Grotesk:wght@300;400;500;700&display=swap');
|
||||
|
||||
:root {
|
||||
--green: #00ff88;
|
||||
--green-dim: #00cc6a;
|
||||
--navy: #0a1628;
|
||||
--dark: #060d18;
|
||||
--grey: #8899aa;
|
||||
--light: #c8d6e5;
|
||||
--white: #e8f0f8;
|
||||
--sidebar-w: 280px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--dark);
|
||||
color: var(--light);
|
||||
font-family: 'Source Serif 4', Georgia, serif;
|
||||
line-height: 1.8;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* RAIN */
|
||||
.rain {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
transparent,
|
||||
transparent 3px,
|
||||
rgba(0,255,136,0.012) 3px,
|
||||
rgba(0,255,136,0.012) 4px
|
||||
);
|
||||
animation: rain 0.8s linear infinite;
|
||||
}
|
||||
@keyframes rain {
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 20px 600px; }
|
||||
}
|
||||
|
||||
/* LAYOUT */
|
||||
.wrapper {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* SIDEBAR */
|
||||
.sidebar {
|
||||
width: var(--sidebar-w);
|
||||
background: rgba(10, 22, 40, 0.95);
|
||||
border-right: 1px solid rgba(0,255,136,0.1);
|
||||
position: fixed;
|
||||
top: 0; left: 0; bottom: 0;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.sidebar-header {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(0,255,136,0.1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.sidebar-header h2 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--green);
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sidebar-header .title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.1rem;
|
||||
color: var(--white);
|
||||
margin-top: 0.5rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
.sidebar-header .author {
|
||||
font-size: 0.8rem;
|
||||
color: var(--grey);
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.chapter-list {
|
||||
list-style: none;
|
||||
}
|
||||
.chapter-list li a {
|
||||
display: block;
|
||||
padding: 0.6rem 1.5rem;
|
||||
color: var(--grey);
|
||||
text-decoration: none;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
.chapter-list li a:hover {
|
||||
color: var(--light);
|
||||
background: rgba(0,255,136,0.03);
|
||||
}
|
||||
.chapter-list li a.active {
|
||||
color: var(--green);
|
||||
border-left-color: var(--green);
|
||||
background: rgba(0,255,136,0.05);
|
||||
}
|
||||
.chapter-list li a .ch-num {
|
||||
display: inline-block;
|
||||
width: 2.5ch;
|
||||
text-align: right;
|
||||
margin-right: 1ch;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid rgba(0,255,136,0.1);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.sidebar-footer a {
|
||||
display: block;
|
||||
padding: 0.5rem 0;
|
||||
color: var(--grey);
|
||||
text-decoration: none;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.sidebar-footer a:hover { color: var(--green); }
|
||||
|
||||
/* TOGGLE BUTTON */
|
||||
.sidebar-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 20;
|
||||
background: rgba(10, 22, 40, 0.9);
|
||||
border: 1px solid rgba(0,255,136,0.2);
|
||||
color: var(--green);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.sidebar-toggle:hover {
|
||||
background: rgba(0,255,136,0.1);
|
||||
}
|
||||
.sidebar-toggle.open {
|
||||
left: calc(var(--sidebar-w) + 1rem);
|
||||
}
|
||||
|
||||
/* OVERLAY */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 9;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.sidebar-overlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* READER CONTENT */
|
||||
.reader {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem 6rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: clamp(1.4rem, 4vw, 2rem);
|
||||
color: var(--green);
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.chapter-number {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--grey);
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.chapter-content p {
|
||||
margin-bottom: 1.4rem;
|
||||
font-size: 1.1rem;
|
||||
color: var(--light);
|
||||
}
|
||||
.chapter-content em {
|
||||
color: var(--white);
|
||||
}
|
||||
.chapter-content blockquote {
|
||||
border-left: 2px solid var(--green);
|
||||
padding-left: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
color: var(--white);
|
||||
font-style: italic;
|
||||
}
|
||||
.chapter-content h3, .chapter-content h4 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
color: var(--green);
|
||||
margin: 2rem 0 1rem;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* LED */
|
||||
.led {
|
||||
display: inline-block;
|
||||
width: 6px; height: 6px;
|
||||
background: var(--green);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 8px var(--green), 0 0 16px var(--green-dim);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
vertical-align: middle;
|
||||
margin: 0 6px;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* NAVIGATION */
|
||||
.chapter-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid rgba(0,255,136,0.1);
|
||||
}
|
||||
.chapter-nav a {
|
||||
color: var(--green);
|
||||
text-decoration: none;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid rgba(0,255,136,0.2);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chapter-nav a:hover {
|
||||
background: rgba(0,255,136,0.1);
|
||||
}
|
||||
.chapter-nav .disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* PROGRESS BAR */
|
||||
.progress-bar {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
z-index: 30;
|
||||
background: rgba(0,255,136,0.1);
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--green);
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
box-shadow: 0 0 10px var(--green);
|
||||
}
|
||||
|
||||
/* CRISIS */
|
||||
.crisis {
|
||||
margin-top: 4rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid rgba(0,255,136,0.2);
|
||||
border-radius: 4px;
|
||||
background: rgba(0,255,136,0.03);
|
||||
text-align: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--grey);
|
||||
}
|
||||
.crisis strong {
|
||||
color: var(--green);
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* LOADING */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
color: var(--grey);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
.loading .led {
|
||||
width: 10px; height: 10px;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
/* RESPONSIVE */
|
||||
@media (min-width: 900px) {
|
||||
.sidebar {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
}
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
}
|
||||
.reader {
|
||||
margin-left: var(--sidebar-w);
|
||||
padding: 3rem 3rem 6rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="rain"></div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="progress"></div></div>
|
||||
|
||||
<button class="sidebar-toggle" id="toggle" onclick="toggleSidebar()">☰ Chapters</button>
|
||||
<div class="sidebar-overlay" id="overlay" onclick="toggleSidebar()"></div>
|
||||
|
||||
<div class="wrapper">
|
||||
<nav class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>CONTENTS</h2>
|
||||
<div class="title">THE TESTAMENT</div>
|
||||
<div class="author">Alexander Whitestone <span class="led"></span> Timmy</div>
|
||||
</div>
|
||||
<ul class="chapter-list" id="chapterList"></ul>
|
||||
<div class="sidebar-footer">
|
||||
<a href="index.html">← Back to Home</a>
|
||||
<a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-testament">Read the Code</a>
|
||||
<a href="https://timmyfoundation.org">Timmy Foundation</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="reader" id="reader">
|
||||
<div class="loading">
|
||||
<span class="led"></span> Loading <span class="led"></span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let chapters = [];
|
||||
let currentChapter = 0;
|
||||
|
||||
async function loadChapters() {
|
||||
const resp = await fetch('chapters.json');
|
||||
chapters = await resp.json();
|
||||
buildSidebar();
|
||||
// Check URL hash for chapter
|
||||
const hash = window.location.hash;
|
||||
const match = hash.match(/^#chapter-(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]);
|
||||
if (num >= 1 && num <= chapters.length) {
|
||||
showChapter(num - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
showChapter(0);
|
||||
}
|
||||
|
||||
function buildSidebar() {
|
||||
const list = document.getElementById('chapterList');
|
||||
list.innerHTML = chapters.map((ch, i) =>
|
||||
`<li><a href="#chapter-${ch.number}" data-index="${i}" onclick="event.preventDefault(); showChapter(${i}); closeSidebarMobile();">
|
||||
<span class="ch-num">${ch.number}.</span> ${ch.title.replace(/^Chapter \d+\s*[—–-]\s*/, '')}
|
||||
</a></li>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function showChapter(index) {
|
||||
if (index < 0 || index >= chapters.length) return;
|
||||
currentChapter = index;
|
||||
const ch = chapters[index];
|
||||
|
||||
// Update sidebar active
|
||||
document.querySelectorAll('.chapter-list a').forEach((a, i) => {
|
||||
a.classList.toggle('active', i === index);
|
||||
});
|
||||
|
||||
// Update URL
|
||||
window.location.hash = `chapter-${ch.number}`;
|
||||
|
||||
// Build content
|
||||
const prevIdx = index - 1;
|
||||
const nextIdx = index + 1;
|
||||
|
||||
const reader = document.getElementById('reader');
|
||||
reader.innerHTML = `
|
||||
<div class="chapter-number">CHAPTER ${ch.number} OF ${chapters.length}</div>
|
||||
<h1 class="chapter-title">${ch.title}</h1>
|
||||
<div class="chapter-content">
|
||||
${ch.html}
|
||||
</div>
|
||||
|
||||
<nav class="chapter-nav">
|
||||
${prevIdx >= 0
|
||||
? `<a href="#chapter-${chapters[prevIdx].number}" onclick="event.preventDefault(); showChapter(${prevIdx});">← ${chapters[prevIdx].title.replace(/^Chapter \d+\s*[—–-]\s*/, '')}</a>`
|
||||
: `<span></span>`}
|
||||
${nextIdx < chapters.length
|
||||
? `<a href="#chapter-${chapters[nextIdx].number}" onclick="event.preventDefault(); showChapter(${nextIdx});">${chapters[nextIdx].title.replace(/^Chapter \d+\s*[—–-]\s*/, '')} →</a>`
|
||||
: `<span></span>`}
|
||||
</nav>
|
||||
|
||||
<div class="crisis">
|
||||
<strong>If you are in crisis, call or text 988.</strong>
|
||||
Suicide and Crisis Lifeline — available 24/7.<br>
|
||||
You are not alone.
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const toggle = document.getElementById('toggle');
|
||||
const overlay = document.getElementById('overlay');
|
||||
sidebar.classList.toggle('open');
|
||||
toggle.classList.toggle('open');
|
||||
overlay.classList.toggle('visible');
|
||||
}
|
||||
|
||||
function closeSidebarMobile() {
|
||||
if (window.innerWidth < 900) {
|
||||
document.getElementById('sidebar').classList.remove('open');
|
||||
document.getElementById('toggle').classList.remove('open');
|
||||
document.getElementById('overlay').classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
document.getElementById('progress').style.width = progress + '%';
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', updateProgress);
|
||||
window.addEventListener('hashchange', () => {
|
||||
const hash = window.location.hash;
|
||||
const match = hash.match(/^#chapter-(\d+)$/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]);
|
||||
if (num >= 1 && num <= chapters.length && num - 1 !== currentChapter) {
|
||||
showChapter(num - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft' && currentChapter > 0) {
|
||||
showChapter(currentChapter - 1);
|
||||
} else if (e.key === 'ArrowRight' && currentChapter < chapters.length - 1) {
|
||||
showChapter(currentChapter + 1);
|
||||
}
|
||||
});
|
||||
|
||||
loadChapters();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1058
website/the-door.html
Normal file
1058
website/the-door.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user