Haleshot's picture
Add Neon Genesis Evangelion themed landing page
e9d126c unverified
raw
history blame
30.2 kB
#!/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(
"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Marimo Learn - Interactive Educational Notebooks</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
""" + generate_eva_css() + """
</style>
</head>
<body>
<div class="eva-container">
<header class="eva-header">
<div class="eva-logo">MARIMO LEARN</div>
<nav class="eva-nav">
<a href="#features">Features</a>
<a href="#courses">Courses</a>
<a href="#contribute">Contribute</a>
<a href="https://docs.marimo.io" target="_blank">Documentation</a>
<a href="https://github.com/marimo-team/learn" target="_blank">GitHub</a>
</nav>
</header>
<section class="eva-hero">
<h1>Interactive Learning with Marimo<span class="eva-cursor"></span></h1>
<p>
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.
</p>
<a href="#courses" class="eva-button">Explore Courses</a>
</section>
<section id="features">
<h2 class="eva-section-title">Why Marimo Learn?</h2>
<div class="eva-features">
<div class="eva-feature">
<div class="eva-feature-icon"><i class="fas fa-bolt"></i></div>
<h3>Reactive Notebooks</h3>
<p>Experience the power of reactive programming with marimo notebooks that automatically update when dependencies change.</p>
</div>
<div class="eva-feature">
<div class="eva-feature-icon"><i class="fas fa-code"></i></div>
<h3>Learn by Doing</h3>
<p>Interactive examples and exercises help you understand concepts through hands-on practice.</p>
</div>
<div class="eva-feature">
<div class="eva-feature-icon"><i class="fas fa-graduation-cap"></i></div>
<h3>Comprehensive Courses</h3>
<p>From Python basics to advanced optimization techniques, our courses cover a wide range of topics.</p>
</div>
</div>
</section>
<section id="courses">
<h2 class="eva-section-title">Explore Courses</h2>
<div class="eva-search">
<input type="text" id="courseSearch" placeholder="Search courses and notebooks...">
<span class="eva-search-icon"><i class="fas fa-search"></i></span>
</div>
<div class="eva-courses">
"""
)
# Sort courses alphabetically
sorted_courses = sorted(courses.values(), key=lambda x: x["title"])
for course in sorted_courses:
# Skip if no notebooks
if not course["notebooks"]:
continue
f.write(
f'<div class="eva-course" data-course-id="{course["id"]}">\n'
f' <div class="eva-course-header">\n'
f' <h2 class="eva-course-title">{course["title"]}</h2>\n'
f' <span class="eva-course-toggle"><i class="fas fa-chevron-down"></i></span>\n'
f' </div>\n'
f' <div class="eva-course-content">\n'
f' <p class="eva-course-description">{course["description"]}</p>\n'
f' <div class="eva-notebooks">\n'
)
for i, notebook in enumerate(course["notebooks"]):
notebook_number = f"{i+1:02d}"
f.write(
f' <div class="eva-notebook">\n'
f' <span class="eva-notebook-number">{notebook_number}</span>\n'
f' <a href="{notebook["path"].replace(".py", ".html")}" data-notebook-title="{notebook["display_name"]}">{notebook["display_name"]}</a>\n'
f' </div>\n'
)
f.write(
f' </div>\n'
f' </div>\n'
f'</div>\n'
)
f.write(
""" </div>
</section>
<section id="contribute" class="eva-cta">
<h2>Contribute to Marimo Learn</h2>
<p>
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!
</p>
<a href="https://github.com/marimo-team/learn" target="_blank" class="eva-button">
<i class="fab fa-github"></i> Contribute on GitHub
</a>
</section>
<footer class="eva-footer">
<div class="eva-footer-copyright">
© 2024 Marimo Learn. Built with <a href="https://marimo.io" target="_blank" style="color: var(--eva-green);">marimo</a>.
</div>
<div class="eva-footer-links">
<a href="https://marimo.io" target="_blank">Marimo Website</a>
<a href="https://docs.marimo.io" target="_blank">Documentation</a>
<a href="https://github.com/marimo-team/learn" target="_blank">GitHub</a>
</div>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Terminal typing effect for hero text
const heroTitle = document.querySelector('.eva-hero h1');
const heroText = document.querySelector('.eva-hero p');
const cursor = document.querySelector('.eva-cursor');
const originalTitle = heroTitle.textContent;
const originalText = heroText.textContent.trim();
heroTitle.textContent = '';
heroText.textContent = '';
let titleIndex = 0;
let textIndex = 0;
function typeTitle() {
if (titleIndex < originalTitle.length) {
heroTitle.textContent += originalTitle.charAt(titleIndex);
titleIndex++;
setTimeout(typeTitle, 50);
} else {
cursor.style.display = 'none';
setTimeout(typeText, 500);
}
}
function typeText() {
if (textIndex < originalText.length) {
heroText.textContent += originalText.charAt(textIndex);
textIndex++;
setTimeout(typeText, 20);
}
}
typeTitle();
// Course toggle functionality
const courseHeaders = document.querySelectorAll('.eva-course-header');
courseHeaders.forEach(header => {
header.addEventListener('click', () => {
const course = header.parentElement;
course.classList.toggle('active');
});
});
// Search functionality
const searchInput = document.getElementById('courseSearch');
const courses = document.querySelectorAll('.eva-course');
const notebooks = document.querySelectorAll('.eva-notebook');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
if (searchTerm === '') {
// Reset all visibility
courses.forEach(course => {
course.style.display = 'block';
course.classList.remove('active');
});
notebooks.forEach(notebook => {
notebook.style.display = 'flex';
});
return;
}
// First hide all courses
courses.forEach(course => {
course.style.display = 'none';
course.classList.remove('active');
});
// Then show courses and notebooks that match the search
let hasResults = false;
notebooks.forEach(notebook => {
const notebookTitle = notebook.querySelector('a').getAttribute('data-notebook-title').toLowerCase();
const matchesSearch = notebookTitle.includes(searchTerm);
notebook.style.display = matchesSearch ? 'flex' : 'none';
if (matchesSearch) {
const course = notebook.closest('.eva-course');
course.style.display = 'block';
course.classList.add('active');
hasResults = true;
}
});
// Also search course titles
courses.forEach(course => {
const courseTitle = course.querySelector('.eva-course-title').textContent.toLowerCase();
const courseDescription = course.querySelector('.eva-course-description').textContent.toLowerCase();
if (courseTitle.includes(searchTerm) || courseDescription.includes(searchTerm)) {
course.style.display = 'block';
course.classList.add('active');
hasResults = true;
}
});
});
// Open the first course by default
if (courses.length > 0) {
courses[0].classList.add('active');
}
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 100,
behavior: 'smooth'
});
}
});
});
});
</script>
</body>
</html>"""
)
except IOError as e:
print(f"Error generating index.html: {e}")
def main() -> None:
parser = argparse.ArgumentParser(description="Build marimo notebooks")
parser.add_argument(
"--output-dir", default="_site", help="Output directory for built files"
)
parser.add_argument(
"--course-dirs", nargs="+", default=None,
help="Specific course directories to build (default: all directories with .py files)"
)
args = parser.parse_args()
# Find all course directories (directories containing .py files)
all_notebooks: List[str] = []
# Directories to exclude from course detection
excluded_dirs = ["scripts", "env", "__pycache__", ".git", ".github", "assets"]
if args.course_dirs:
course_dirs = args.course_dirs
else:
# Automatically detect course directories (any directory with .py files)
course_dirs = []
for item in os.listdir("."):
if (os.path.isdir(item) and
not item.startswith(".") and
not item.startswith("_") and
item not in excluded_dirs):
# Check if directory contains .py files
if list(Path(item).glob("*.py")):
course_dirs.append(item)
print(f"Found course directories: {', '.join(course_dirs)}")
for directory in course_dirs:
dir_path = Path(directory)
if not dir_path.exists():
print(f"Warning: Directory not found: {dir_path}")
continue
notebooks = [str(path) for path in dir_path.rglob("*.py")
if not path.name.startswith("_") and "/__pycache__/" not in str(path)]
all_notebooks.extend(notebooks)
if not all_notebooks:
print("No notebooks found!")
return
# Export notebooks sequentially
successful_notebooks = []
for nb in all_notebooks:
# Determine if notebook should be exported as app or notebook
# For now, export all as notebooks
if export_html_wasm(nb, args.output_dir, as_app=False):
successful_notebooks.append(nb)
# Organize notebooks by course (only include successfully exported notebooks)
courses = organize_notebooks_by_course(successful_notebooks)
# Generate index with organized courses
generate_index(courses, args.output_dir)
# Save course data as JSON for potential use by other tools
courses_json_path = os.path.join(args.output_dir, "courses.json")
with open(courses_json_path, "w", encoding="utf-8") as f:
json.dump(courses, f, indent=2)
print(f"Build complete! Site generated in {args.output_dir}")
print(f"Successfully exported {len(successful_notebooks)} out of {len(all_notebooks)} notebooks")
if __name__ == "__main__":
main()