Spaces:
Running
Running
File size: 10,417 Bytes
e9d126c 77b9c0b a7e66b2 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b a7e66b2 77b9c0b e9d126c 77b9c0b a7e66b2 e9d126c 77b9c0b a7e66b2 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b a7e66b2 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b f09994a 77b9c0b f09994a 77b9c0b f09994a 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b e9d126c 77b9c0b d3bf5bc 77b9c0b d3bf5bc 77b9c0b d3bf5bc 77b9c0b d3bf5bc e9d126c 77b9c0b e9d126c 77b9c0b e9d126c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
#!/usr/bin/env python3
import os
import subprocess
import argparse
import json
import datetime
import markdown
from datetime import date
from pathlib import Path
from typing import Dict, List, Any, Optional, Tuple
from jinja2 import Environment, FileSystemLoader
def export_html_wasm(notebook_path: str, output_dir: str, as_app: bool = False) -> bool:
"""Export a single marimo notebook to HTML format.
Args:
notebook_path: Path to the notebook to export
output_dir: Directory to write the output HTML files
as_app: If True, export as app instead of notebook
Returns:
bool: True if export succeeded, False otherwise
"""
# Create directory for the output
os.makedirs(output_dir, exist_ok=True)
# Determine the output path (preserving directory structure)
rel_path = os.path.basename(os.path.dirname(notebook_path))
if rel_path != os.path.dirname(notebook_path):
# Create subdirectory if needed
os.makedirs(os.path.join(output_dir, rel_path), exist_ok=True)
# Determine output filename (same as input but with .html extension)
output_filename = os.path.basename(notebook_path).replace(".py", ".html")
output_path = os.path.join(output_dir, rel_path, output_filename)
# Run marimo export command
mode = "--mode app" if as_app else "--mode edit"
cmd = f"marimo export html-wasm {mode} {notebook_path} -o {output_path} --sandbox"
print(f"Exporting {notebook_path} to {rel_path}/{output_filename} as {'app' if as_app else 'notebook'}")
print(f"Running command: {cmd}")
try:
result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True)
print(f"Successfully exported {notebook_path} to {output_path}")
return True
except subprocess.CalledProcessError as e:
print(f"Error exporting {notebook_path}: {e}")
print(f"Command output: {e.output}")
return False
def get_course_metadata(course_dir: Path) -> Dict[str, Any]:
"""Extract metadata from a course directory.
Reads the README.md file to extract title and description.
Args:
course_dir: Path to the course directory
Returns:
Dict: Dictionary containing course metadata (title, description)
"""
readme_path = course_dir / "README.md"
title = course_dir.name.replace("_", " ").title()
description = ""
description_html = ""
if readme_path.exists():
with open(readme_path, "r", encoding="utf-8") as f:
content = f.read()
# Try to extract title from first heading
title_match = content.split("\n")[0]
if title_match.startswith("# "):
title = title_match[2:].strip()
# Extract description from content after first heading
desc_content = "\n".join(content.split("\n")[1:]).strip()
if desc_content:
# Take first paragraph as description, preserve markdown formatting
description = desc_content.split("\n\n")[0].strip()
# Convert markdown to HTML
description_html = markdown.markdown(description)
return {
"title": title,
"description": description,
"description_html": description_html
}
def organize_notebooks_by_course(all_notebooks: List[str]) -> Dict[str, Dict[str, Any]]:
"""Organize notebooks by course.
Args:
all_notebooks: List of paths to notebooks
Returns:
Dict: A dictionary where keys are course directories and values are
metadata about the course and its notebooks
"""
courses = {}
for notebook_path in sorted(all_notebooks):
# Parse the path to determine course
# The first directory in the path is the course
path_parts = Path(notebook_path).parts
if len(path_parts) < 2:
print(f"Skipping notebook with invalid path: {notebook_path}")
continue
course_id = path_parts[0]
# If this is a new course, initialize it
if course_id not in courses:
course_metadata = get_course_metadata(Path(course_id))
courses[course_id] = {
"id": course_id,
"title": course_metadata["title"],
"description": course_metadata["description"],
"description_html": course_metadata["description_html"],
"notebooks": []
}
# Extract the notebook number and name from the filename
filename = Path(notebook_path).name
basename = filename.replace(".py", "")
# Extract notebook metadata
notebook_title = basename.replace("_", " ").title()
# Try to extract a sequence number from the start of the filename
# Match patterns like: 01_xxx, 1_xxx, etc.
import re
number_match = re.match(r'^(\d+)(?:[_-]|$)', basename)
notebook_number = number_match.group(1) if number_match else None
# If we found a number, remove it from the title
if number_match:
notebook_title = re.sub(r'^\d+\s*[_-]?\s*', '', notebook_title)
# Calculate the HTML output path (for linking)
html_path = f"{course_id}/{filename.replace('.py', '.html')}"
# Add the notebook to the course
courses[course_id]["notebooks"].append({
"path": notebook_path,
"html_path": html_path,
"title": notebook_title,
"display_name": notebook_title,
"original_number": notebook_number
})
# Sort notebooks by number if available, otherwise by title
for course_id, course_data in courses.items():
# Sort the notebooks list by number and title
course_data["notebooks"] = sorted(
course_data["notebooks"],
key=lambda x: (
int(x["original_number"]) if x["original_number"] is not None else float('inf'),
x["title"]
)
)
return courses
def generate_clean_tailwind_landing_page(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
"""Generate a clean tailwindcss landing page with green accents.
This generates a modern, minimal landing page for marimo notebooks using tailwindcss.
The page is designed with clean aesthetics and green color accents using Jinja2 templates.
Args:
courses: Dictionary of courses metadata
output_dir: Directory to write the output index.html file
"""
print("Generating clean tailwindcss landing page")
index_path = os.path.join(output_dir, "index.html")
os.makedirs(output_dir, exist_ok=True)
# Load Jinja2 template
current_dir = Path(__file__).parent
templates_dir = current_dir / "templates"
env = Environment(loader=FileSystemLoader(templates_dir))
template = env.get_template('index.html')
try:
with open(index_path, "w", encoding="utf-8") as f:
# Render the template with the provided data
rendered_html = template.render(
courses=courses,
current_year=datetime.date.today().year
)
f.write(rendered_html)
print(f"Successfully generated clean tailwindcss landing page at {index_path}")
except IOError as e:
print(f"Error generating clean tailwindcss landing page: {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 landing page using Tailwind CSS
generate_clean_tailwind_landing_page(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()
|