brain backup 2026-09-14

This commit is contained in:
Adolfo Reyna
2026-09-14 22:38:47 -04:00
parent ca346cc6e7
commit f0ead4f6fa
147 changed files with 73451 additions and 8 deletions
+470
View File
@@ -0,0 +1,470 @@
#!/usr/bin/env python3
"""Render the Bryan Parent books as linked PDFs.
The renderer intentionally keeps Markdown as the source of truth. It produces:
- editorial: comments and editorial review sections remain visible;
- clean: HTML comments, editorial review sections, and development-theme notes
are removed for a distraction-free reading edition.
Chrome's print engine preserves same-document anchors, so the generated PDF has
clickable contents, chapter maps, and repeated "Contents" links.
"""
from __future__ import annotations
import argparse
import html
import json
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_SOURCE = ROOT / "projects/covenant_parenting_bryan_hickman_revised.md"
DEFAULT_OUT = ROOT / "exports"
CHROME = Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
PARENT_REVIEW_CONTRACT = ROOT / "tools" / "parent_review_contract.json"
def validate_parent_review_contract(markdown: str) -> None:
"""Fail closed if the parent-review transformation lost requested semantics."""
contract = json.loads(PARENT_REVIEW_CONTRACT.read_text(encoding="utf-8"))
headings = [line[3:].strip() for line in markdown.splitlines() if line.startswith("## ")]
for prefix in contract["required_main_chapter_prefixes"]:
if not any(heading.startswith(prefix) for heading in headings):
raise ValueError(f"Parent-review contract requires main heading {prefix!r}")
if contract["required_workbook_heading"] not in markdown:
raise ValueError("Parent-review contract requires the Reflection and Action workbook placement")
for forbidden in contract["excluded_content"]:
if forbidden in markdown:
raise ValueError(f"Parent-review contract forbids excluded content {forbidden!r}")
EDITORIAL_SECTION = re.compile(
r"^(?:Development themes|Themes captured for future (?:book|workbook) development|"
r"Editorial(?:, pastoral, developmental, and safeguarding| and pastoral)? review(?: flags)?|"
r"Editorial, pastoral, developmental, and safeguarding review)$",
re.I,
)
def parse_frontmatter(text: str) -> tuple[dict[str, str], str]:
if not text.startswith("---\n"):
return {}, text
end = text.find("\n---", 4)
if end < 0:
return {}, text
raw = text[4:end].strip("\n")
data: dict[str, str] = {}
for line in raw.splitlines():
if ":" in line and not line.startswith(" "):
key, value = line.split(":", 1)
data[key.strip()] = value.strip().strip('"')
return data, text[end + 4 :].lstrip("\n")
def clean_markdown(text: str, mode: str) -> str:
"""Remove or preserve editorial material before Markdown conversion."""
# The parent-review edition deliberately retains its flowing opening.
keep_opening = "<!-- KEEP_PARENT_OPENING -->" in text
# Normalize comments first. In editorial mode we retain a parser token so
# the note is rendered as a visible callout rather than as raw HTML.
if mode == "clean":
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
else:
text = re.sub(
r"<!--\s*EDITORIAL COMMENT:\s*(.*?)\s*-->",
lambda m: "@@EDITORIAL_COMMENT " + re.sub(r"\s+", " ", m.group(1)).strip() + "@@",
text,
flags=re.S | re.I,
)
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
if mode != "clean":
return text
# Drop editorial-only sections, but keep the surrounding teaching. A
# heading at level 3 owns content until the next heading at level 3 or 2.
lines = text.splitlines()
output: list[str] = []
skip_level: int | None = None
for line in lines:
heading = re.match(r"^(#{1,6})\s+(.*?)\s*$", line)
if heading:
level = len(heading.group(1))
title = heading.group(2).strip().strip("# ")
if skip_level is not None and level <= skip_level:
skip_level = None
if level == 3 and EDITORIAL_SECTION.match(title):
skip_level = 3
continue
if skip_level is None:
output.append(line)
# A clean reading edition begins with the first chapter, not the revised
# draft's provenance, editorial-status, and Scripture-audit preamble.
first_chapter = next(
(idx for idx, line in enumerate(output) if re.match(r"^##\s+", line)),
None,
)
if first_chapter is not None and not keep_opening:
output = output[first_chapter:]
# NotebookLM source-reference markers such as [1] and [2, 48]
# belong in the archival/editorial material, not in the reading edition.
output_text = "\n".join(output)
output_text = re.sub(r"\s*\[\s*\d+(?:\s*,\s*\d+)*\s*\]", "", output_text)
return output_text
def inline_markdown(value: str) -> str:
"""Small, safe inline Markdown subset used by this book."""
# Preserve URLs before escaping, then restore link markup.
placeholders: list[str] = []
def stash(fragment: str) -> str:
placeholders.append(fragment)
return f"\x00{len(placeholders) - 1}\x00"
value = re.sub(
r"\[([^\]]+)\]\((https?://[^\s)]+|#[^\s)]+)\)",
lambda m: stash(f'<a href="{html.escape(m.group(2), quote=True)}">{html.escape(m.group(1))}</a>'),
value,
)
value = re.sub(
r"\[\[([^\]]+)\]\]",
lambda m: html.escape(m.group(1).replace("_", " ")),
value,
)
value = html.escape(value, quote=False)
value = re.sub(r"`([^`]+)`", r"<code>\1</code>", value)
value = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", value)
value = re.sub(r"__([^_]+)__", r"<strong>\1</strong>", value)
value = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", value)
value = re.sub(r"(?<!_)_([^_]+)_(?!_)", r"<em>\1</em>", value)
value = value.replace(" ", "<br>")
for i, fragment in enumerate(placeholders):
value = value.replace(f"\x00{i}\x00", fragment)
return value
def slugify(text: str, used: set[str]) -> str:
plain = re.sub(r"[*_`]+", "", text)
plain = re.sub(r"[^\w\s-]", "", plain, flags=re.UNICODE).strip().lower()
slug = re.sub(r"[-\s]+", "-", plain) or "section"
base = slug
n = 2
while slug in used:
slug = f"{base}-{n}"
n += 1
used.add(slug)
return slug
def is_table_line(line: str) -> bool:
return line.lstrip().startswith("|") and line.rstrip().endswith("|")
def table_html(lines: list[str]) -> str:
rows = []
for line in lines:
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if all(re.fullmatch(r":?-+:?", c) for c in cells):
continue
rows.append(cells)
if not rows:
return ""
out = ["<table><thead><tr>"]
for cell in rows[0]:
out.append(f"<th>{inline_markdown(cell)}</th>")
out.append("</tr></thead><tbody>")
for row in rows[1:]:
out.append("<tr>" + "".join(f"<td>{inline_markdown(c)}</td>" for c in row) + "</tr>")
out.append("</tbody></table>")
return "".join(out)
def headings_from_markdown(text: str) -> list[tuple[int, str, str]]:
used: set[str] = set()
found = []
for line in text.splitlines():
m = re.match(r"^(#{1,6})\s+(.*?)\s*$", line)
if not m:
continue
level = len(m.group(1))
title = m.group(2).strip().strip("# ")
if level == 1:
continue
found.append((level, title, slugify(title, used)))
return found
def markdown_to_body(text: str, mode: str, source_dir: Path) -> tuple[str, list[tuple[int, str, str]]]:
headings = headings_from_markdown(text)
heading_iter = iter(headings)
used: set[str] = set()
lines = text.splitlines()
chunks: list[str] = []
i = 0
in_code = False
code_lines: list[str] = []
while i < len(lines):
line = lines[i]
if line.startswith("```"):
if in_code:
chunks.append(f"<pre><code>{html.escape(chr(10).join(code_lines))}</code></pre>")
code_lines = []
in_code = False
else:
in_code = True
i += 1
continue
if in_code:
code_lines.append(line)
i += 1
continue
if not line.strip():
i += 1
continue
if line.startswith("@@EDITORIAL_COMMENT ") and line.endswith("@@"):
if mode == "editorial":
note = line[len("@@EDITORIAL_COMMENT ") : -2].strip()
chunks.append(f'<aside class="editorial-comment"><b>Editorial comment</b><br>{inline_markdown(note)}</aside>')
i += 1
continue
image = re.match(r"^!\[([^\]]*)\]\(([^)]+)\)\s*$", line)
if image:
alt, target = image.groups()
image_path = (source_dir / target).resolve()
if not image_path.is_file():
raise FileNotFoundError(f"Image not found: {image_path}")
chunks.append(
f'<figure class="content-figure"><img src="{image_path.as_uri()}" alt="{html.escape(alt, quote=True)}">'
f'<figcaption>{inline_markdown(alt)}</figcaption></figure>'
)
i += 1
continue
m = re.match(r"^(#{1,6})\s+(.*?)\s*$", line)
if m:
level = len(m.group(1))
title = m.group(2).strip().strip("# ")
if level == 1:
i += 1
continue
anchor = slugify(title, used)
if level == 2:
chunks.append(f'<div class="chapter-start" id="{anchor}"></div>')
chunks.append(f'<h{level} id="{anchor}">{inline_markdown(title)}</h{level}>')
# The Contents link is retained without a distracting per-chapter outline.
if level == 2:
chunks.append('<div class="chapter-back"><a href="#contents">Back to Contents</a></div>')
i += 1
next(heading_iter, None)
continue
if re.match(r"^[-*_]{3,}\s*$", line):
chunks.append("<hr>")
i += 1
continue
if is_table_line(line):
table_lines = []
while i < len(lines) and is_table_line(lines[i]):
table_lines.append(lines[i])
i += 1
chunks.append(table_html(table_lines))
continue
if re.match(r"^>\s?", line):
q = []
while i < len(lines) and re.match(r"^>\s?", lines[i]):
q.append(re.sub(r"^>\s?", "", lines[i]))
i += 1
chunks.append(f'<blockquote>{inline_markdown(chr(10).join(q))}</blockquote>')
continue
if re.match(r"^\s*[-*+]\s+", line):
items = []
while i < len(lines) and re.match(r"^\s*[-*+]\s+", lines[i]):
items.append(re.sub(r"^\s*[-*+]\s+", "", lines[i]))
i += 1
chunks.append("<ul>" + "".join(f"<li>{inline_markdown(x)}</li>" for x in items) + "</ul>")
continue
if re.match(r"^\s*\d+[.)]\s+", line):
items = []
while i < len(lines) and re.match(r"^\s*\d+[.)]\s+", lines[i]):
items.append(re.sub(r"^\s*\d+[.)]\s+", "", lines[i]))
i += 1
chunks.append("<ol>" + "".join(f"<li>{inline_markdown(x)}</li>" for x in items) + "</ol>")
continue
# Paragraph: collect until a block boundary.
para = [line.strip()]
i += 1
while i < len(lines) and lines[i].strip():
nxt = lines[i]
if (re.match(r"^(#{1,6})\s+", nxt) or re.match(r"^>\s?", nxt)
or re.match(r"^\s*[-*+]\s+", nxt) or re.match(r"^\s*\d+[.)]\s+", nxt)
or re.match(r"^[-*_]{3,}\s*$", nxt) or is_table_line(nxt)
or nxt.startswith("@@EDITORIAL_COMMENT ")):
break
para.append(nxt.strip())
i += 1
chunks.append(f"<p>{inline_markdown(chr(10).join(para))}</p>")
return "\n".join(chunks), headings
CSS = r"""
@page { size: 6in 9in; margin: .70in .65in .70in .75in; }
* { box-sizing: border-box; }
html { background: #e9e7e1; }
body { margin: 0; color: #25231f; background: #fffdf8; font-family: Georgia, 'Times New Roman', serif; font-size: 10.1pt; line-height: 1.40; }
a { color: #315d75; text-decoration: underline; }
.cover { height: 7.55in; display: flex; flex-direction: column; justify-content: center; text-align: center; page-break-after: always; }
.cover h1 { font-size: 28pt; line-height: 1.08; margin: 0 auto .24in; max-width: 4.7in; color: #263d4a; }
.cover .subtitle { font-size: 12pt; color: #6b5d4d; max-width: 4.2in; margin: 0 auto; }
.cover .rule { width: 1.2in; border-top: 2px solid #b39057; margin: .28in auto; }
.cover .mode { margin-top: .35in; font-size: 8.5pt; letter-spacing: .08em; text-transform: uppercase; color: #827463; }
.contents { page-break-after: always; }
.contents h1 { color: #263d4a; font-size: 22pt; margin-top: 0; }
.toc-list { list-style: none; padding: 0; margin: .2in 0; }
.toc-list li { margin: .11in 0; }
.toc-list a { text-decoration: none; color: #263d4a; }
.toc-list .toc-level-3 { margin-left: .22in; font-size: 9.2pt; }
.contents-note { color: #746b60; font-size: 8.7pt; border-left: 2px solid #b39057; padding-left: .12in; margin-bottom: .25in; }
.chapter-start { page-break-before: always; }
h2 { color: #263d4a; font-size: 19pt; line-height: 1.12; margin: 0 0 .18in; page-break-after: avoid; }
h3 { color: #486372; font-size: 13pt; margin: .28in 0 .08in; page-break-after: avoid; }
h4 { color: #655b4e; font-size: 11pt; margin: .2in 0 .05in; page-break-after: avoid; }
p { margin: 0 0 .13in; widows: 3; orphans: 3; }
blockquote { margin: .18in .18in; padding: .10in .16in; border-left: 3px solid #b39057; color: #51483e; background: #f5f0e7; font-size: 10pt; }
ul, ol { margin: .05in 0 .16in .25in; padding-left: .18in; }
li { margin: .035in 0; }
hr { border: 0; border-top: 1px solid #d4c9b8; margin: .28in 0; }
code { font-family: Menlo, monospace; font-size: .88em; }
pre { white-space: pre-wrap; background: #f0ede7; padding: .12in; font-size: 8pt; }
table { border-collapse: collapse; width: 100%; margin: .16in 0; font-size: 8.4pt; }
th, td { border: 1px solid #d8d0c4; padding: .05in; vertical-align: top; }
th { background: #eef2f1; color: #263d4a; text-align: left; }
.chapter-map { margin: 0 0 .24in; padding: .12in .16in .08in; background: #eef2f1; border: 1px solid #d3dfdf; page-break-inside: avoid; font-size: 8.8pt; }
.chapter-map strong { color: #263d4a; }
.chapter-map ul { margin: .04in 0 .02in .18in; }
.chapter-map li { margin: .01in 0; }
.editorial-comment { margin: .15in 0; padding: .10in .14in; border-left: 3px solid #a25b38; background: #fbefe8; color: #70432f; font-family: Arial, sans-serif; font-size: 8.6pt; }
.chapter-back { text-align: right; margin: -.10in 0 .20in; font: 7.5pt Arial, sans-serif; }
.chapter-back a { color: #6b5d4d; text-decoration: none; }
.content-figure { margin: .24in 0 .30in; padding: 0; page-break-inside: avoid; text-align: center; }
.content-figure img { display: block; width: 100%; max-height: 3.1in; object-fit: contain; }
.content-figure figcaption { margin-top: .06in; color: #746b60; font: italic 8.5pt Georgia, serif; }
@media print { html { background: white; } }
"""
def make_html(source: Path, mode: str) -> tuple[str, str, list[tuple[int, str, str]]]:
raw = source.read_text(encoding="utf-8")
meta, body = parse_frontmatter(raw)
body = clean_markdown(body, mode)
if meta.get("status") == "parent-review-edition" and mode == "clean":
validate_parent_review_contract(body)
body_html, headings = markdown_to_body(body, mode, source.parent)
title = meta.get("title", "The Pattern of Covenant Parenting")
title = re.sub(r"\s+—\s+Bryan Hickman Teaching.*$", "", title)
subtitle = "A clean reading edition" if mode == "clean" else "Editorial review draft"
major = [(level, text, anchor) for level, text, anchor in headings if level == 2]
toc = ['<nav class="toc-list">']
for level, text, anchor in headings:
if level == 2:
toc.append(f'<li><a href="#{anchor}">{inline_markdown(text)}</a></li>')
elif level == 3 and mode == "editorial":
# Editorial PDFs get the detailed outline; clean PDFs stay easy to scan.
toc.append(f'<li class="toc-level-3"><a href="#{anchor}">{inline_markdown(text)}</a></li>')
toc.append("</nav>")
html_doc = f'''<!doctype html>
<html><head><meta charset="utf-8"><title>{html.escape(title)}</title><style>{CSS}</style></head>
<body>
<section class="cover" id="cover"><h1>{html.escape(title)}</h1><div class="rule"></div><div class="subtitle">{html.escape(subtitle)}</div><div class="mode">Bryan Hickman teaching synthesis</div></section>
<section class="contents" id="contents"><h1>Contents</h1><p class="contents-note">Use the links below to move through the book. A Back to Contents link appears at the beginning of each chapter.</p>{''.join(toc)}</section>
<main>{body_html}</main>
</body></html>'''
return html_doc, title, headings
def add_pdf_navigation(output: Path, title: str, headings: list[tuple[int, str, str]], mode: str) -> int:
"""Add reader-sidebar bookmarks after Chrome has preserved HTML anchors."""
import fitz
doc = fitz.open(output)
page_text = [re.sub(r"\s+", " ", page.get_text()).strip() for page in doc]
toc: list[list[object]] = []
for level, heading, _anchor in headings:
if level not in (2, 3):
continue
needle = re.sub(r"\s+", " ", heading).strip()
page_number = next(
(i + 1 for i, text in enumerate(page_text[2:], start=2) if needle in text),
None,
)
if page_number is not None:
toc.append([1 if level == 2 else 2, heading, page_number])
doc.set_toc(toc)
metadata = dict(doc.metadata)
metadata.update({
"title": title,
"subject": "Clickable Contents, chapter maps, and PDF bookmarks",
"keywords": f"Bryan Hickman, covenant parenting, {mode} edition",
})
doc.set_metadata(metadata)
temp = output.with_suffix(".navigation.pdf")
doc.save(temp, garbage=4, deflate=True)
doc.close()
temp.replace(output)
return len(toc)
def render(source: Path, output: Path, mode: str) -> dict[str, object]:
if not CHROME.exists():
raise SystemExit(f"Google Chrome not found at {CHROME}")
html_doc, title, headings = make_html(source, mode)
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="parent-book-") as td:
html_path = Path(td) / "book.html"
profile = Path(td) / "chrome-profile"
html_path.write_text(html_doc, encoding="utf-8")
cmd = [
str(CHROME), "--headless=new", "--no-sandbox", "--disable-gpu",
f"--user-data-dir={profile}", "--no-pdf-header-footer",
"--allow-file-access-from-files", "--run-all-compositor-stages-before-draw",
"--virtual-time-budget=3000", f"--print-to-pdf={output}", html_path.as_uri(),
]
# Chrome can finish writing the PDF but keep a headless helper alive
# on macOS. Treat that specific case as success, after terminating the
# short-lived renderer instead of making the caller wait for 180s.
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
try:
_, stderr = proc.communicate(timeout=45)
except subprocess.TimeoutExpired:
proc.kill()
_, stderr = proc.communicate()
if not output.exists() or output.stat().st_size == 0:
raise RuntimeError(f"Chrome PDF render timed out: {stderr[-2000:]}")
if proc.returncode not in (0, -9) and (not output.exists() or output.stat().st_size == 0):
raise RuntimeError(f"Chrome PDF render failed: {stderr[-2000:]}")
# Remove an accidental zero-byte artifact before reporting failure.
if not output.exists() or output.stat().st_size == 0:
raise RuntimeError("Chrome reported success but produced no PDF")
bookmarks = add_pdf_navigation(output, title, headings, mode)
return {"title": title, "output": str(output), "bytes": output.stat().st_size, "mode": mode, "bookmarks": bookmarks}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
ap.add_argument("--mode", choices=["clean", "editorial"], required=True)
ap.add_argument("--output", type=Path)
args = ap.parse_args()
if args.output is None:
stem = args.source.stem
suffix = "clean" if args.mode == "clean" else "editorial"
args.output = DEFAULT_OUT / f"{stem}_{suffix}.pdf"
result = render(args.source, args.output, args.mode)
print(result)
if __name__ == "__main__":
main()