Skip to content

Commit 138d947

Browse files
authored
Merge pull request #2 from B67687/pages/notebook-html-rendering
Fix GitHub Pages notebook rendering
2 parents 4eb1d63 + 35960ff commit 138d947

4 files changed

Lines changed: 273 additions & 0 deletions

File tree

.github/workflows/deploy-pages.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Deploy GitHub Pages
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pages: write
15+
id-token: write
16+
17+
concurrency:
18+
group: pages
19+
cancel-in-progress: true
20+
21+
jobs:
22+
build:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout repository
26+
uses: actions/checkout@v4
27+
28+
- name: Set up Pages
29+
uses: actions/configure-pages@v5
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: "3.12"
35+
36+
- name: Install build dependencies
37+
run: pip install -r pages-requirements.txt
38+
39+
- name: Build Pages site
40+
run: python build_pages.py
41+
42+
- name: Upload Pages artifact
43+
uses: actions/upload-pages-artifact@v3
44+
with:
45+
path: _site
46+
47+
deploy:
48+
if: github.event_name != 'pull_request'
49+
environment:
50+
name: github-pages
51+
url: ${{ steps.deployment.outputs.page_url }}
52+
needs: build
53+
runs-on: ubuntu-latest
54+
steps:
55+
- name: Deploy to GitHub Pages
56+
id: deployment
57+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ venv.bak/
145145

146146
# mkdocs documentation
147147
/site
148+
/_site
148149

149150
# mypy
150151
.mypy_cache/

build_pages.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
from __future__ import annotations
2+
3+
import html
4+
import os
5+
import re
6+
import shutil
7+
import stat
8+
import subprocess
9+
import sys
10+
from pathlib import Path
11+
12+
13+
ROOT = Path(__file__).resolve().parent
14+
OUTPUT_DIR = Path(os.environ.get("PAGES_OUTPUT_DIR", ROOT / "_site"))
15+
EXCLUDED_PARTS = {
16+
".git",
17+
".github",
18+
".ipynb_checkpoints",
19+
"__pycache__",
20+
"_site",
21+
}
22+
STATIC_SUFFIXES = {
23+
".png",
24+
".jpg",
25+
".jpeg",
26+
".gif",
27+
".svg",
28+
".webp",
29+
".ico",
30+
".txt",
31+
".yml",
32+
".yaml",
33+
}
34+
35+
36+
def should_skip(path: Path) -> bool:
37+
return any(part in EXCLUDED_PARTS for part in path.parts)
38+
39+
40+
def handle_remove_readonly(func, path, excinfo) -> None:
41+
os.chmod(path, stat.S_IWRITE)
42+
func(path)
43+
44+
45+
def clear_output_dir() -> None:
46+
if OUTPUT_DIR.exists():
47+
shutil.rmtree(OUTPUT_DIR, onexc=handle_remove_readonly)
48+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
49+
50+
51+
def run_nbconvert(notebook_path: Path) -> Path:
52+
relative_path = notebook_path.relative_to(ROOT)
53+
destination = OUTPUT_DIR / relative_path.with_suffix(".html")
54+
destination.parent.mkdir(parents=True, exist_ok=True)
55+
56+
command = [
57+
sys.executable,
58+
"-m",
59+
"nbconvert",
60+
"--to",
61+
"html",
62+
"--output",
63+
destination.stem,
64+
"--output-dir",
65+
str(destination.parent),
66+
str(notebook_path),
67+
]
68+
subprocess.run(command, check=True)
69+
rewrite_html_links(destination)
70+
return destination
71+
72+
73+
def rewrite_notebook_links(text: str) -> str:
74+
return re.sub(r"(?P<prefix>[\(/\"'=])(?P<target>[^\"')>]+?)\.ipynb(?P<suffix>[\"')>#?])", r"\g<prefix>\g<target>.html\g<suffix>", text)
75+
76+
77+
def rewrite_html_links(html_path: Path) -> None:
78+
content = html_path.read_text(encoding="utf-8")
79+
updated = rewrite_notebook_links(content)
80+
if updated != content:
81+
html_path.write_text(updated, encoding="utf-8")
82+
83+
84+
def copy_static_files() -> None:
85+
for path in ROOT.rglob("*"):
86+
if path.is_dir() or should_skip(path):
87+
continue
88+
if path == ROOT / "README.md":
89+
continue
90+
if path.suffix.lower() not in STATIC_SUFFIXES:
91+
continue
92+
93+
destination = OUTPUT_DIR / path.relative_to(ROOT)
94+
destination.parent.mkdir(parents=True, exist_ok=True)
95+
shutil.copy2(path, destination)
96+
97+
98+
def render_markdown(markdown_text: str) -> str:
99+
try:
100+
import markdown
101+
except ImportError as exc:
102+
raise RuntimeError(
103+
"The 'markdown' package is required to build the Pages homepage. "
104+
"Install it with 'pip install markdown'."
105+
) from exc
106+
107+
return markdown.markdown(
108+
markdown_text,
109+
extensions=[
110+
"extra",
111+
"toc",
112+
"tables",
113+
"fenced_code",
114+
"sane_lists",
115+
],
116+
output_format="html5",
117+
)
118+
119+
120+
def build_homepage() -> None:
121+
readme_path = ROOT / "README.md"
122+
readme_text = readme_path.read_text(encoding="utf-8")
123+
readme_text = rewrite_notebook_links(readme_text)
124+
body = render_markdown(readme_text)
125+
126+
page = f"""<!DOCTYPE html>
127+
<html lang="en">
128+
<head>
129+
<meta charset="utf-8">
130+
<meta name="viewport" content="width=device-width, initial-scale=1">
131+
<title>MathLearningNotes</title>
132+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.8.1/github-markdown.min.css">
133+
<style>
134+
body {{
135+
box-sizing: border-box;
136+
margin: 0;
137+
background: #f6f8fa;
138+
color: #1f2328;
139+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
140+
}}
141+
.page {{
142+
max-width: 980px;
143+
margin: 0 auto;
144+
padding: 32px 16px 48px;
145+
}}
146+
.markdown-body {{
147+
background: #ffffff;
148+
border: 1px solid #d0d7de;
149+
border-radius: 12px;
150+
padding: 32px;
151+
}}
152+
.site-note {{
153+
margin-bottom: 16px;
154+
padding: 12px 16px;
155+
border-left: 4px solid #0969da;
156+
background: #ddf4ff;
157+
border-radius: 8px;
158+
}}
159+
@media (max-width: 767px) {{
160+
.page {{
161+
padding: 16px 10px 32px;
162+
}}
163+
.markdown-body {{
164+
padding: 20px;
165+
}}
166+
}}
167+
</style>
168+
</head>
169+
<body>
170+
<main class="page">
171+
<div class="site-note">
172+
Notebook links on this site open rendered HTML pages generated from the repository's <code>.ipynb</code> files.
173+
</div>
174+
<article class="markdown-body">
175+
{body}
176+
</article>
177+
</main>
178+
</body>
179+
</html>
180+
"""
181+
(OUTPUT_DIR / "index.html").write_text(page, encoding="utf-8")
182+
183+
184+
def create_nojekyll_file() -> None:
185+
(OUTPUT_DIR / ".nojekyll").write_text("", encoding="utf-8")
186+
187+
188+
def main() -> None:
189+
clear_output_dir()
190+
191+
notebooks = sorted(
192+
path for path in ROOT.rglob("*.ipynb") if not should_skip(path)
193+
)
194+
if not notebooks:
195+
raise RuntimeError("No notebooks were found to convert.")
196+
197+
for notebook in notebooks:
198+
print(f"Converting {notebook.relative_to(ROOT)}")
199+
run_nbconvert(notebook)
200+
201+
copy_static_files()
202+
build_homepage()
203+
create_nojekyll_file()
204+
205+
print(f"Built {len(notebooks)} notebooks into {OUTPUT_DIR}")
206+
207+
208+
if __name__ == "__main__":
209+
try:
210+
main()
211+
except subprocess.CalledProcessError as exc:
212+
command = " ".join(html.escape(part) for part in exc.cmd)
213+
raise SystemExit(f"Build failed while running: {command}") from exc

pages-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
nbconvert>=7
2+
markdown

0 commit comments

Comments
 (0)