diff --git a/.gitignore b/.gitignore index b1dd850..99145e5 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,6 @@ cython_debug/ .pypirc .DS_Store + +# Generated build output +public/ diff --git a/build.py b/build.py new file mode 100644 index 0000000..82e9e85 --- /dev/null +++ b/build.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +"""Build script: scans shared/, generates zip downloads and public/index.html.""" + +import html +import os +import shutil +import zipfile +from dataclasses import dataclass, field +from pathlib import Path + +SHARED_DIR = Path("shared") +PUBLIC_DIR = Path("public") +ZIPS_DIR = PUBLIC_DIR / "zips" + +EXCLUDE_NAMES = {".DS_Store", "__pycache__", ".git", "Thumbs.db"} +EXCLUDE_SUFFIXES = {".pyc"} + + +@dataclass +class SubSection: + name: str + zip_url: str + files: list[str] + + +@dataclass +class Section: + title: str + subsections: list[SubSection] = field(default_factory=list) + zip_url: str | None = None + files: list[str] | None = None + + +# --- File filtering --- + +def should_include(path: Path) -> bool: + return path.name not in EXCLUDE_NAMES and path.suffix not in EXCLUDE_SUFFIXES + + +# --- Zip and file listing --- + +def create_zip(source_dir: Path, zip_path: Path) -> None: + """Zip source_dir into zip_path; contents extract under a named folder.""" + with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: + for file_path in sorted(source_dir.rglob("*")): + if not file_path.is_file(): + continue + relative = file_path.relative_to(source_dir.parent) + if any(part in EXCLUDE_NAMES for part in relative.parts): + continue + if not should_include(file_path): + continue + zf.write(file_path, arcname=relative) + + +def list_files(source_dir: Path) -> list[str]: + """Return sorted list of filtered relative file paths for display.""" + results = [] + for file_path in sorted(source_dir.rglob("*")): + if not file_path.is_file(): + continue + relative = file_path.relative_to(source_dir) + if any(part in EXCLUDE_NAMES for part in relative.parts): + continue + if not should_include(file_path): + continue + results.append(str(relative)) + return results + + +# --- Data model construction --- + +def scan_shared() -> list[Section]: + sections = [] + + assignments_dir = SHARED_DIR / "assignments" + if assignments_dir.exists(): + zip_name = "assignments.zip" + create_zip(assignments_dir, ZIPS_DIR / zip_name) + sections.append(Section( + title="Assignments", + zip_url=f"zips/{zip_name}", + files=list_files(assignments_dir), + )) + + notes_dir = SHARED_DIR / "notes-and-examples" + if notes_dir.exists(): + date_dirs = sorted( + d for d in notes_dir.iterdir() + if d.is_dir() and should_include(d) + ) + subsections = [] + for date_dir in date_dirs: + zip_name = f"notes-and-examples-{date_dir.name}.zip" + create_zip(date_dir, ZIPS_DIR / zip_name) + subsections.append(SubSection( + name=date_dir.name, + zip_url=f"zips/{zip_name}", + files=list_files(date_dir), + )) + sections.append(Section(title="Notes and Examples", subsections=subsections)) + + return sections + + +# --- HTML rendering --- + +HTML_TEMPLATE = """\ + + + + + + DSA Tutoring + + + +

DSA Tutoring

+

Lesson files and assignments. Click a download button to get a .zip archive.

+{body} + +""" + + +def render_file_list(files: list[str]) -> str: + items = "".join(f"
  • {html.escape(f)}
  • \n" for f in files) + return f" " + + +def render_download_btn(zip_url: str, label: str) -> str: + return ( + f"" + f"Download {html.escape(label)} (.zip)" + ) + + +def render_section(section: Section) -> str: + if section.subsections: + parts = [] + for sub in section.subsections: + parts.append( + f"
    \n" + f"
    \n" + f"

    {html.escape(sub.name)}

    \n" + f" {render_download_btn(sub.zip_url, sub.name)}\n" + f"
    \n" + f"{render_file_list(sub.files)}\n" + f"
    " + ) + inner = "\n".join(parts) + else: + inner = ( + f"
    \n" + f" {render_download_btn(section.zip_url, section.title)}\n" + f"{render_file_list(section.files or [])}\n" + f"
    " + ) + + return ( + f"
    \n" + f"

    {html.escape(section.title)}

    \n" + f"{inner}\n" + f"
    " + ) + + +def render_html(sections: list[Section]) -> str: + body = "\n".join(render_section(s) for s in sections) + return HTML_TEMPLATE.format(body=body) + + +# --- Entry point --- + +def main() -> None: + os.chdir(Path(__file__).parent) + + if PUBLIC_DIR.exists(): + shutil.rmtree(PUBLIC_DIR) + PUBLIC_DIR.mkdir() + ZIPS_DIR.mkdir() + + sections = scan_shared() + html_content = render_html(sections) + + output = PUBLIC_DIR / "index.html" + output.write_text(html_content, encoding="utf-8") + print(f"Built {output}") + + print(f"Zips in {ZIPS_DIR}:") + for z in sorted(ZIPS_DIR.iterdir()): + print(f" {z.name}") + + +if __name__ == "__main__": + main()