#!/usr/bin/env python3 import os import subprocess import argparse import json from typing import List, Dict, Any from pathlib import Path import re 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 description = " ".join(description_lines).strip() # Clean up the description description = description.replace("_", "") description = description.replace("[work in progress]", "") description = description.replace("(https://github.com/marimo-team/learn/issues/51)", "") # Remove any other GitHub issue links description = re.sub(r'\[.*?\]\(https://github\.com/.*?/issues/\d+\)', '', description) description = re.sub(r'https://github\.com/.*?/issues/\d+', '', description) # Clean up any double spaces description = re.sub(r'\s+', ' ', description).strip() metadata["description"] = description 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:]) # Convert display name to title case, but handle italics properly parts = display_name.split("_") formatted_parts = [] i = 0 while i < len(parts): if i + 1 < len(parts) and parts[i] == "" and parts[i+1] == "": # Skip empty parts that might come from consecutive underscores i += 2 continue if i + 1 < len(parts) and (parts[i] == "" or parts[i+1] == ""): # This is an italics marker if parts[i] == "": # Opening italics text_part = parts[i+1].replace("_", " ").title() formatted_parts.append(f"{text_part}") i += 2 else: # Text followed by italics marker text_part = parts[i].replace("_", " ").title() formatted_parts.append(text_part) i += 1 else: # Regular text text_part = parts[i].replace("_", " ").title() formatted_parts.append(text_part) i += 1 display_name = " ".join(formatted_parts) courses[course_id]["notebooks"].append({ "id": notebook_id, "path": notebook_path, "display_name": display_name, "order": order, "original_number": notebook_id.split("_")[0] if "_" in notebook_id else "" }) # 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 with light/dark mode support.""" return """ :root { /* Light mode colors (default) */ --eva-purple: #7209b7; --eva-green: #1c7361; --eva-orange: #e65100; --eva-blue: #0039cb; --eva-red: #c62828; --eva-black: #f5f5f5; --eva-dark: #e0e0e0; --eva-terminal-bg: rgba(255, 255, 255, 0.9); --eva-text: #333333; --eva-border-radius: 4px; --eva-transition: all 0.3s ease; } /* Dark mode colors */ [data-theme="dark"] { --eva-purple: #9a1eb3; --eva-green: #1c7361; --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; } body { background-color: var(--eva-black); color: var(--eva-text); font-family: 'Courier New', monospace; margin: 0; padding: 0; line-height: 1.6; transition: background-color 0.3s ease, color 0.3s ease; } .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: var(--eva-black); z-index: 100; backdrop-filter: blur(5px); padding-top: 1rem; transition: background-color 0.3s ease; } [data-theme="light"] .eva-header { background-color: rgba(245, 245, 245, 0.95); } .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(28, 115, 97, 0.5); } [data-theme="light"] .eva-logo { text-shadow: 0 0 10px rgba(28, 115, 97, 0.3); } .eva-nav { display: flex; gap: 1.5rem; align-items: center; } .eva-nav a { color: var(--eva-text); 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; } .theme-toggle { background: none; border: none; color: var(--eva-text); cursor: pointer; font-size: 1.2rem; padding: 0.5rem; margin-left: 1rem; transition: color 0.3s; } .theme-toggle:hover { color: var(--eva-green); } .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; transition: background-color 0.3s ease, border-color 0.3s ease; } [data-theme="light"] .eva-hero { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.7)), url('https://raw.githubusercontent.com/marimo-team/marimo/main/docs/_static/marimo-logotype-thick.svg'); } .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(28, 115, 97, 0.5); } [data-theme="light"] .eva-hero h1 { text-shadow: 0 0 10px rgba(28, 115, 97, 0.3); } .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); } /* Flashcard view for courses */ .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), height 0.4s cubic-bezier(0.19, 1, 0.22, 1); position: relative; overflow: hidden; height: 350px; display: flex; flex-direction: column; } .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-badge { position: absolute; top: 15px; right: -40px; background: linear-gradient(135deg, var(--eva-orange) 0%, #ff9500 100%); color: var(--eva-black); font-size: 0.65rem; padding: 0.3rem 2.5rem; text-transform: uppercase; font-weight: bold; z-index: 3; letter-spacing: 1px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); transform: rotate(45deg); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.2); border-top: 1px solid rgba(255, 255, 255, 0.3); border-bottom: 1px solid rgba(0, 0, 0, 0.2); white-space: nowrap; overflow: hidden; } .eva-course-badge i { margin-right: 4px; font-size: 0.7rem; } [data-theme="light"] .eva-course-badge { box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); text-shadow: 0 1px 1px rgba(255, 255, 255, 0.4); } .eva-course-badge::before { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.3), transparent); animation: scanline 2s linear infinite; } .eva-course-header { padding: 1rem 1.5rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(154, 30, 179, 0.3); z-index: 2; background-color: var(--eva-terminal-bg); position: absolute; top: 0; left: 0; width: 100%; height: 3.5rem; box-sizing: border-box; } .eva-course-title { font-size: 1.3rem; color: var(--eva-purple); text-transform: uppercase; letter-spacing: 1px; margin: 0; } .eva-course-toggle { color: var(--eva-purple); font-size: 1.5rem; transition: transform 0.4s cubic-bezier(0.19, 1, 0.22, 1); } .eva-course.active .eva-course-toggle { transform: rotate(180deg); } .eva-course-front { display: flex; flex-direction: column; justify-content: space-between; padding: 1.5rem; margin-top: 3.5rem; transition: opacity 0.3s ease, transform 0.3s ease; position: absolute; top: 0; left: 0; width: 100%; height: calc(100% - 3.5rem); background-color: var(--eva-terminal-bg); z-index: 1; box-sizing: border-box; } .eva-course.active .eva-course-front { opacity: 0; transform: translateY(-10px); pointer-events: none; } .eva-course-description { margin-top: 0.5rem; margin-bottom: 1.5rem; font-size: 0.9rem; line-height: 1.6; flex-grow: 1; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; max-height: 150px; } .eva-course-stats { display: flex; justify-content: space-between; font-size: 0.8rem; color: var(--eva-text); opacity: 0.7; } .eva-course-content { position: absolute; top: 3.5rem; left: 0; width: 100%; height: calc(100% - 3.5rem); padding: 1.5rem; background-color: var(--eva-terminal-bg); transition: opacity 0.3s ease, transform 0.3s ease; opacity: 0; transform: translateY(10px); pointer-events: none; overflow-y: auto; z-index: 1; box-sizing: border-box; } .eva-course.active .eva-course-content { opacity: 1; transform: translateY(0); pointer-events: auto; } .eva-course.active { height: auto; min-height: 350px; max-height: 800px; transition: height 0.4s cubic-bezier(0.19, 1, 0.22, 1), transform 0.3s ease, box-shadow 0.3s ease; } .eva-notebooks { margin-top: 1rem; display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 0.75rem; } .eva-notebook { margin-bottom: 0.5rem; padding: 0.75rem; border-left: 2px solid var(--eva-blue); transition: all 0.25s ease; display: flex; align-items: center; background-color: rgba(0, 0, 0, 0.2); border-radius: 0 var(--eva-border-radius) var(--eva-border-radius) 0; opacity: 1; transform: translateX(0); } [data-theme="light"] .eva-notebook { background-color: rgba(0, 0, 0, 0.05); } .eva-notebook:hover { background-color: rgba(0, 102, 255, 0.1); padding-left: 1rem; transform: translateX(3px); } .eva-notebook a { color: var(--eva-text); 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.75rem; opacity: 0.7; min-width: 24px; font-weight: bold; } .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-course-button { margin-top: 1rem; margin-bottom: 1rem; align-self: center; } .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; flex-direction: column; align-items: center; gap: 2rem; } .eva-footer-logo { max-width: 200px; margin-bottom: 1rem; } .eva-footer-links { display: flex; gap: 1.5rem; margin-bottom: 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-social-links { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; } .eva-social-links a { color: var(--eva-text); font-size: 1.5rem; transition: var(--eva-transition); } .eva-social-links a:hover { color: var(--eva-green); transform: translateY(-3px); } .eva-footer-copyright { font-size: 0.9rem; text-align: center; } .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(28, 115, 97, 0.3); } .eva-search input::placeholder { color: rgba(224, 224, 224, 0.5); } [data-theme="light"] .eva-search input::placeholder { color: rgba(51, 51, 51, 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; } .eva-notebooks { grid-template-columns: 1fr; } } .eva-course.closing .eva-course-content { opacity: 0; transform: translateY(10px); transition: opacity 0.2s ease, transform 0.2s ease; } .eva-course.closing .eva-course-front { opacity: 1; transform: translateY(0); transition: opacity 0.3s ease 0.1s, transform 0.3s ease 0.1s; } """ 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'{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