Spaces:
Sleeping
Sleeping
Refactor HTML generation in build.py by modularizing components (format) into dedicated functions for better readability and maintainability.
Browse files- scripts/build.py +201 -151
scripts/build.py
CHANGED
|
@@ -30,8 +30,33 @@ def export_html_wasm(notebook_path: str, output_dir: str, as_app: bool = False)
|
|
| 30 |
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
| 31 |
|
| 32 |
cmd.extend([notebook_path, "-o", output_file])
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
return True
|
|
|
|
|
|
|
|
|
|
| 35 |
except subprocess.CalledProcessError as e:
|
| 36 |
print(f"Error exporting {notebook_path}:")
|
| 37 |
print(e.stderr)
|
|
@@ -880,17 +905,9 @@ def generate_eva_css() -> str:
|
|
| 880 |
"""
|
| 881 |
|
| 882 |
|
| 883 |
-
def
|
| 884 |
-
"""Generate the
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
index_path = os.path.join(output_dir, "index.html")
|
| 888 |
-
os.makedirs(output_dir, exist_ok=True)
|
| 889 |
-
|
| 890 |
-
try:
|
| 891 |
-
with open(index_path, "w", encoding="utf-8") as f:
|
| 892 |
-
f.write(
|
| 893 |
-
"""<!DOCTYPE html>
|
| 894 |
<html lang="en" data-theme="light">
|
| 895 |
<head>
|
| 896 |
<meta charset="UTF-8">
|
|
@@ -898,7 +915,7 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 898 |
<title>Marimo Learn - Interactive Educational Notebooks</title>
|
| 899 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 900 |
<style>
|
| 901 |
-
|
| 902 |
</style>
|
| 903 |
</head>
|
| 904 |
<body>
|
|
@@ -915,8 +932,12 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 915 |
<i class="fas fa-moon"></i>
|
| 916 |
</button>
|
| 917 |
</nav>
|
| 918 |
-
</header>
|
|
|
|
| 919 |
|
|
|
|
|
|
|
|
|
|
| 920 |
<section class="eva-hero">
|
| 921 |
<h1>Interactive Learning with Marimo<span class="eva-cursor"></span></h1>
|
| 922 |
<p>
|
|
@@ -925,8 +946,12 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 925 |
Python notebook that makes data exploration delightful.
|
| 926 |
</p>
|
| 927 |
<a href="#courses" class="eva-button">Explore Courses</a>
|
| 928 |
-
</section>
|
|
|
|
| 929 |
|
|
|
|
|
|
|
|
|
|
| 930 |
<section id="features">
|
| 931 |
<h2 class="eva-section-title">Why Marimo Learn?</h2>
|
| 932 |
<div class="eva-features">
|
|
@@ -946,150 +971,128 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 946 |
<p>From Python basics to advanced optimization techniques, our courses cover a wide range of topics.</p>
|
| 947 |
</div>
|
| 948 |
</div>
|
| 949 |
-
</section>
|
| 950 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
<section id="courses">
|
| 952 |
<h2 class="eva-section-title">Explore Courses</h2>
|
| 953 |
<div class="eva-search">
|
| 954 |
<input type="text" id="courseSearch" placeholder="Search courses and notebooks...">
|
| 955 |
<span class="eva-search-icon"><i class="fas fa-search"></i></span>
|
| 956 |
</div>
|
| 957 |
-
<div class="eva-courses">
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 963 |
|
| 964 |
-
#
|
| 965 |
-
|
|
|
|
| 966 |
|
| 967 |
-
#
|
| 968 |
-
|
| 969 |
-
for course_id, course in courses_by_id.items():
|
| 970 |
-
# Consider a course as "work in progress" if it has few notebooks or contains specific phrases
|
| 971 |
-
if (len(course["notebooks"]) < 5 or
|
| 972 |
-
"work in progress" in course["description"].lower() or
|
| 973 |
-
"help us add" in course["description"].lower() or
|
| 974 |
-
"check back later" in course["description"].lower()):
|
| 975 |
-
work_in_progress.add(course_id)
|
| 976 |
|
| 977 |
-
#
|
| 978 |
-
|
| 979 |
-
if course_id in courses_by_id:
|
| 980 |
-
course = courses_by_id[course_id]
|
| 981 |
-
|
| 982 |
-
# Skip if no notebooks
|
| 983 |
-
if not course["notebooks"]:
|
| 984 |
-
continue
|
| 985 |
-
|
| 986 |
-
# Count notebooks
|
| 987 |
-
notebook_count = len(course["notebooks"])
|
| 988 |
-
|
| 989 |
-
# Determine if this course is a work in progress
|
| 990 |
-
is_wip = course_id in work_in_progress
|
| 991 |
-
|
| 992 |
-
f.write(
|
| 993 |
-
f'<div class="eva-course" data-course-id="{course["id"]}">\n'
|
| 994 |
-
)
|
| 995 |
-
|
| 996 |
-
# Add WIP badge if needed
|
| 997 |
-
if is_wip:
|
| 998 |
-
f.write(f' <div class="eva-course-badge"><i class="fas fa-code-branch"></i> In Progress</div>\n')
|
| 999 |
-
|
| 1000 |
-
f.write(
|
| 1001 |
-
f' <div class="eva-course-header">\n'
|
| 1002 |
-
f' <h2 class="eva-course-title">{course["title"]}</h2>\n'
|
| 1003 |
-
f' <span class="eva-course-toggle"><i class="fas fa-chevron-down"></i></span>\n'
|
| 1004 |
-
f' </div>\n'
|
| 1005 |
-
f' <div class="eva-course-front">\n'
|
| 1006 |
-
f' <p class="eva-course-description">{course["description"]}</p>\n'
|
| 1007 |
-
f' <div class="eva-course-stats">\n'
|
| 1008 |
-
f' <span><i class="fas fa-book"></i> {notebook_count} notebook{"s" if notebook_count != 1 else ""}</span>\n'
|
| 1009 |
-
f' </div>\n'
|
| 1010 |
-
f' <button class="eva-button eva-course-button">View Notebooks</button>\n'
|
| 1011 |
-
f' </div>\n'
|
| 1012 |
-
f' <div class="eva-course-content">\n'
|
| 1013 |
-
f' <div class="eva-notebooks">\n'
|
| 1014 |
-
)
|
| 1015 |
-
|
| 1016 |
-
for i, notebook in enumerate(course["notebooks"]):
|
| 1017 |
-
# Use original file number instead of sequential numbering
|
| 1018 |
-
notebook_number = notebook.get("original_number", f"{i+1:02d}")
|
| 1019 |
-
f.write(
|
| 1020 |
-
f' <div class="eva-notebook">\n'
|
| 1021 |
-
f' <span class="eva-notebook-number">{notebook_number}</span>\n'
|
| 1022 |
-
f' <a href="{notebook["path"].replace(".py", ".html")}" data-notebook-title="{notebook["display_name"]}">{notebook["display_name"]}</a>\n'
|
| 1023 |
-
f' </div>\n'
|
| 1024 |
-
)
|
| 1025 |
-
|
| 1026 |
-
f.write(
|
| 1027 |
-
f' </div>\n'
|
| 1028 |
-
f' </div>\n'
|
| 1029 |
-
f'</div>\n'
|
| 1030 |
-
)
|
| 1031 |
-
|
| 1032 |
-
# Remove from the dictionary so we don't output it again
|
| 1033 |
-
del courses_by_id[course_id]
|
| 1034 |
|
| 1035 |
-
|
| 1036 |
-
sorted_remaining_courses = sorted(courses_by_id.values(), key=lambda x: x["title"])
|
| 1037 |
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
| 1042 |
-
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
f' <div class="eva-course-stats">\n'
|
| 1065 |
-
f' <span><i class="fas fa-book"></i> {notebook_count} notebook{"s" if notebook_count != 1 else ""}</span>\n'
|
| 1066 |
-
f' </div>\n'
|
| 1067 |
-
f' <button class="eva-button eva-course-button">View Notebooks</button>\n'
|
| 1068 |
-
f' </div>\n'
|
| 1069 |
-
f' <div class="eva-course-content">\n'
|
| 1070 |
-
f' <div class="eva-notebooks">\n'
|
| 1071 |
-
)
|
| 1072 |
-
|
| 1073 |
-
for i, notebook in enumerate(course["notebooks"]):
|
| 1074 |
-
# Use original file number instead of sequential numbering
|
| 1075 |
-
notebook_number = notebook.get("original_number", f"{i+1:02d}")
|
| 1076 |
-
f.write(
|
| 1077 |
-
f' <div class="eva-notebook">\n'
|
| 1078 |
-
f' <span class="eva-notebook-number">{notebook_number}</span>\n'
|
| 1079 |
-
f' <a href="{notebook["path"].replace(".py", ".html")}" data-notebook-title="{notebook["display_name"]}">{notebook["display_name"]}</a>\n'
|
| 1080 |
-
f' </div>\n'
|
| 1081 |
-
)
|
| 1082 |
|
| 1083 |
-
f.write(
|
| 1084 |
-
f' </div>\n'
|
| 1085 |
-
f' </div>\n'
|
| 1086 |
-
f'</div>\n'
|
| 1087 |
-
)
|
| 1088 |
-
|
| 1089 |
-
f.write(
|
| 1090 |
-
""" </div>
|
| 1091 |
-
</section>
|
| 1092 |
|
|
|
|
|
|
|
|
|
|
| 1093 |
<section id="contribute" class="eva-cta">
|
| 1094 |
<h2>Contribute to Marimo Learn</h2>
|
| 1095 |
<p>
|
|
@@ -1099,8 +1102,12 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 1099 |
<a href="https://github.com/marimo-team/learn" target="_blank" class="eva-button">
|
| 1100 |
<i class="fab fa-github"></i> Contribute on GitHub
|
| 1101 |
</a>
|
| 1102 |
-
</section>
|
|
|
|
| 1103 |
|
|
|
|
|
|
|
|
|
|
| 1104 |
<footer class="eva-footer">
|
| 1105 |
<div class="eva-footer-logo">
|
| 1106 |
<a href="https://marimo.io" target="_blank">
|
|
@@ -1111,7 +1118,7 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 1111 |
<a href="https://github.com/marimo-team" target="_blank" aria-label="GitHub"><i class="fab fa-github"></i></a>
|
| 1112 |
<a href="https://marimo.io/discord?ref=learn" target="_blank" aria-label="Discord"><i class="fab fa-discord"></i></a>
|
| 1113 |
<a href="https://twitter.com/marimo_io" target="_blank" aria-label="Twitter"><i class="fab fa-twitter"></i></a>
|
| 1114 |
-
<a href="https://www.youtube.com/@marimo-
|
| 1115 |
<a href="https://www.linkedin.com/company/marimo-io" target="_blank" aria-label="LinkedIn"><i class="fab fa-linkedin"></i></a>
|
| 1116 |
</div>
|
| 1117 |
<div class="eva-footer-links">
|
|
@@ -1122,9 +1129,12 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 1122 |
<div class="eva-footer-copyright">
|
| 1123 |
© 2025 Marimo Inc. All rights reserved.
|
| 1124 |
</div>
|
| 1125 |
-
</footer>
|
| 1126 |
-
|
| 1127 |
|
|
|
|
|
|
|
|
|
|
| 1128 |
<script>
|
| 1129 |
// Set light theme as default immediately
|
| 1130 |
document.documentElement.setAttribute('data-theme', 'light');
|
|
@@ -1391,10 +1401,50 @@ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
|
| 1391 |
});
|
| 1392 |
});
|
| 1393 |
});
|
| 1394 |
-
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1395 |
</body>
|
| 1396 |
</html>"""
|
| 1397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1398 |
except IOError as e:
|
| 1399 |
print(f"Error generating index.html: {e}")
|
| 1400 |
|
|
|
|
| 30 |
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
| 31 |
|
| 32 |
cmd.extend([notebook_path, "-o", output_file])
|
| 33 |
+
print(f"Running command: {' '.join(cmd)}")
|
| 34 |
+
|
| 35 |
+
# Use Popen to handle interactive prompts
|
| 36 |
+
process = subprocess.Popen(
|
| 37 |
+
cmd,
|
| 38 |
+
stdin=subprocess.PIPE,
|
| 39 |
+
stdout=subprocess.PIPE,
|
| 40 |
+
stderr=subprocess.PIPE,
|
| 41 |
+
text=True
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Send 'Y' to the prompt
|
| 45 |
+
stdout, stderr = process.communicate(input="Y\n", timeout=60)
|
| 46 |
+
|
| 47 |
+
if process.returncode != 0:
|
| 48 |
+
print(f"Error exporting {notebook_path}:")
|
| 49 |
+
print(f"Command: {' '.join(cmd)}")
|
| 50 |
+
print(f"Return code: {process.returncode}")
|
| 51 |
+
print(f"Stdout: {stdout}")
|
| 52 |
+
print(f"Stderr: {stderr}")
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
print(f"Successfully exported {notebook_path} to {output_file}")
|
| 56 |
return True
|
| 57 |
+
except subprocess.TimeoutExpired:
|
| 58 |
+
print(f"Timeout exporting {notebook_path} - command took too long to execute")
|
| 59 |
+
return False
|
| 60 |
except subprocess.CalledProcessError as e:
|
| 61 |
print(f"Error exporting {notebook_path}:")
|
| 62 |
print(e.stderr)
|
|
|
|
| 905 |
"""
|
| 906 |
|
| 907 |
|
| 908 |
+
def get_html_header():
|
| 909 |
+
"""Generate the HTML header with CSS and meta tags."""
|
| 910 |
+
return """<!DOCTYPE html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 911 |
<html lang="en" data-theme="light">
|
| 912 |
<head>
|
| 913 |
<meta charset="UTF-8">
|
|
|
|
| 915 |
<title>Marimo Learn - Interactive Educational Notebooks</title>
|
| 916 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 917 |
<style>
|
| 918 |
+
{css}
|
| 919 |
</style>
|
| 920 |
</head>
|
| 921 |
<body>
|
|
|
|
| 932 |
<i class="fas fa-moon"></i>
|
| 933 |
</button>
|
| 934 |
</nav>
|
| 935 |
+
</header>"""
|
| 936 |
+
|
| 937 |
|
| 938 |
+
def get_html_hero_section():
|
| 939 |
+
"""Generate the hero section of the page."""
|
| 940 |
+
return """
|
| 941 |
<section class="eva-hero">
|
| 942 |
<h1>Interactive Learning with Marimo<span class="eva-cursor"></span></h1>
|
| 943 |
<p>
|
|
|
|
| 946 |
Python notebook that makes data exploration delightful.
|
| 947 |
</p>
|
| 948 |
<a href="#courses" class="eva-button">Explore Courses</a>
|
| 949 |
+
</section>"""
|
| 950 |
+
|
| 951 |
|
| 952 |
+
def get_html_features_section():
|
| 953 |
+
"""Generate the features section of the page."""
|
| 954 |
+
return """
|
| 955 |
<section id="features">
|
| 956 |
<h2 class="eva-section-title">Why Marimo Learn?</h2>
|
| 957 |
<div class="eva-features">
|
|
|
|
| 971 |
<p>From Python basics to advanced optimization techniques, our courses cover a wide range of topics.</p>
|
| 972 |
</div>
|
| 973 |
</div>
|
| 974 |
+
</section>"""
|
| 975 |
|
| 976 |
+
|
| 977 |
+
def get_html_courses_start():
|
| 978 |
+
"""Generate the beginning of the courses section."""
|
| 979 |
+
return """
|
| 980 |
<section id="courses">
|
| 981 |
<h2 class="eva-section-title">Explore Courses</h2>
|
| 982 |
<div class="eva-search">
|
| 983 |
<input type="text" id="courseSearch" placeholder="Search courses and notebooks...">
|
| 984 |
<span class="eva-search-icon"><i class="fas fa-search"></i></span>
|
| 985 |
</div>
|
| 986 |
+
<div class="eva-courses">"""
|
| 987 |
+
|
| 988 |
+
|
| 989 |
+
def generate_course_card(course, notebook_count, is_wip):
|
| 990 |
+
"""Generate HTML for a single course card."""
|
| 991 |
+
html = f'<div class="eva-course" data-course-id="{course["id"]}">\n'
|
| 992 |
+
|
| 993 |
+
# Add WIP badge if needed
|
| 994 |
+
if is_wip:
|
| 995 |
+
html += ' <div class="eva-course-badge"><i class="fas fa-code-branch"></i> In Progress</div>\n'
|
| 996 |
+
|
| 997 |
+
html += f''' <div class="eva-course-header">
|
| 998 |
+
<h2 class="eva-course-title">{course["title"]}</h2>
|
| 999 |
+
<span class="eva-course-toggle"><i class="fas fa-chevron-down"></i></span>
|
| 1000 |
+
</div>
|
| 1001 |
+
<div class="eva-course-front">
|
| 1002 |
+
<p class="eva-course-description">{course["description"]}</p>
|
| 1003 |
+
<div class="eva-course-stats">
|
| 1004 |
+
<span><i class="fas fa-book"></i> {notebook_count} notebook{"s" if notebook_count != 1 else ""}</span>
|
| 1005 |
+
</div>
|
| 1006 |
+
<button class="eva-button eva-course-button">View Notebooks</button>
|
| 1007 |
+
</div>
|
| 1008 |
+
<div class="eva-course-content">
|
| 1009 |
+
<div class="eva-notebooks">
|
| 1010 |
+
'''
|
| 1011 |
+
|
| 1012 |
+
# Add notebooks
|
| 1013 |
+
for i, notebook in enumerate(course["notebooks"]):
|
| 1014 |
+
notebook_number = notebook.get("original_number", f"{i+1:02d}")
|
| 1015 |
+
html += f''' <div class="eva-notebook">
|
| 1016 |
+
<span class="eva-notebook-number">{notebook_number}</span>
|
| 1017 |
+
<a href="{notebook["path"].replace(".py", ".html")}" data-notebook-title="{notebook["display_name"]}">{notebook["display_name"]}</a>
|
| 1018 |
+
</div>
|
| 1019 |
+
'''
|
| 1020 |
+
|
| 1021 |
+
html += ''' </div>
|
| 1022 |
+
</div>
|
| 1023 |
+
</div>
|
| 1024 |
+
'''
|
| 1025 |
+
return html
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
def generate_course_cards(courses):
|
| 1029 |
+
"""Generate HTML for all course cards."""
|
| 1030 |
+
html = ""
|
| 1031 |
+
|
| 1032 |
+
# Define the custom order for courses
|
| 1033 |
+
course_order = ["python", "probability", "polars", "optimization", "functional_programming"]
|
| 1034 |
+
|
| 1035 |
+
# Create a dictionary of courses by ID for easy lookup
|
| 1036 |
+
courses_by_id = {course["id"]: course for course in courses.values()}
|
| 1037 |
+
|
| 1038 |
+
# Determine which courses are "work in progress" based on description or notebook count
|
| 1039 |
+
work_in_progress = set()
|
| 1040 |
+
for course_id, course in courses_by_id.items():
|
| 1041 |
+
# Consider a course as "work in progress" if it has few notebooks or contains specific phrases
|
| 1042 |
+
if (len(course["notebooks"]) < 5 or
|
| 1043 |
+
"work in progress" in course["description"].lower() or
|
| 1044 |
+
"help us add" in course["description"].lower() or
|
| 1045 |
+
"check back later" in course["description"].lower()):
|
| 1046 |
+
work_in_progress.add(course_id)
|
| 1047 |
+
|
| 1048 |
+
# First output courses in the specified order
|
| 1049 |
+
for course_id in course_order:
|
| 1050 |
+
if course_id in courses_by_id:
|
| 1051 |
+
course = courses_by_id[course_id]
|
| 1052 |
|
| 1053 |
+
# Skip if no notebooks
|
| 1054 |
+
if not course["notebooks"]:
|
| 1055 |
+
continue
|
| 1056 |
|
| 1057 |
+
# Count notebooks
|
| 1058 |
+
notebook_count = len(course["notebooks"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1059 |
|
| 1060 |
+
# Determine if this course is a work in progress
|
| 1061 |
+
is_wip = course_id in work_in_progress
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1062 |
|
| 1063 |
+
html += generate_course_card(course, notebook_count, is_wip)
|
|
|
|
| 1064 |
|
| 1065 |
+
# Remove from the dictionary so we don't output it again
|
| 1066 |
+
del courses_by_id[course_id]
|
| 1067 |
+
|
| 1068 |
+
# Then output any remaining courses alphabetically
|
| 1069 |
+
sorted_remaining_courses = sorted(courses_by_id.values(), key=lambda x: x["title"])
|
| 1070 |
+
|
| 1071 |
+
for course in sorted_remaining_courses:
|
| 1072 |
+
# Skip if no notebooks
|
| 1073 |
+
if not course["notebooks"]:
|
| 1074 |
+
continue
|
| 1075 |
+
|
| 1076 |
+
# Count notebooks
|
| 1077 |
+
notebook_count = len(course["notebooks"])
|
| 1078 |
+
|
| 1079 |
+
# Determine if this course is a work in progress
|
| 1080 |
+
is_wip = course["id"] in work_in_progress
|
| 1081 |
+
|
| 1082 |
+
html += generate_course_card(course, notebook_count, is_wip)
|
| 1083 |
+
|
| 1084 |
+
return html
|
| 1085 |
+
|
| 1086 |
+
|
| 1087 |
+
def get_html_courses_end():
|
| 1088 |
+
"""Generate the end of the courses section."""
|
| 1089 |
+
return """ </div>
|
| 1090 |
+
</section>"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1091 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1092 |
|
| 1093 |
+
def get_html_contribute_section():
|
| 1094 |
+
"""Generate the contribute section."""
|
| 1095 |
+
return """
|
| 1096 |
<section id="contribute" class="eva-cta">
|
| 1097 |
<h2>Contribute to Marimo Learn</h2>
|
| 1098 |
<p>
|
|
|
|
| 1102 |
<a href="https://github.com/marimo-team/learn" target="_blank" class="eva-button">
|
| 1103 |
<i class="fab fa-github"></i> Contribute on GitHub
|
| 1104 |
</a>
|
| 1105 |
+
</section>"""
|
| 1106 |
+
|
| 1107 |
|
| 1108 |
+
def get_html_footer():
|
| 1109 |
+
"""Generate the page footer."""
|
| 1110 |
+
return """
|
| 1111 |
<footer class="eva-footer">
|
| 1112 |
<div class="eva-footer-logo">
|
| 1113 |
<a href="https://marimo.io" target="_blank">
|
|
|
|
| 1118 |
<a href="https://github.com/marimo-team" target="_blank" aria-label="GitHub"><i class="fab fa-github"></i></a>
|
| 1119 |
<a href="https://marimo.io/discord?ref=learn" target="_blank" aria-label="Discord"><i class="fab fa-discord"></i></a>
|
| 1120 |
<a href="https://twitter.com/marimo_io" target="_blank" aria-label="Twitter"><i class="fab fa-twitter"></i></a>
|
| 1121 |
+
<a href="https://www.youtube.com/@marimo-team" target="_blank" aria-label="YouTube"><i class="fab fa-youtube"></i></a>
|
| 1122 |
<a href="https://www.linkedin.com/company/marimo-io" target="_blank" aria-label="LinkedIn"><i class="fab fa-linkedin"></i></a>
|
| 1123 |
</div>
|
| 1124 |
<div class="eva-footer-links">
|
|
|
|
| 1129 |
<div class="eva-footer-copyright">
|
| 1130 |
© 2025 Marimo Inc. All rights reserved.
|
| 1131 |
</div>
|
| 1132 |
+
</footer>"""
|
| 1133 |
+
|
| 1134 |
|
| 1135 |
+
def get_html_scripts():
|
| 1136 |
+
"""Generate the JavaScript for the page."""
|
| 1137 |
+
return """
|
| 1138 |
<script>
|
| 1139 |
// Set light theme as default immediately
|
| 1140 |
document.documentElement.setAttribute('data-theme', 'light');
|
|
|
|
| 1401 |
});
|
| 1402 |
});
|
| 1403 |
});
|
| 1404 |
+
</script>"""
|
| 1405 |
+
|
| 1406 |
+
|
| 1407 |
+
def get_html_footer_closing():
|
| 1408 |
+
"""Generate closing HTML tags."""
|
| 1409 |
+
return """
|
| 1410 |
+
</div>
|
| 1411 |
</body>
|
| 1412 |
</html>"""
|
| 1413 |
+
|
| 1414 |
+
|
| 1415 |
+
def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
|
| 1416 |
+
"""Generate the index.html file with Neon Genesis Evangelion aesthetics."""
|
| 1417 |
+
print("Generating index.html")
|
| 1418 |
+
|
| 1419 |
+
index_path = os.path.join(output_dir, "index.html")
|
| 1420 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 1421 |
+
|
| 1422 |
+
try:
|
| 1423 |
+
with open(index_path, "w", encoding="utf-8") as f:
|
| 1424 |
+
# Build the page HTML from individual components
|
| 1425 |
+
header = get_html_header().format(css=generate_eva_css())
|
| 1426 |
+
hero = get_html_hero_section()
|
| 1427 |
+
features = get_html_features_section()
|
| 1428 |
+
courses_start = get_html_courses_start()
|
| 1429 |
+
course_cards = generate_course_cards(courses)
|
| 1430 |
+
courses_end = get_html_courses_end()
|
| 1431 |
+
contribute = get_html_contribute_section()
|
| 1432 |
+
footer = get_html_footer()
|
| 1433 |
+
scripts = get_html_scripts()
|
| 1434 |
+
closing = get_html_footer_closing()
|
| 1435 |
+
|
| 1436 |
+
# Write all elements to the file
|
| 1437 |
+
f.write(header)
|
| 1438 |
+
f.write(hero)
|
| 1439 |
+
f.write(features)
|
| 1440 |
+
f.write(courses_start)
|
| 1441 |
+
f.write(course_cards)
|
| 1442 |
+
f.write(courses_end)
|
| 1443 |
+
f.write(contribute)
|
| 1444 |
+
f.write(footer)
|
| 1445 |
+
f.write(scripts)
|
| 1446 |
+
f.write(closing)
|
| 1447 |
+
|
| 1448 |
except IOError as e:
|
| 1449 |
print(f"Error generating index.html: {e}")
|
| 1450 |
|