1 Commits

Author SHA1 Message Date
nopeitsnothing f667d020d5 Fix PDF build in CI
Added workflow for building PDF. Progress.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-12 04:51:12 -04:00
12 changed files with 173 additions and 438 deletions
+4 -14
View File
@@ -31,7 +31,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: "3.13" python-version: "3.12"
- name: Install MkDocs Material - name: Install MkDocs Material
run: pip install mkdocs-material run: pip install mkdocs-material
@@ -44,22 +44,12 @@ jobs:
- name: Build PDF - name: Build PDF
env: env:
CI: true CI: true
run: python scripts/build_guide_pdf.py --both run: python scripts/build_guide_pdf.py
- name: Upload PDF artifact - name: Upload PDF artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: light-pdf name: guide-pdf
path: export/thgtoa.pdf path: export/guide.pdf
archive: false
if-no-files-found: error
retention-days: 90
- name: Upload PDF artifact (Dark Mode)
uses: actions/upload-artifact@v7
with:
name: dark-pdf
path: export/thgtoa-dark.pdf
archive: false
if-no-files-found: error if-no-files-found: error
retention-days: 90 retention-days: 90
-33
View File
@@ -1,33 +0,0 @@
name: '🦠 VirusTotal Scan'
on:
workflow_dispatch:
push:
branches:
- 'main'
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: '📦 Checkout'
uses: actions/checkout@v6
- name: '📦 Set up Go'
uses: actions/setup-go@v6
with:
go-version: '1.26.2'
- run: go version
- name: '🦠 Scan PDF files using VT'
uses: crazy-max/ghaction-virustotal@v5
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
update_release_body: true
files: |
./export/thgtoa.pdf
./export/thgtoa-dark.pdf
-1
View File
@@ -20,4 +20,3 @@ site/
_site/ _site/
_site_test/ _site_test/
export/ export/
build/
+2 -7
View File
@@ -7,16 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed
- Refactored GitHub Actions workflow **Build PDF** (`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs (`export/thgtoa.pdf` and `export/thgtoa-dark.pdf` respectively).
- Restored previous VT scans workflow **VirusTotal Scan** (`.github/workflows/vt-scan.yml`): submit files to VT for malware scanning. Links will be published on the site.
## [1.2.1] - 2026-04-11 ## [1.2.1] - 2026-04-11
### Added ### Added
- GitHub Actions workflow **Build PDF** (`.github/workflows/build-pdf.yml`): installs Chromium on `ubuntu-latest`, runs `scripts/build_guide_pdf.py`, uploads `export/guide.pdf` as the `guide-pdf` artifact. Runs on `workflow_dispatch`, on pushes to `main` that touch docs or build inputs, and on matching pull requests. - GitHub Actions workflow **Build guide PDF** (`.github/workflows/build-pdf.yml`): installs Chromium on `ubuntu-latest`, runs `scripts/build_guide_pdf.py`, uploads `export/guide.pdf` as the `guide-pdf` artifact. Runs on `workflow_dispatch`, on pushes to `main` that touch docs or build inputs, and on matching pull requests.
- `scripts/build_guide_pdf.py` to build the MkDocs site and render the guide to a single PDF (`export/guide.pdf` by default) using a Chromium-based browser (Chrome or Edge) headless print-to-PDF. - `scripts/build_guide_pdf.py` to build the MkDocs site and render the guide to a single PDF (`export/guide.pdf` by default) using a Chromium-based browser (Chrome or Edge) headless print-to-PDF.
- `docs/stylesheets/extra.css` and `extra_css` in `mkdocs.yml` for shared site styling. - `docs/stylesheets/extra.css` and `extra_css` in `mkdocs.yml` for shared site styling.
@@ -28,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Guide landing layout: wrap the opening block in `docs/guide/index.md` with a `guide-intro-lead` container so the logo and first sections share one layout context for web and print. - Guide landing layout: wrap the opening block in `docs/guide/index.md` with a `guide-intro-lead` container so the logo and first sections share one layout context for web and print.
- `.gitignore` to exclude local build outputs `export/`, `site/`, and `_site_test/`. - `.gitignore` to exclude local build outputs `export/`, `site/`, and `_site_test/`.
- `scripts/build_guide_pdf.py`: when the `CI` environment variable is set, pass Chromium flags (`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`) so headless print works on typical CI images. - `scripts/build_guide_pdf.py`: when the `CI` environment variable is set, pass Chromium flags (`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`) so headless print works on typical CI images.
- `README.md`: note the **Build PDF** GitHub Actions workflow and the `guide-pdf` artifact. - `README.md`: note the **Build guide PDF** GitHub Actions workflow and the `guide-pdf` artifact.
### Fixed ### Fixed
+2 -2
View File
@@ -18,9 +18,9 @@ This guide is an open-source non-profit initiative, [licensed](LICENSE.html) und
python scripts/build_guide_pdf.py python scripts/build_guide_pdf.py
``` ```
This runs `mkdocs build` (output defaults to `./site`), then uses **Google Chrome** or **Microsoft Edge** in headless mode to print `site/guide/index.html` to **`export/thgtoa.pdf`** (images and styling preserved). If the site is already built: `python scripts/build_guide_pdf.py --skip-mkdocs`. Other options: `--site-dir`, `--pdf`, and `python scripts/build_guide_pdf.py --help`. This runs `mkdocs build` (output defaults to `./site`), then uses **Google Chrome** or **Microsoft Edge** in headless mode to print `site/guide/index.html` to **`export/guide.pdf`** (images and styling preserved). If the site is already built: `python scripts/build_guide_pdf.py --skip-mkdocs`. Other options: `--site-dir`, `--pdf`, and `python scripts/build_guide_pdf.py --help`.
On **GitHub Actions**, the [Build guide PDF](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) workflow does the same using Chromium on Ubuntu when you push to `main` or open a pull request that touches the guide or build inputs; download the **`thgtoa.pdf`** artifact from a successful run. You can also run it manually (**Actions** → **Build guide PDF****Run workflow**). On **GitHub Actions**, the [Build guide PDF](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) workflow does the same using Chromium on Ubuntu when you push to `main` or open a pull request that touches the guide or build inputs; download the **`guide-pdf`** artifact from a successful run. You can also run it manually (**Actions** → **Build guide PDF****Run workflow**).
- **OpenDocument (ODT):** not produced by this repository (previous hosted export removed). - **OpenDocument (ODT):** not produced by this repository (previous hosted export removed).
- **Raw Markdown (very large):** [docs/guide/index.md on GitHub](https://raw.githubusercontent.com/Anon-Planet/thgtoa/refs/heads/main/docs/guide/index.md) - **Raw Markdown (very large):** [docs/guide/index.md on GitHub](https://raw.githubusercontent.com/Anon-Planet/thgtoa/refs/heads/main/docs/guide/index.md)
+1 -1
View File
@@ -15,7 +15,7 @@ schema:
--- ---
![Anonymous Planet logo](../media/profile.png){ align=right } ![Anonymous Planet logo](../media/profile.png){ align=right }
**Anonymous Planet** are the maintainers of the [_Hitchhiker's Guide_](../guide) and the [_PSA Community_](https://psa.anonymousplanet.org). It is responsible for maintaining the projects and code repositories. This project is part of our ongoing efforts to provide open-source tools and resources for the community, with regular updates and improvements added to the changelog. **Anonymous Planet** are the maintainers of the [_Hitchhiker's Guide_](https://anonymousplanet.org/guide.html) and the [_PSA Community_](https://psa.anonymousplanet.org). It is responsible for maintaining the projects and code repositories.
The purpose: providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. It is written with the hopes that good people (e.g., activists, journalists, scientists, lawyers, whistle-blowers, etc.) will be able to fight oppression, censorship and harassment! The website and projects are free (as in freedom) and not affiliated with any donor or projects discussed. The purpose: providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. It is written with the hopes that good people (e.g., activists, journalists, scientists, lawyers, whistle-blowers, etc.) will be able to fight oppression, censorship and harassment! The website and projects are free (as in freedom) and not affiliated with any donor or projects discussed.
+2 -2
View File
@@ -27,9 +27,9 @@ schema:
!!! Note "PDF export (single file)" !!! Note "PDF export (single file)"
The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) on the [**source repository**](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`thgtoa`** and **`thgtoa-dark`** artifacts. You can start a fresh build anytime (**Actions** → **Build guide PDF****Run workflow**). The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) on the [**thgtoa** source repository](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`guide-pdf`** artifact. You can start a fresh build anytime (**Actions** → **Build guide PDF****Run workflow**).
To produce the same file locally, clone the repository and run `python3 scripts/build_guide_pdf.py --both` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide). To produce the same file locally, clone the repository and run `python scripts/build_guide_pdf.py` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide).
!!! Note "Our official git mirrors" !!! Note "Our official git mirrors"
-130
View File
@@ -1,130 +0,0 @@
/* Generate dark mode PDF of the HTML at guide/index.html */
/*
DARK_MODE_PDF.CSS
Use this stylesheet when generating a PDF from HTML.
*/
:root {
/* Color Palette */
--bg-color: #121212; /* Deep dark grey (easier on eyes than pure black) */
--text-primary: #e0e0e0; /* Off-white for readability */
--text-secondary: #a0a0a0; /* Grey for captions/metadata */
--accent-color: #bb86fc; /* Light purple accent (optional) */
--border-color: #333333; /* Subtle borders */
/* Fonts - System fonts ensure best rendering across PDF engines */
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* --- RESET & BASE STYLES --- */
* {
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: var(--font-main);
line-height: 1.6;
margin: 0;
padding: 0;
}
/* --- TYPOGRAPHY & HEADINGS --- */
h1, h2, h3, h4, h5, h6 {
color: var(--text-primary);
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
p {
margin-bottom: 1rem;
color: var(--text-secondary); /* Slightly dimmer text for body copy */
}
a {
color: var(--accent-color);
text-decoration: underline;
}
/* --- CONTAINER & LAYOUT --- */
.container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
/* Cards / Sections with dark backgrounds */
.card {
background-color: #1e1e1e;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
/* --- TABLES (Common in PDFs) --- */
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
color: var(--text-primary);
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: #2c2c2c;
font-weight: bold;
}
tr:last-child td {
border-bottom: none;
}
/* --- IMAGES --- */
img {
max-width: 100%;
height: auto;
display: block;
margin: 20px 0;
/* This ensures high contrast images don't get washed out */
filter: brightness(1.1);
}
/* --- CRITICAL FOR PDF GENERATORS --- */
/* Forces the browser/PDF engine to print background colors and graphics */
@media print {
@page {
size: A4; /* Change to 'Letter' if preferred */
margin: 20mm;
}
body {
background-color: var(--bg-color) !important;
color: var(--text-primary) !important;
}
.card, table th {
-webkit-print-color-adjust: exact !important; /* Chrome/Safari */
print-color-adjust: exact !important; /* Firefox/Standard */
background-color: #1e1e1e !important;
color: var(--text-primary) !important;
}
/* Prevent page breaks in the middle of a sentence or card if possible */
.card {
break-inside: avoid;
}
/* Hide elements you don't want in PDF (like navigation bars) */
nav, footer, button {
display: none !important;
}
}
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -122,4 +122,4 @@ markdown_extensions:
toc_depth: 3 toc_depth: 3
copyright: | copyright: |
&copy; 2023-2026 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a> &copy; 2023-2025 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
+161 -247
View File
@@ -1,247 +1,161 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extended version of build_guide_pdf.py with dark mode support. """Build the MkDocs site, then render docs/guide/ to a single PDF via a Chromium-based browser.
This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium. Uses headless Chrome/Edge print-to-PDF (embeds images). WeasyPrint-based mkdocs-with-pdf is
omitted here because it needs GTK/Pango (awkward on Windows).
Usage:
python scripts/build_guide_pdf_extended.py # Generate light mode PDF only Usage (from repo root):
python scripts/build_guide_pdf_extended.py --dark-mode # Generate dark mode PDF only python scripts/build_guide_pdf.py
python scripts/build_guide_pdf_extended.py --both # Generate both light and dark mode PDFs python scripts/build_guide_pdf.py --site-dir build/html --pdf export/guide.pdf
"""
Examples:
python scripts/build_guide_pdf_extended.py --site-dir build/html --pdf-light export/thgtoa.pdf from __future__ import annotations
python scripts/build_guide_pdf_extended.py --dark-mode --pdf-dark export/thgtoa-dark.pdf
""" import argparse
import os
from __future__ import annotations import shutil
import subprocess
import argparse import sys
import os import time
import shutil from pathlib import Path
import subprocess
import sys
import time def repo_root() -> Path:
from pathlib import Path return Path(__file__).resolve().parent.parent
def repo_root() -> Path: def find_chromium_executable() -> Path | None:
return Path(__file__).resolve().parent.parent if sys.platform == "win32":
paths = [
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
def find_chromium_executable() -> Path | None: Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
if sys.platform == "win32": Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
paths = [ Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe", Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe", ]
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe", for p in paths:
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe", if p.is_file():
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe", return p
] for name in ("chrome", "msedge"):
for p in paths: w = shutil.which(name)
if p.is_file(): if w:
return p return Path(w)
for name in ("chrome", "msedge"): elif sys.platform == "darwin":
w = shutil.which(name) for p in (
if w: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
return Path(w) "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
elif sys.platform == "darwin": "/Applications/Chromium.app/Contents/MacOS/Chromium",
for p in ( ):
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", if os.path.isfile(p):
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", return Path(p)
"/Applications/Chromium.app/Contents/MacOS/Chromium", for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
): w = shutil.which(name)
if os.path.isfile(p): if w:
return Path(p) return Path(w)
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"): return None
w = shutil.which(name)
if w:
return Path(w) def run_mkdocs(site_dir: Path) -> None:
return None site_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
def run_mkdocs(site_dir: Path) -> None: cwd=repo_root(),
site_dir.mkdir(parents=True, exist_ok=True) check=True,
subprocess.run( )
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
cwd=repo_root(),
check=True, def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> 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, dark_mode: bool = False) -> Path: pdf_out.parent.mkdir(parents=True, exist_ok=True)
"""Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows partial = pdf_out.parent / f".{pdf_out.name}.writing"
does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead. partial.unlink(missing_ok=True)
Args: uri = html_file.resolve().as_uri()
browser: Path to Chromium executable # Chromium headless print; allow time for fonts/images on very large pages.
html_file: Path to HTML file to convert cmd = [str(browser)]
pdf_out: Output PDF path if os.environ.get("CI"):
dark_mode: If True, use dark mode color scheme via --prefers-color-scheme flag # GitHub Actions / other CI runners often need these for Chromium to start.
""" cmd += [
pdf_out.parent.mkdir(parents=True, exist_ok=True) "--no-sandbox",
partial = pdf_out.parent / f".{pdf_out.name}.writing" "--disable-setuid-sandbox",
partial.unlink(missing_ok=True) "--disable-dev-shm-usage",
]
uri = html_file.resolve().as_uri() cmd += [
"--headless=new",
# Chromium headless print; allow time for fonts/images on very large pages. "--disable-gpu",
cmd = [str(browser)] "--no-pdf-header-footer",
if os.environ.get("CI"): f"--print-to-pdf={partial.resolve()}",
# GitHub Actions / other CI runners often need these for Chromium to start. uri,
cmd += [ ]
"--no-sandbox", subprocess.run(cmd, check=True, timeout=600)
"--disable-setuid-sandbox", deadline = time.time() + 120
"--disable-dev-shm-usage", while time.time() < deadline:
] if partial.exists() and partial.stat().st_size > 0:
break
cmd += [ time.sleep(0.25)
"--headless=new", else:
"--disable-gpu", partial.unlink(missing_ok=True)
"--no-pdf-header-footer", raise RuntimeError(f"PDF was not written to {partial}")
]
try:
# Add dark mode preference if requested if pdf_out.exists():
if dark_mode: pdf_out.unlink()
cmd.append("--prefers-color-scheme=dark") except PermissionError:
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}")
cmd += [ fallback.unlink(missing_ok=True)
f"--print-to-pdf={partial.resolve()}", partial.replace(fallback)
uri, return fallback
]
partial.replace(pdf_out)
subprocess.run(cmd, check=True, timeout=600) return pdf_out
deadline = time.time() + 120
while time.time() < deadline:
if partial.exists() and partial.stat().st_size > 0: def main() -> int:
break root = repo_root()
time.sleep(0.25) ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF.")
else: ap.add_argument(
partial.unlink(missing_ok=True) "--site-dir",
raise RuntimeError(f"PDF was not written to {partial}") type=Path,
default=root / "site",
try: help="MkDocs output directory (default: ./site)",
if pdf_out.exists(): )
pdf_out.unlink() ap.add_argument(
except PermissionError: "--pdf",
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}") type=Path,
fallback.unlink(missing_ok=True) default=root / "export" / "guide.pdf",
partial.replace(fallback) help="Output PDF path (default: ./export/guide.pdf)",
return fallback )
ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.")
partial.replace(pdf_out) args = ap.parse_args()
return pdf_out
guide_html = args.site_dir / "guide" / "index.html"
if not args.skip_mkdocs:
def generate_dark_mode_html(html_file: Path, output_file: Path, dark_css_path: Path) -> None: run_mkdocs(args.site_dir)
"""Create a temporary HTML file with dark mode stylesheet applied. if not guide_html.is_file():
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
This is used when we need to force dark mode rendering via CSS rather than browser flags. return 1
"""
try: browser = find_chromium_executable()
from bs4 import BeautifulSoup if not browser:
print(
# Read the original HTML "No Chromium-based browser found (Chrome, Edge, or Chromium). "
html_content = html_file.read_text(encoding='utf-8') "Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
soup = BeautifulSoup(html_content, 'html.parser') file=sys.stderr,
)
# Add dark mode stylesheet link if not present return 1
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): out = print_to_pdf(browser, guide_html, args.pdf)
head = soup.head or soup.new_tag('head') size_kb = out.stat().st_size // 1024
link_tag = soup.new_tag('link', rel='stylesheet', href=str(dark_css_path)) print(f"Wrote {out.resolve()} ({size_kb} KiB)")
if soup.head: if out.resolve() != args.pdf.resolve():
soup.head.append(link_tag) print(
else: f"Note: {args.pdf.name} was in use; close it and rename or replace with the file above.",
# Create a new head section file=sys.stderr,
new_head = soup.new_tag('head') )
new_head.append(link_tag) return 0
soup.insert(0, new_head)
# Write the modified HTML if __name__ == "__main__":
output_file.parent.mkdir(parents=True, exist_ok=True) raise SystemExit(main())
output_file.write_text(str(soup), encoding='utf-8')
except ImportError:
print("BeautifulSoup not available. Skipping CSS injection.")
def main() -> int:
root = repo_root()
ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF (light and/or dark mode).")
ap.add_argument(
"--site-dir",
type=Path,
default=root / "build" / "html",
help="MkDocs output directory (default: ./build/html)",
)
ap.add_argument(
"--pdf-light",
type=Path,
default=root / "export" / "thgtoa.pdf",
help="Output PDF path for light mode (default: ./export/guide.pdf)",
)
ap.add_argument(
"--pdf-dark",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Output PDF path for dark mode (default: ./export/guide-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")
args = ap.parse_args()
# Determine which modes to generate
if args.dark_mode:
modes = ["dark"]
elif args.both:
modes = ["light", "dark"]
else:
modes = ["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 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
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)
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.",
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():
print(
f"Note: {args.pdf_dark.name} was in use; close it and rename or replace with the file above.",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())