#!/usr/bin/env python3 import os import subprocess import argparse import json from typing import List, Dict, Any from pathlib import Path def export_html_wasm(notebook_path: str, output_dir: str, as_app: bool = False) -> bool: """Export a single marimo notebook to HTML format. Returns: bool: True if export succeeded, False otherwise """ output_path = notebook_path.replace(".py", ".html") cmd = ["marimo", "export", "html-wasm"] if as_app: print(f"Exporting {notebook_path} to {output_path} as app") cmd.extend(["--mode", "run", "--no-show-code"]) else: print(f"Exporting {notebook_path} to {output_path} as notebook") cmd.extend(["--mode", "edit"]) try: output_file = os.path.join(output_dir, output_path) os.makedirs(os.path.dirname(output_file), exist_ok=True) cmd.extend([notebook_path, "-o", output_file]) subprocess.run(cmd, capture_output=True, text=True, check=True) return True except subprocess.CalledProcessError as e: print(f"Error exporting {notebook_path}:") print(e.stderr) return False except Exception as e: print(f"Unexpected error exporting {notebook_path}: {e}") return False def get_course_metadata(course_dir: Path) -> Dict[str, Any]: """Extract metadata from a course directory.""" metadata = { "id": course_dir.name, "title": course_dir.name.replace("_", " ").title(), "description": "", "notebooks": [] } # Try to read README.md for description readme_path = course_dir / "README.md" if readme_path.exists(): with open(readme_path, "r", encoding="utf-8") as f: content = f.read() # Extract first paragraph as description if content: lines = content.split("\n") # Skip title line if it exists start_idx = 1 if lines and lines[0].startswith("#") else 0 description_lines = [] for line in lines[start_idx:]: if line.strip() and not line.startswith("#"): description_lines.append(line) elif description_lines: # Stop at the next heading break metadata["description"] = " ".join(description_lines).strip() return metadata def organize_notebooks_by_course(all_notebooks: List[str]) -> Dict[str, Dict[str, Any]]: """Organize notebooks by course.""" courses = {} for notebook_path in all_notebooks: path = Path(notebook_path) course_id = path.parts[0] if course_id not in courses: course_dir = Path(course_id) courses[course_id] = get_course_metadata(course_dir) # Extract notebook info filename = path.name notebook_id = path.stem # Try to extract order from filename (e.g., 001_numbers.py -> 1) order = 999 if "_" in notebook_id: try: order_str = notebook_id.split("_")[0] order = int(order_str) except ValueError: pass # Create display name by removing order prefix and underscores display_name = notebook_id if "_" in notebook_id: display_name = "_".join(notebook_id.split("_")[1:]) display_name = display_name.replace("_", " ").title() courses[course_id]["notebooks"].append({ "id": notebook_id, "path": notebook_path, "display_name": display_name, "order": order }) # Sort notebooks by order for course_id in courses: courses[course_id]["notebooks"].sort(key=lambda x: x["order"]) return courses def generate_eva_css() -> str: """Generate Neon Genesis Evangelion inspired CSS.""" return """ :root { --eva-purple: #9a1eb3; --eva-green: #00ff00; --eva-orange: #ff6600; --eva-blue: #0066ff; --eva-red: #ff0000; --eva-black: #111111; --eva-dark: #222222; --eva-terminal-bg: rgba(0, 0, 0, 0.85); --eva-text: #e0e0e0; --eva-border-radius: 4px; --eva-transition: all 0.3s ease; } body { background-color: var(--eva-black); color: var(--eva-text); font-family: 'Courier New', monospace; margin: 0; padding: 0; line-height: 1.6; } .eva-container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .eva-header { border-bottom: 2px solid var(--eva-green); padding-bottom: 1rem; margin-bottom: 2rem; display: flex; justify-content: space-between; align-items: center; position: sticky; top: 0; background-color: rgba(17, 17, 17, 0.95); z-index: 100; backdrop-filter: blur(5px); padding-top: 1rem; } .eva-logo { font-size: 2.5rem; font-weight: bold; color: var(--eva-green); text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px rgba(0, 255, 0, 0.5); } .eva-nav { display: flex; gap: 1.5rem; } .eva-nav a { color: white; text-decoration: none; text-transform: uppercase; font-size: 0.9rem; letter-spacing: 1px; transition: color 0.3s; position: relative; padding: 0.5rem 0; } .eva-nav a:hover { color: var(--eva-green); } .eva-nav a:hover::after { content: ''; position: absolute; bottom: -5px; left: 0; width: 100%; height: 2px; background-color: var(--eva-green); animation: scanline 1.5s linear infinite; } .eva-hero { background-color: var(--eva-terminal-bg); border: 1px solid var(--eva-green); padding: 3rem 2rem; margin-bottom: 3rem; position: relative; overflow: hidden; border-radius: var(--eva-border-radius); display: flex; flex-direction: column; align-items: flex-start; background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.7)), url('https://raw.githubusercontent.com/marimo-team/marimo/main/docs/_static/marimo-logotype-thick.svg'); background-size: cover; background-position: center; background-blend-mode: overlay; } .eva-hero::before { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 2px; background-color: var(--eva-green); animation: scanline 3s linear infinite; } .eva-hero h1 { font-size: 2.5rem; margin-bottom: 1rem; color: var(--eva-green); text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px rgba(0, 255, 0, 0.5); } .eva-hero p { font-size: 1.1rem; max-width: 800px; margin-bottom: 2rem; line-height: 1.8; } .eva-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-bottom: 3rem; } .eva-feature { background-color: var(--eva-terminal-bg); border: 1px solid var(--eva-blue); padding: 1.5rem; border-radius: var(--eva-border-radius); transition: var(--eva-transition); position: relative; overflow: hidden; } .eva-feature:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0, 102, 255, 0.2); } .eva-feature-icon { font-size: 2rem; margin-bottom: 1rem; color: var(--eva-blue); } .eva-feature h3 { font-size: 1.3rem; margin-bottom: 1rem; color: var(--eva-blue); } .eva-section-title { font-size: 2rem; color: var(--eva-green); margin-bottom: 2rem; text-transform: uppercase; letter-spacing: 2px; text-align: center; position: relative; padding-bottom: 1rem; } .eva-section-title::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 100px; height: 2px; background-color: var(--eva-green); } .eva-courses { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 2rem; } .eva-course { background-color: var(--eva-terminal-bg); border: 1px solid var(--eva-purple); border-radius: var(--eva-border-radius); transition: var(--eva-transition); position: relative; overflow: hidden; } .eva-course:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(154, 30, 179, 0.3); } .eva-course::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 2px; background-color: var(--eva-purple); animation: scanline 2s linear infinite; } .eva-course-header { padding: 1.5rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(154, 30, 179, 0.3); } .eva-course-title { font-size: 1.5rem; color: var(--eva-purple); text-transform: uppercase; letter-spacing: 1px; margin: 0; } .eva-course-toggle { color: var(--eva-purple); font-size: 1.5rem; transition: var(--eva-transition); } .eva-course-content { padding: 0 1.5rem; max-height: 0; overflow: hidden; transition: var(--eva-transition); } .eva-course.active .eva-course-content { padding: 1.5rem; max-height: 1000px; } .eva-course.active .eva-course-toggle { transform: rotate(180deg); } .eva-course-description { margin-bottom: 1.5rem; font-size: 0.9rem; line-height: 1.6; } .eva-notebooks { margin-top: 1rem; } .eva-notebook { margin-bottom: 0.75rem; padding: 0.5rem; border-left: 2px solid var(--eva-blue); transition: var(--eva-transition); display: flex; align-items: center; } .eva-notebook:hover { background-color: rgba(0, 102, 255, 0.1); padding-left: 1rem; } .eva-notebook a { color: white; text-decoration: none; display: block; font-size: 0.9rem; flex-grow: 1; } .eva-notebook a:hover { color: var(--eva-blue); } .eva-notebook-number { color: var(--eva-blue); font-size: 0.8rem; margin-right: 0.5rem; opacity: 0.7; min-width: 24px; } .eva-button { display: inline-block; background-color: transparent; color: var(--eva-green); border: 1px solid var(--eva-green); padding: 0.7rem 1.5rem; text-decoration: none; text-transform: uppercase; font-size: 0.9rem; letter-spacing: 1px; transition: var(--eva-transition); cursor: pointer; border-radius: var(--eva-border-radius); position: relative; overflow: hidden; } .eva-button:hover { background-color: var(--eva-green); color: var(--eva-black); } .eva-button::after { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: 0.5s; } .eva-button:hover::after { left: 100%; } .eva-cta { background-color: var(--eva-terminal-bg); border: 1px solid var(--eva-orange); padding: 3rem 2rem; margin: 4rem 0; text-align: center; border-radius: var(--eva-border-radius); position: relative; overflow: hidden; } .eva-cta h2 { font-size: 2rem; color: var(--eva-orange); margin-bottom: 1.5rem; text-transform: uppercase; } .eva-cta p { max-width: 600px; margin: 0 auto 2rem; font-size: 1.1rem; } .eva-cta .eva-button { color: var(--eva-orange); border-color: var(--eva-orange); } .eva-cta .eva-button:hover { background-color: var(--eva-orange); color: var(--eva-black); } .eva-footer { margin-top: 4rem; padding-top: 2rem; border-top: 2px solid var(--eva-green); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 2rem; } .eva-footer-links { display: flex; gap: 1.5rem; } .eva-footer-links a { color: var(--eva-text); text-decoration: none; transition: var(--eva-transition); } .eva-footer-links a:hover { color: var(--eva-green); } .eva-footer-copyright { font-size: 0.9rem; } .eva-search { position: relative; margin-bottom: 3rem; } .eva-search input { width: 100%; padding: 1rem; background-color: var(--eva-terminal-bg); border: 1px solid var(--eva-green); color: var(--eva-text); font-family: 'Courier New', monospace; font-size: 1rem; border-radius: var(--eva-border-radius); outline: none; transition: var(--eva-transition); } .eva-search input:focus { box-shadow: 0 0 10px rgba(0, 255, 0, 0.3); } .eva-search input::placeholder { color: rgba(224, 224, 224, 0.5); } .eva-search-icon { position: absolute; right: 1rem; top: 50%; transform: translateY(-50%); color: var(--eva-green); font-size: 1.2rem; } @keyframes scanline { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .eva-cursor { display: inline-block; width: 10px; height: 1.2em; background-color: var(--eva-green); margin-left: 2px; animation: blink 1s infinite; vertical-align: middle; } @media (max-width: 768px) { .eva-courses { grid-template-columns: 1fr; } .eva-header { flex-direction: column; align-items: flex-start; padding: 1rem; } .eva-nav { margin-top: 1rem; flex-wrap: wrap; } .eva-hero { padding: 2rem 1rem; } .eva-hero h1 { font-size: 2rem; } .eva-features { grid-template-columns: 1fr; } .eva-footer { flex-direction: column; align-items: center; text-align: center; } } """ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None: """Generate the index.html file with Neon Genesis Evangelion aesthetics.""" print("Generating index.html") index_path = os.path.join(output_dir, "index.html") os.makedirs(output_dir, exist_ok=True) try: with open(index_path, "w", encoding="utf-8") as f: f.write( """
A curated collection of educational notebooks covering computer science, mathematics, data science, and more. Built with marimo - the reactive Python notebook that makes data exploration delightful.
Explore CoursesExperience the power of reactive programming with marimo notebooks that automatically update when dependencies change.
Interactive examples and exercises help you understand concepts through hands-on practice.
From Python basics to advanced optimization techniques, our courses cover a wide range of topics.
{course["description"]}
\n' f'Help us expand our collection of educational notebooks. Whether you're an expert in machine learning, statistics, or any other field, your contributions are welcome!
Contribute on GitHub