mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-05-07 03:54:18 +02:00
Compare commits
6 Commits
v1.2.1
...
c49cc87390
| Author | SHA1 | Date | |
|---|---|---|---|
| c49cc87390 | |||
| a14191bc7b | |||
| a47e02939d | |||
| 52a38f5deb | |||
| 206a6ff6b7 | |||
| 0e8de6ccc0 |
@@ -1,4 +1,4 @@
|
|||||||
name: 🚀 Build guide PDF
|
name: 📖 Build PDF
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -26,12 +26,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.13"
|
||||||
|
|
||||||
- name: Install MkDocs Material
|
- name: Install MkDocs Material
|
||||||
run: pip install mkdocs-material
|
run: pip install mkdocs-material
|
||||||
@@ -44,12 +44,22 @@ jobs:
|
|||||||
- name: Build PDF
|
- name: Build PDF
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
run: python scripts/build_guide_pdf.py
|
run: python scripts/build_guide_pdf.py --both
|
||||||
|
|
||||||
- name: Upload PDF artifact
|
- name: Upload PDF artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: guide-pdf
|
name: light-pdf
|
||||||
path: export/guide.pdf
|
path: export/thgtoa.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
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
@@ -20,3 +20,4 @@ site/
|
|||||||
_site/
|
_site/
|
||||||
_site_test/
|
_site_test/
|
||||||
export/
|
export/
|
||||||
|
build/
|
||||||
|
|||||||
+7
-2
@@ -7,11 +7,16 @@ 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 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.
|
- 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.
|
||||||
|
|
||||||
- `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.
|
||||||
@@ -23,7 +28,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 guide PDF** GitHub Actions workflow and the `guide-pdf` artifact.
|
- `README.md`: note the **Build PDF** GitHub Actions workflow and the `guide-pdf` artifact.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -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/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`.
|
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`.
|
||||||
|
|
||||||
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**).
|
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**).
|
||||||
- **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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 [**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**).
|
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**).
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
!!! Note "Our official git mirrors"
|
!!! Note "Our official git mirrors"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/* 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.
Binary file not shown.
+1
-1
@@ -122,4 +122,4 @@ markdown_extensions:
|
|||||||
toc_depth: 3
|
toc_depth: 3
|
||||||
|
|
||||||
copyright: |
|
copyright: |
|
||||||
© 2023-2025 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
|
© 2023-2026 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
|
||||||
|
|||||||
+247
-161
@@ -1,161 +1,247 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Build the MkDocs site, then render docs/guide/ to a single PDF via a Chromium-based browser.
|
"""Extended version of build_guide_pdf.py with dark mode support.
|
||||||
|
|
||||||
Uses headless Chrome/Edge print-to-PDF (embeds images). WeasyPrint-based mkdocs-with-pdf is
|
This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium.
|
||||||
omitted here because it needs GTK/Pango (awkward on Windows).
|
|
||||||
|
Usage:
|
||||||
Usage (from repo root):
|
python scripts/build_guide_pdf_extended.py # Generate light mode PDF only
|
||||||
python scripts/build_guide_pdf.py
|
python scripts/build_guide_pdf_extended.py --dark-mode # Generate dark mode PDF only
|
||||||
python scripts/build_guide_pdf.py --site-dir build/html --pdf export/guide.pdf
|
python scripts/build_guide_pdf_extended.py --both # Generate both light and dark mode PDFs
|
||||||
"""
|
|
||||||
|
Examples:
|
||||||
from __future__ import annotations
|
python scripts/build_guide_pdf_extended.py --site-dir build/html --pdf-light export/thgtoa.pdf
|
||||||
|
python scripts/build_guide_pdf_extended.py --dark-mode --pdf-dark export/thgtoa-dark.pdf
|
||||||
import argparse
|
"""
|
||||||
import os
|
|
||||||
import shutil
|
from __future__ import annotations
|
||||||
import subprocess
|
|
||||||
import sys
|
import argparse
|
||||||
import time
|
import os
|
||||||
from pathlib import Path
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
def repo_root() -> Path:
|
import time
|
||||||
return Path(__file__).resolve().parent.parent
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def find_chromium_executable() -> Path | None:
|
def repo_root() -> Path:
|
||||||
if sys.platform == "win32":
|
return Path(__file__).resolve().parent.parent
|
||||||
paths = [
|
|
||||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
|
||||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
def find_chromium_executable() -> Path | None:
|
||||||
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
if sys.platform == "win32":
|
||||||
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
|
paths = [
|
||||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
|
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||||
]
|
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
|
||||||
for p in paths:
|
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
|
||||||
if p.is_file():
|
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
|
||||||
return p
|
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
|
||||||
for name in ("chrome", "msedge"):
|
]
|
||||||
w = shutil.which(name)
|
for p in paths:
|
||||||
if w:
|
if p.is_file():
|
||||||
return Path(w)
|
return p
|
||||||
elif sys.platform == "darwin":
|
for name in ("chrome", "msedge"):
|
||||||
for p in (
|
w = shutil.which(name)
|
||||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
if w:
|
||||||
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
return Path(w)
|
||||||
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
elif sys.platform == "darwin":
|
||||||
):
|
for p in (
|
||||||
if os.path.isfile(p):
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||||
return Path(p)
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
||||||
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||||
w = shutil.which(name)
|
):
|
||||||
if w:
|
if os.path.isfile(p):
|
||||||
return Path(w)
|
return Path(p)
|
||||||
return None
|
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
|
||||||
|
w = shutil.which(name)
|
||||||
|
if w:
|
||||||
def run_mkdocs(site_dir: Path) -> None:
|
return Path(w)
|
||||||
site_dir.mkdir(parents=True, exist_ok=True)
|
return None
|
||||||
subprocess.run(
|
|
||||||
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
|
|
||||||
cwd=repo_root(),
|
def run_mkdocs(site_dir: Path) -> None:
|
||||||
check=True,
|
site_dir.mkdir(parents=True, exist_ok=True)
|
||||||
)
|
subprocess.run(
|
||||||
|
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
|
||||||
|
cwd=repo_root(),
|
||||||
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path) -> Path:
|
check=True,
|
||||||
"""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.
|
|
||||||
"""
|
|
||||||
pdf_out.parent.mkdir(parents=True, exist_ok=True)
|
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool = False) -> Path:
|
||||||
partial = pdf_out.parent / f".{pdf_out.name}.writing"
|
"""Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows
|
||||||
partial.unlink(missing_ok=True)
|
does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead.
|
||||||
|
|
||||||
uri = html_file.resolve().as_uri()
|
Args:
|
||||||
# Chromium headless print; allow time for fonts/images on very large pages.
|
browser: Path to Chromium executable
|
||||||
cmd = [str(browser)]
|
html_file: Path to HTML file to convert
|
||||||
if os.environ.get("CI"):
|
pdf_out: Output PDF path
|
||||||
# GitHub Actions / other CI runners often need these for Chromium to start.
|
dark_mode: If True, use dark mode color scheme via --prefers-color-scheme flag
|
||||||
cmd += [
|
"""
|
||||||
"--no-sandbox",
|
pdf_out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
"--disable-setuid-sandbox",
|
partial = pdf_out.parent / f".{pdf_out.name}.writing"
|
||||||
"--disable-dev-shm-usage",
|
partial.unlink(missing_ok=True)
|
||||||
]
|
|
||||||
cmd += [
|
uri = html_file.resolve().as_uri()
|
||||||
"--headless=new",
|
|
||||||
"--disable-gpu",
|
# Chromium headless print; allow time for fonts/images on very large pages.
|
||||||
"--no-pdf-header-footer",
|
cmd = [str(browser)]
|
||||||
f"--print-to-pdf={partial.resolve()}",
|
if os.environ.get("CI"):
|
||||||
uri,
|
# GitHub Actions / other CI runners often need these for Chromium to start.
|
||||||
]
|
cmd += [
|
||||||
subprocess.run(cmd, check=True, timeout=600)
|
"--no-sandbox",
|
||||||
deadline = time.time() + 120
|
"--disable-setuid-sandbox",
|
||||||
while time.time() < deadline:
|
"--disable-dev-shm-usage",
|
||||||
if partial.exists() and partial.stat().st_size > 0:
|
]
|
||||||
break
|
|
||||||
time.sleep(0.25)
|
cmd += [
|
||||||
else:
|
"--headless=new",
|
||||||
partial.unlink(missing_ok=True)
|
"--disable-gpu",
|
||||||
raise RuntimeError(f"PDF was not written to {partial}")
|
"--no-pdf-header-footer",
|
||||||
|
]
|
||||||
try:
|
|
||||||
if pdf_out.exists():
|
# Add dark mode preference if requested
|
||||||
pdf_out.unlink()
|
if dark_mode:
|
||||||
except PermissionError:
|
cmd.append("--prefers-color-scheme=dark")
|
||||||
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}")
|
|
||||||
fallback.unlink(missing_ok=True)
|
cmd += [
|
||||||
partial.replace(fallback)
|
f"--print-to-pdf={partial.resolve()}",
|
||||||
return fallback
|
uri,
|
||||||
|
]
|
||||||
partial.replace(pdf_out)
|
|
||||||
return pdf_out
|
subprocess.run(cmd, check=True, timeout=600)
|
||||||
|
deadline = time.time() + 120
|
||||||
|
while time.time() < deadline:
|
||||||
def main() -> int:
|
if partial.exists() and partial.stat().st_size > 0:
|
||||||
root = repo_root()
|
break
|
||||||
ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF.")
|
time.sleep(0.25)
|
||||||
ap.add_argument(
|
else:
|
||||||
"--site-dir",
|
partial.unlink(missing_ok=True)
|
||||||
type=Path,
|
raise RuntimeError(f"PDF was not written to {partial}")
|
||||||
default=root / "site",
|
|
||||||
help="MkDocs output directory (default: ./site)",
|
try:
|
||||||
)
|
if pdf_out.exists():
|
||||||
ap.add_argument(
|
pdf_out.unlink()
|
||||||
"--pdf",
|
except PermissionError:
|
||||||
type=Path,
|
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}")
|
||||||
default=root / "export" / "guide.pdf",
|
fallback.unlink(missing_ok=True)
|
||||||
help="Output PDF path (default: ./export/guide.pdf)",
|
partial.replace(fallback)
|
||||||
)
|
return fallback
|
||||||
ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.")
|
|
||||||
args = ap.parse_args()
|
partial.replace(pdf_out)
|
||||||
|
return pdf_out
|
||||||
guide_html = args.site_dir / "guide" / "index.html"
|
|
||||||
if not args.skip_mkdocs:
|
|
||||||
run_mkdocs(args.site_dir)
|
def generate_dark_mode_html(html_file: Path, output_file: Path, dark_css_path: Path) -> None:
|
||||||
if not guide_html.is_file():
|
"""Create a temporary HTML file with dark mode stylesheet applied.
|
||||||
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
|
|
||||||
return 1
|
This is used when we need to force dark mode rendering via CSS rather than browser flags.
|
||||||
|
"""
|
||||||
browser = find_chromium_executable()
|
try:
|
||||||
if not browser:
|
from bs4 import BeautifulSoup
|
||||||
print(
|
|
||||||
"No Chromium-based browser found (Chrome, Edge, or Chromium). "
|
# Read the original HTML
|
||||||
"Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
|
html_content = html_file.read_text(encoding='utf-8')
|
||||||
file=sys.stderr,
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
)
|
|
||||||
return 1
|
# Add dark mode stylesheet link if not present
|
||||||
|
existing_links = [link.get('href', '') for link in soup.find_all('link', rel='stylesheet')]
|
||||||
out = print_to_pdf(browser, guide_html, args.pdf)
|
if not any(dark_css_path.name in link for link in existing_links):
|
||||||
size_kb = out.stat().st_size // 1024
|
head = soup.head or soup.new_tag('head')
|
||||||
print(f"Wrote {out.resolve()} ({size_kb} KiB)")
|
link_tag = soup.new_tag('link', rel='stylesheet', href=str(dark_css_path))
|
||||||
if out.resolve() != args.pdf.resolve():
|
if soup.head:
|
||||||
print(
|
soup.head.append(link_tag)
|
||||||
f"Note: {args.pdf.name} was in use; close it and rename or replace with the file above.",
|
else:
|
||||||
file=sys.stderr,
|
# Create a new head section
|
||||||
)
|
new_head = soup.new_tag('head')
|
||||||
return 0
|
new_head.append(link_tag)
|
||||||
|
soup.insert(0, new_head)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
# Write the modified HTML
|
||||||
raise SystemExit(main())
|
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.")
|
||||||
|
|
||||||
|
|
||||||
|
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())
|
||||||
|
|||||||
Reference in New Issue
Block a user