"""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 def build_inference(): d = fitz.open(ROOT / "Inference Engineering" / "source-file.pdf") top = [(t, p) for lvl, t, p in d.get_toc() if lvl <= 2] # Skip the Table of Contents bookmark: it is front matter (pages 1-10 with the cover and TOC). top = [(t, p) for t, p in top if t != "Table of Contents"] last_body = next(p for t, p in top if t == "Acknowledgements") secs = [("Front-Matter", "Front matter (cover, title, contents)", 1, top[0][1] - 1, "")] body = [(t, p) for t, p in top if p < last_body] for i, (t, p) in enumerate(body): end = (body[i + 1][1] - 1) if i + 1 < len(body) else last_body - 1 title = re.sub(r"^Chapter \d+:\s*", "", t) secs.append((ch(i + 1), title, p, end, "")) secs.append(("Back-Matter", "Acknowledgements", last_body, len(d), "")) return secs def build_os32(): # Scanned book (no bookmarks): printed page N is PDF page N + 22. Chapters that open a Part # take in the Part divider plate and its blank verso, two pages before the chapter plate. chapters = [ # (n, title, printed start page, part that opens here) (1, "Overview", 3, "I"), (2, "General Discussion and Background", 13, ""), (3, "The Tasking Model", 25, "II"), (4, "Interprocess Communications", 41, ""), (5, "Memory Management", 55, ""), (6, "The Hardware Interface", 81, ""), (7, "OS Initialization", 99, ""), (8, "Programming Interfaces", 115, ""), (9, "Application Programming", 129, "III"), (10, "Systems Programming", 141, ""), (11, "The MMURTL Monitor Program", 159, ""), (12, "The Debugger", 181, ""), (13, "Keyboard Service", 189, ""), (14, "File System Service", 201, ""), (15, "API Specification", 221, ""), (16, "MMURTL Sample Software", 265, ""), (17, "Introduction to the Source Code", 327, "IV"), (18, "Kernel Code", 335, ""), (19, "Memory Management Code", 377, ""), (20, "Timer Management Code", 405, ""), (21, "Initialization Code", 419, ""), (22, "Job Management Code", 439, ""), (23, "Debugger Code", 471, ""), (24, "Selected Device Driver Code", 497, ""), (25, "Keyboard Code", 541, ""), (26, "Video Code", 571, ""), (27, "File System Code", 591, ""), (28, "DASM: A 32-Bit Intel-Based Assembler", 663, "V"), (29, "CM32: A 32-Bit C Compiler", 697, ""), ] index_start = 719 + 22 starts = [(n, t, s + 22 - (2 if part else 0), part) for n, t, s, part in chapters] secs = [("Front-Matter", "Front matter (covers, title, copyright, contents, preface, acknowledgments, about the author)", 1, starts[0][2] - 1, "")] for i, (n, t, a, part) in enumerate(starts): end = starts[i + 1][2] - 1 if i + 1 < len(starts) else index_start - 1 note = f"Includes the Part {part} divider plate and its blank verso before the chapter." if part else "" secs.append((ch(n), t, a, end, note)) secs.append(("Back-Matter", "Index, publisher ads, CD-ROM installation and contents, back cover", index_start, 772, "")) return secs BUILDERS = { "Developing Your Own 32-Bit Operating System": build_os32, "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, "Inference Engineering": build_inference, } 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])