|
- """Export a book PDF to text and split it into per-chapter source-text.md files.
-
- Usage: python tools/split_book.py "<Book Title>"
-
- Reads library/<Book Title>/source-file.pdf and writes
- library/<Book Title>/<Chapter-XX-Title-Slug>/<Chapter-XX>-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"<!-- PDF page {p} -->\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])
|