From 11f88859bf3dacf21e72cceb58dedaaf529f81d4 Mon Sep 17 00:00:00 2001 From: nopeitsnothing Date: Fri, 22 May 2026 16:25:54 -0400 Subject: [PATCH] 2/8 refactor(pdf): wire dark mode through convert.py Removes the dead Chromium dark mode path and BeautifulSoup CSS injection code. Dark PDF is now produced by calling convert.py on the finished light PDF. --both builds light then dark; --dark alone works if the light PDF already exists. Signed-off-by: nopeitsnothing --- scripts/build_guide_pdf.py | 162 +++++++++++++++---------------------- 1 file changed, 66 insertions(+), 96 deletions(-) diff --git a/scripts/build_guide_pdf.py b/scripts/build_guide_pdf.py index c276e2b..f3a99f2 100644 --- a/scripts/build_guide_pdf.py +++ b/scripts/build_guide_pdf.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 -"""Experimental dark mode support. - -This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium. +"""Build light-mode PDF with MkDocs + Chromium, then produce dark-mode PDF via convert.py. Usage: - python scripts/build_guide_pdf.py # Generate light mode PDF only - python scripts/build_guide_pdf.py --dark-mode # Generate dark mode PDF only - python scripts/build_guide_pdf.py --both # Generate both light and dark mode PDFs + python scripts/build_guide_pdf.py # Light PDF only + python scripts/build_guide_pdf.py --dark # Dark PDF only (requires light PDF to exist) + python scripts/build_guide_pdf.py --both # Light PDF, then dark PDF Examples: python scripts/build_guide_pdf.py --site-dir build/html --pdf-light export/thgtoa.pdf - python scripts/build_guide_pdf.py --dark-mode --pdf-dark export/thgtoa-dark.pdf + python scripts/build_guide_pdf.py --both --pdf-light export/thgtoa.pdf --pdf-dark export/thgtoa-dark.pdf """ from __future__ import annotations @@ -68,15 +66,11 @@ def run_mkdocs(site_dir: Path) -> None: ) -def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool = False) -> Path: - """Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows - does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead. +def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path: + """Render html_file to pdf_out via headless Chromium. - Args: - browser: Path to Chromium executable - html_file: Path to HTML file to convert - pdf_out: Output PDF path - dark_mode: If True, use dark mode color scheme via --prefers-color-scheme flag + Uses a temp file first so an open guide.pdf on Windows does not block the + build; if the final path is still locked, writes guide-new.pdf instead. """ pdf_out.parent.mkdir(parents=True, exist_ok=True) partial = pdf_out.parent / f".{pdf_out.name}.writing" @@ -84,32 +78,23 @@ def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool uri = html_file.resolve().as_uri() - # Chromium headless print; allow time for fonts/images on very large pages. cmd = [str(browser)] if os.environ.get("CI"): - # GitHub Actions / other CI runners often need these for Chromium to start. cmd += [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", ] - cmd += [ "--headless=new", "--disable-gpu", "--no-pdf-header-footer", - ] - - # Add dark mode preference if requested - if dark_mode: - cmd.append("--prefers-color-scheme=dark") - - cmd += [ f"--print-to-pdf={partial.resolve()}", uri, ] subprocess.run(cmd, check=True, timeout=600) + deadline = time.time() + 120 while time.time() < deadline: if partial.exists() and partial.stat().st_size > 0: @@ -131,42 +116,23 @@ def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool partial.replace(pdf_out) return pdf_out - -def generate_dark_mode_html(html_file: Path, output_file: Path, dark_css_path: Path) -> None: - """Create a temporary HTML file with dark mode stylesheet applied. - - This is used when we need to force dark mode rendering via CSS rather than browser flags. - """ - try: - from bs4 import BeautifulSoup - - # Read the original HTML - html_content = html_file.read_text(encoding='utf-8') - soup = BeautifulSoup(html_content, 'html.parser') - - # Add dark mode stylesheet link if not present - existing_links = [link.get('href', '') for link in soup.find_all('link', rel='stylesheet')] - if not any(dark_css_path.name in link for link in existing_links): - head = soup.head or soup.new_tag('head') - link_tag = soup.new_tag('link', rel='stylesheet', href=str(dark_css_path)) - if soup.head: - soup.head.append(link_tag) - else: - # Create a new head section - new_head = soup.new_tag('head') - new_head.append(link_tag) - soup.insert(0, new_head) - - # Write the modified HTML - output_file.parent.mkdir(parents=True, exist_ok=True) - output_file.write_text(str(soup), encoding='utf-8') - except ImportError: - print("BeautifulSoup not available. Skipping CSS injection.") +# Use scripts/convert.py in place of broken dark-mode hack +def build_dark_pdf(light_pdf: Path, dark_pdf: Path) -> Path: + """Convert the light PDF to dark mode using scripts/convert.py.""" + convert_script = repo_root() / "scripts" / "convert.py" + print(f"Converting {light_pdf.name} → {dark_pdf.name} (dark mode)…") + subprocess.run( + [sys.executable, str(convert_script), str(light_pdf), str(dark_pdf)], + check=True, + ) + return dark_pdf def main() -> int: root = repo_root() - ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF (light and/or dark mode).") + ap = argparse.ArgumentParser( + description="Build MkDocs + single-page guide PDF (light and/or dark mode)." + ) ap.add_argument( "--site-dir", type=Path, @@ -177,68 +143,72 @@ def main() -> int: "--pdf-light", type=Path, default=root / "export" / "thgtoa.pdf", - help="Output PDF path for light mode (default: ./export/thgtoa.pdf)", + help="Output path for light PDF (default: ./export/thgtoa.pdf)", ) ap.add_argument( "--pdf-dark", type=Path, default=root / "export" / "thgtoa-dark.pdf", - help="Output PDF path for dark mode (default: ./export/thgtoa-dark.pdf)", + help="Output path for dark PDF (default: ./export/thgtoa-dark.pdf)", ) - ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.") - ap.add_argument("--dark-mode", action="store_true", help="Generate dark mode PDF only") - ap.add_argument("--both", action="store_true", help="Generate both light and dark mode PDFs") + ap.add_argument( + "--skip-mkdocs", + action="store_true", + help="Reuse existing site dir; skip MkDocs build.", + ) + + mode = ap.add_mutually_exclusive_group() + mode.add_argument("--dark", action="store_true", help="Dark PDF only (light PDF must already exist)") + mode.add_argument("--both", action="store_true", help="Build light PDF, then dark PDF") + # default (no flag) = light only args = ap.parse_args() - # Determine which modes to generate - if args.dark_mode: - modes = ["dark"] - elif args.both: - modes = ["light", "dark"] - else: - modes = ["light"] + build_light = not args.dark + build_dark = args.dark or args.both - guide_html = args.site_dir / "guide" / "index.html" + # --- Light PDF (Chromium) --- + if build_light: + guide_html = args.site_dir / "guide" / "index.html" - if not args.skip_mkdocs or any(mode == "light" for mode in modes): - run_mkdocs(args.site_dir) + if not args.skip_mkdocs: + run_mkdocs(args.site_dir) - if not guide_html.is_file(): - print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr) - return 1 + if not guide_html.is_file(): + print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr) + return 1 - browser = find_chromium_executable() - if not browser: - print( - "No Chromium-based browser found (Chrome, Edge, or Chromium). " - "Install Google Chrome or Microsoft Edge, or add Chromium to PATH.", - file=sys.stderr, - ) - return 1 + browser = find_chromium_executable() + if not browser: + print( + "No Chromium-based browser found (Chrome, Edge, or Chromium). " + "Install Google Chrome or Microsoft Edge, or add Chromium to PATH.", + file=sys.stderr, + ) + return 1 - dark_css_path = root / "docs" / "stylesheets" / "dark-extra.css" - - # Generate light mode PDF (default) - if "light" in modes: - out_light = print_to_pdf(browser, guide_html, args.pdf_light, dark_mode=False) + out_light = print_to_pdf(browser, guide_html, args.pdf_light) size_kb = out_light.stat().st_size // 1024 print(f"Wrote {out_light.resolve()} ({size_kb} KiB) [Light Mode]") if out_light.resolve() != args.pdf_light.resolve(): print( - f"Note: {args.pdf_light.name} was in use; close it and rename or replace with the file above.", + f"Note: {args.pdf_light.name} was in use; " + "close it and rename or replace with the file above.", file=sys.stderr, ) - # Generate dark mode PDF - if "dark" in modes: - out_dark = print_to_pdf(browser, guide_html, args.pdf_dark, dark_mode=True) - size_kb = out_dark.stat().st_size // 1024 - print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]") - if out_dark.resolve() != args.pdf_dark.resolve(): + # --- Dark PDF (pixel converter) --- + if build_dark: + if not args.pdf_light.exists(): print( - f"Note: {args.pdf_dark.name} was in use; close it and rename or replace with the file above.", + f"Light PDF not found at {args.pdf_light}. " + "Run without --dark first, or use --both.", file=sys.stderr, ) + return 1 + + out_dark = build_dark_pdf(args.pdf_light, args.pdf_dark) + size_kb = out_dark.stat().st_size // 1024 + print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]") return 0