"""Export a book PDF to text and split it into per-chapter source-text.md files. Usage: python tools/split_book.py "" Reads library//source-file.pdf and writes library///-source-text.md for each section defined in SECTIONS below (add an entry for a new book). Sections are (folder, title, first_page, last_page, note) with 1-based PDF pages. Pages not covered by any section are reported. """ import re import sys from pathlib import Path import fitz # PyMuPDF ROOT = Path(__file__).resolve().parent.parent / "library" YBYE_NOTE_AFTER = "Includes the Step action plan that closes this step." YBYE_NOTE_BEFORE = "Includes the Step introduction page that precedes the chapter." def ch(n): return f"Chapter-{n:02d}" def slug(title, limit=60): """Title -> folder-name slug (see AGENTS.md Naming Convention).""" t = re.sub(r"^\d+(\.\d+)?[a-z]?\s*-\s*", "", title.strip()) # drop the book's own numbering prefix t = re.sub(r"[:,.'\"‘’“”?/\()]", "", t) t = re.sub(r"[^\w\s-]", "", t) words, out = t.split(), "" for w in words: if len(out) + len(w) + (1 if out else 0) > limit: break out = f"{out}-{w}" if out else w return re.sub(r"-+", "-", out) or "Untitled" def folder_name(key, title): """Chapter-05 + title -> Chapter-05-Anatomy-of-a-Specification; Front/Back-Matter keep their name.""" return key if key in ("Front-Matter", "Back-Matter") else f"{key}-{slug(title)}" def build_cetx(): d = fitz.open(ROOT / "Context Engineering" / "source-file.pdf") top = [(t, p) for lvl, t, p in d.get_toc() if lvl == 1] secs = [("Front-Matter", "Front matter", 1, top[0][1] - 1, "")] for i, (t, p) in enumerate(top): end = (top[i + 1][1] - 1) if i + 1 < len(top) else len(d) secs.append((ch(i + 1), t, p, end, "")) return secs def build_12wy(): d = fitz.open(ROOT / "The 12 Week Year" / "source-file.pdf") starts = [] for lvl, t, p in d.get_toc(): if t.startswith("Chapter ") and lvl in (1, 2): num = int(t.split(":")[0].split()[1]) starts.append((num, t.split(":", 1)[1].strip(), p)) secs = [("Front-Matter", "Front matter", 1, starts[0][2] - 1, "")] for i, (n, t, p) in enumerate(starts): end = starts[i + 1][2] - 1 if i + 1 < len(starts) else 168 note = "" if n == 1: note = "Includes the Part I divider page that follows the chapter." secs.append((ch(n), t, p, end, note)) secs.append(("Back-Matter", "References", 169, len(d), "")) return secs def build_ybye(): chapters = [ # (n, title, first, last, note) (0, "Your Best Is Yet to Come (Opening)", 11, 21, ""), (1, "Your Beliefs Shape Your Reality", 22, 29, YBYE_NOTE_BEFORE), (2, "Some Beliefs Hold You Back", 30, 38, ""), (3, "You Can Upgrade Your Beliefs", 39, 53, YBYE_NOTE_AFTER), (4, "Thinking Backward Is a Must", 54, 64, YBYE_NOTE_BEFORE), (5, "Regret Reveals Opportunity", 65, 73, ""), (6, "Gratitude Makes the Difference", 74, 82, YBYE_NOTE_AFTER), (7, "Great Goals Check Seven Boxes", 83, 102, YBYE_NOTE_BEFORE), (8, "Achievements and Habits Work Together", 103, 109, ""), (9, "Seriously, Risk Is Your Friend", 110, 124, YBYE_NOTE_AFTER), (10, "Your What Needs a Why", 125, 139, YBYE_NOTE_BEFORE), (11, "You Can Master Your Own Motivation", 140, 148, ""), (12, "The Journey Is Better with Friends", 149, 160, YBYE_NOTE_AFTER), (13, "One Journey Is Many Steps", 161, 170, YBYE_NOTE_BEFORE), (14, "You Can Trigger Success", 171, 177, ""), (15, "Visibility Is Essential", 178, 190, YBYE_NOTE_AFTER), ] secs = [("Front-Matter", "Front matter (title, copyright, endorsements, contents)", 1, 10, "")] secs += [(ch(n), t, a, b, note) for n, t, a, b, note in chapters] secs.append(("Back-Matter", "LEAP Principle, Sample Goal Templates, Notes, Thanks, Index, About the Author", 191, 235, "")) return secs # pages 236-242 (ads, back cover) are intentionally excluded def parse_table(book, first_col_prefix, skip_rows=0): rows = [] for line in (ROOT / book / "book-structure.md").read_text(encoding="utf-8").splitlines(): if line.startswith("|") and not line.startswith("| ---") and not line.startswith("| Reading"): rows.append([c.strip() for c in line.strip("|").split("|")]) return rows def build_sdd(): d = fitz.open(ROOT / "Spec Driven Development" / "source-file.pdf") secs = [("Front-Matter", "Front matter (cover, title, contents)", 1, 10, "")] for r in parse_table("Spec Driven Development", ""): n = int(r[0].split("-")[1]) a, b = r[1].replace("–", "-").split("-") secs.append((ch(n), r[2], int(a), int(b), "")) return secs def build_focus(): d = fitz.open(ROOT / "FOCUS Architecture" / "source-file.pdf") rows = [] for r in parse_table("FOCUS Architecture", ""): rows.append((r[0], r[1], int(r[2]))) secs = [("Front-Matter", "Front matter", 1, rows[0][2] - 1, "")] for i, (kind, title, start) in enumerate(rows): end = rows[i + 1][2] - 1 if i + 1 < len(rows) else len(d) folder = "Interlude" if kind == "Interlude" else ch(int(kind.split()[1])) secs.append((folder, title, start, end, "")) return secs BUILDERS = { "Context Engineering": build_cetx, "The 12 Week Year": build_12wy, "Your Best Year Ever": build_ybye, "Spec Driven Development": build_sdd, "FOCUS Architecture": build_focus, } def main(book): pdf = ROOT / book / "source-file.pdf" doc = fitz.open(pdf) secs = BUILDERS[book]() covered = {} for folder, title, a, b, note in secs: pages, blank = [], [] for p in range(a, b + 1): if p in covered: raise SystemExit(f"page {p} in both {covered[p]} and {folder}") covered[p] = folder text = doc[p - 1].get_text().strip() if len(text) < 30: blank.append(p) pages.append(f"\n{text}\n") header = [ f"# {book} — {folder}: {title}", f"- **Source**: /library/{book}/source-file.pdf", f"- **PDF pages**: {a}–{b}", f"- **Pages without text**: {', '.join(map(str, blank)) if blank else 'none'}", ] if note: header.append(f"- **Note**: {note}") out = ROOT / book / folder_name(folder, title) out.mkdir(exist_ok=True) (out / f"{folder}-source-text.md").write_text("\n".join(header) + "\n\n---\n\n" + "\n".join(pages), encoding="utf-8", newline="\n") print(f"{folder:14} pp {a}-{b} blank={blank} {title[:50]}") missing = [p for p in range(1, len(doc) + 1) if p not in covered] print("pages not covered:", missing) if __name__ == "__main__": main(sys.argv[1])