Spaces:
Sleeping
Sleeping
#!/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() | |