Skip to content

Commit be8ad57

Browse files
TrevorAustinclaude
andcommitted
add build script for static site generation
Integrates preprocessor, Jinja2 templates, CSS, and JS into a complete build pipeline that reads markdown lecture notes and produces a static site in _site/. Includes integration tests and adds _site/ to .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9e0c25d commit be8ad57

3 files changed

Lines changed: 141 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
apikey.js
33
*.pyc
44
*/__pycache__/*
5-
.worktrees/
5+
.worktrees/
6+
_site/

build.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import glob
2+
import os
3+
import re
4+
import shutil
5+
from pathlib import Path
6+
7+
from jinja2 import Environment, FileSystemLoader
8+
import markdown
9+
10+
from preprocessor import split_slides, process_slide, rewrite_image_paths
11+
12+
13+
def build_site(output_dir: str = "_site"):
14+
"""Build the entire static site."""
15+
root = Path(__file__).parent
16+
out = Path(output_dir)
17+
18+
# Clean output
19+
if out.exists():
20+
shutil.rmtree(out)
21+
out.mkdir(parents=True)
22+
23+
# Set up Jinja2 — autoescape=False since templates use {{ content }}
24+
# with pre-rendered HTML and this is a static site generator, not a web app
25+
env = Environment(loader=FileSystemLoader(root / "templates"), autoescape=False)
26+
27+
# Copy static assets
28+
shutil.copytree(root / "static" / "css", out / "css")
29+
shutil.copytree(root / "static" / "js", out / "js")
30+
shutil.copytree(root / "examples", out / "examples")
31+
shutil.copytree(root / "lecture_notes" / "images", out / "lecture_notes" / "images")
32+
33+
# Find week files (exclude _old files)
34+
week_files = sorted(glob.glob(str(root / "lecture_notes" / "week_*.md")))
35+
week_files = [f for f in week_files if "_old" not in f]
36+
37+
weeks = []
38+
39+
for week_file in week_files:
40+
week_num = int(re.search(r"week_(\d+)", week_file).group(1))
41+
weeks.append(week_num)
42+
43+
md_content = Path(week_file).read_text()
44+
slide_strings = split_slides(md_content)
45+
46+
total_slides = len(slide_strings)
47+
week_dir = out / "lecture_notes" / f"week_{week_num}"
48+
49+
for i, slide_md in enumerate(slide_strings, 1):
50+
html_content, classes = process_slide(slide_md)
51+
html_content = rewrite_image_paths(html_content)
52+
53+
prev_url = f"/lecture_notes/week_{week_num}/{i - 1}/" if i > 1 else ""
54+
next_url = f"/lecture_notes/week_{week_num}/{i + 1}/" if i < total_slides else ""
55+
first_url = f"/lecture_notes/week_{week_num}/1/"
56+
57+
slide_html = env.get_template("slide.html").render(
58+
content=html_content,
59+
classes=classes,
60+
week=week_num,
61+
slide_num=i,
62+
total_slides=total_slides,
63+
prev_url=prev_url,
64+
next_url=next_url,
65+
first_url=first_url,
66+
)
67+
68+
slide_dir = week_dir / str(i)
69+
slide_dir.mkdir(parents=True, exist_ok=True)
70+
(slide_dir / "index.html").write_text(slide_html)
71+
72+
# Lecture notes index
73+
lecture_index_html = env.get_template("lecture_index.html").render(weeks=weeks)
74+
lectures_dir = out / "lecture_notes"
75+
lectures_dir.mkdir(parents=True, exist_ok=True)
76+
(lectures_dir / "index.html").write_text(lecture_index_html)
77+
78+
# Syllabus
79+
syllabus_md = (root / "syllabus.md").read_text()
80+
md_renderer = markdown.Markdown(
81+
extensions=["fenced_code", "codehilite", "tables"],
82+
extension_configs={
83+
"codehilite": {"css_class": "highlight", "guess_lang": False}
84+
},
85+
)
86+
syllabus_html = md_renderer.convert(syllabus_md)
87+
page_html = env.get_template("page.html").render(
88+
title="Syllabus", content=syllabus_html
89+
)
90+
syllabus_dir = out / "syllabus"
91+
syllabus_dir.mkdir(parents=True, exist_ok=True)
92+
(syllabus_dir / "index.html").write_text(page_html)
93+
94+
# Homepage
95+
homepage_html = env.get_template("index.html").render()
96+
(out / "index.html").write_text(homepage_html)
97+
98+
99+
if __name__ == "__main__":
100+
build_site()

tests/test_build.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
import shutil
3+
from pathlib import Path
4+
from build import build_site
5+
6+
7+
def test_build_produces_output(tmp_path, monkeypatch):
8+
"""Smoke test: build produces _site/ with expected structure."""
9+
monkeypatch.chdir(Path(__file__).parent.parent)
10+
build_site(output_dir=str(tmp_path / "_site"))
11+
site = tmp_path / "_site"
12+
13+
assert (site / "index.html").exists()
14+
assert (site / "syllabus" / "index.html").exists()
15+
assert (site / "lecture_notes" / "index.html").exists()
16+
assert (site / "lecture_notes" / "week_1" / "1" / "index.html").exists()
17+
assert (site / "css" / "style.css").exists()
18+
assert (site / "js" / "navigation.js").exists()
19+
assert (site / "examples").is_dir()
20+
assert (site / "lecture_notes" / "images").is_dir()
21+
22+
23+
def test_build_slide_has_navigation(tmp_path, monkeypatch):
24+
"""Slide pages include navigation data attributes."""
25+
monkeypatch.chdir(Path(__file__).parent.parent)
26+
build_site(output_dir=str(tmp_path / "_site"))
27+
28+
slide = (tmp_path / "_site" / "lecture_notes" / "week_1" / "2" / "index.html").read_text()
29+
assert 'data-prev=' in slide
30+
assert 'data-next=' in slide
31+
assert 'data-slide-num="2"' in slide
32+
33+
34+
def test_build_no_week_8_old(tmp_path, monkeypatch):
35+
"""week_8_old.md should not produce output."""
36+
monkeypatch.chdir(Path(__file__).parent.parent)
37+
build_site(output_dir=str(tmp_path / "_site"))
38+
39+
assert not (tmp_path / "_site" / "lecture_notes" / "week_8_old").exists()

0 commit comments

Comments
 (0)