Haleshot commited on
Commit
e9d126c
·
unverified ·
1 Parent(s): 0aa6802

Add Neon Genesis Evangelion themed landing page

Browse files
Files changed (2) hide show
  1. scripts/build.py +962 -0
  2. scripts/preview.py +76 -0
scripts/build.py ADDED
@@ -0,0 +1,962 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import subprocess
5
+ import argparse
6
+ import json
7
+ from typing import List, Dict, Any
8
+ from pathlib import Path
9
+
10
+
11
+ def export_html_wasm(notebook_path: str, output_dir: str, as_app: bool = False) -> bool:
12
+ """Export a single marimo notebook to HTML format.
13
+
14
+ Returns:
15
+ bool: True if export succeeded, False otherwise
16
+ """
17
+ output_path = notebook_path.replace(".py", ".html")
18
+
19
+ cmd = ["marimo", "export", "html-wasm"]
20
+ if as_app:
21
+ print(f"Exporting {notebook_path} to {output_path} as app")
22
+ cmd.extend(["--mode", "run", "--no-show-code"])
23
+ else:
24
+ print(f"Exporting {notebook_path} to {output_path} as notebook")
25
+ cmd.extend(["--mode", "edit"])
26
+
27
+ try:
28
+ output_file = os.path.join(output_dir, output_path)
29
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
30
+
31
+ cmd.extend([notebook_path, "-o", output_file])
32
+ subprocess.run(cmd, capture_output=True, text=True, check=True)
33
+ return True
34
+ except subprocess.CalledProcessError as e:
35
+ print(f"Error exporting {notebook_path}:")
36
+ print(e.stderr)
37
+ return False
38
+ except Exception as e:
39
+ print(f"Unexpected error exporting {notebook_path}: {e}")
40
+ return False
41
+
42
+
43
+ def get_course_metadata(course_dir: Path) -> Dict[str, Any]:
44
+ """Extract metadata from a course directory."""
45
+ metadata = {
46
+ "id": course_dir.name,
47
+ "title": course_dir.name.replace("_", " ").title(),
48
+ "description": "",
49
+ "notebooks": []
50
+ }
51
+
52
+ # Try to read README.md for description
53
+ readme_path = course_dir / "README.md"
54
+ if readme_path.exists():
55
+ with open(readme_path, "r", encoding="utf-8") as f:
56
+ content = f.read()
57
+ # Extract first paragraph as description
58
+ if content:
59
+ lines = content.split("\n")
60
+ # Skip title line if it exists
61
+ start_idx = 1 if lines and lines[0].startswith("#") else 0
62
+ description_lines = []
63
+ for line in lines[start_idx:]:
64
+ if line.strip() and not line.startswith("#"):
65
+ description_lines.append(line)
66
+ elif description_lines: # Stop at the next heading
67
+ break
68
+ metadata["description"] = " ".join(description_lines).strip()
69
+
70
+ return metadata
71
+
72
+
73
+ def organize_notebooks_by_course(all_notebooks: List[str]) -> Dict[str, Dict[str, Any]]:
74
+ """Organize notebooks by course."""
75
+ courses = {}
76
+
77
+ for notebook_path in all_notebooks:
78
+ path = Path(notebook_path)
79
+ course_id = path.parts[0]
80
+
81
+ if course_id not in courses:
82
+ course_dir = Path(course_id)
83
+ courses[course_id] = get_course_metadata(course_dir)
84
+
85
+ # Extract notebook info
86
+ filename = path.name
87
+ notebook_id = path.stem
88
+
89
+ # Try to extract order from filename (e.g., 001_numbers.py -> 1)
90
+ order = 999
91
+ if "_" in notebook_id:
92
+ try:
93
+ order_str = notebook_id.split("_")[0]
94
+ order = int(order_str)
95
+ except ValueError:
96
+ pass
97
+
98
+ # Create display name by removing order prefix and underscores
99
+ display_name = notebook_id
100
+ if "_" in notebook_id:
101
+ display_name = "_".join(notebook_id.split("_")[1:])
102
+ display_name = display_name.replace("_", " ").title()
103
+
104
+ courses[course_id]["notebooks"].append({
105
+ "id": notebook_id,
106
+ "path": notebook_path,
107
+ "display_name": display_name,
108
+ "order": order
109
+ })
110
+
111
+ # Sort notebooks by order
112
+ for course_id in courses:
113
+ courses[course_id]["notebooks"].sort(key=lambda x: x["order"])
114
+
115
+ return courses
116
+
117
+
118
+ def generate_eva_css() -> str:
119
+ """Generate Neon Genesis Evangelion inspired CSS."""
120
+ return """
121
+ :root {
122
+ --eva-purple: #9a1eb3;
123
+ --eva-green: #00ff00;
124
+ --eva-orange: #ff6600;
125
+ --eva-blue: #0066ff;
126
+ --eva-red: #ff0000;
127
+ --eva-black: #111111;
128
+ --eva-dark: #222222;
129
+ --eva-terminal-bg: rgba(0, 0, 0, 0.85);
130
+ --eva-text: #e0e0e0;
131
+ --eva-border-radius: 4px;
132
+ --eva-transition: all 0.3s ease;
133
+ }
134
+
135
+ body {
136
+ background-color: var(--eva-black);
137
+ color: var(--eva-text);
138
+ font-family: 'Courier New', monospace;
139
+ margin: 0;
140
+ padding: 0;
141
+ line-height: 1.6;
142
+ }
143
+
144
+ .eva-container {
145
+ max-width: 1200px;
146
+ margin: 0 auto;
147
+ padding: 2rem;
148
+ }
149
+
150
+ .eva-header {
151
+ border-bottom: 2px solid var(--eva-green);
152
+ padding-bottom: 1rem;
153
+ margin-bottom: 2rem;
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: center;
157
+ position: sticky;
158
+ top: 0;
159
+ background-color: rgba(17, 17, 17, 0.95);
160
+ z-index: 100;
161
+ backdrop-filter: blur(5px);
162
+ padding-top: 1rem;
163
+ }
164
+
165
+ .eva-logo {
166
+ font-size: 2.5rem;
167
+ font-weight: bold;
168
+ color: var(--eva-green);
169
+ text-transform: uppercase;
170
+ letter-spacing: 2px;
171
+ text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
172
+ }
173
+
174
+ .eva-nav {
175
+ display: flex;
176
+ gap: 1.5rem;
177
+ }
178
+
179
+ .eva-nav a {
180
+ color: white;
181
+ text-decoration: none;
182
+ text-transform: uppercase;
183
+ font-size: 0.9rem;
184
+ letter-spacing: 1px;
185
+ transition: color 0.3s;
186
+ position: relative;
187
+ padding: 0.5rem 0;
188
+ }
189
+
190
+ .eva-nav a:hover {
191
+ color: var(--eva-green);
192
+ }
193
+
194
+ .eva-nav a:hover::after {
195
+ content: '';
196
+ position: absolute;
197
+ bottom: -5px;
198
+ left: 0;
199
+ width: 100%;
200
+ height: 2px;
201
+ background-color: var(--eva-green);
202
+ animation: scanline 1.5s linear infinite;
203
+ }
204
+
205
+ .eva-hero {
206
+ background-color: var(--eva-terminal-bg);
207
+ border: 1px solid var(--eva-green);
208
+ padding: 3rem 2rem;
209
+ margin-bottom: 3rem;
210
+ position: relative;
211
+ overflow: hidden;
212
+ border-radius: var(--eva-border-radius);
213
+ display: flex;
214
+ flex-direction: column;
215
+ align-items: flex-start;
216
+ 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');
217
+ background-size: cover;
218
+ background-position: center;
219
+ background-blend-mode: overlay;
220
+ }
221
+
222
+ .eva-hero::before {
223
+ content: '';
224
+ position: absolute;
225
+ top: 0;
226
+ left: 0;
227
+ width: 100%;
228
+ height: 2px;
229
+ background-color: var(--eva-green);
230
+ animation: scanline 3s linear infinite;
231
+ }
232
+
233
+ .eva-hero h1 {
234
+ font-size: 2.5rem;
235
+ margin-bottom: 1rem;
236
+ color: var(--eva-green);
237
+ text-transform: uppercase;
238
+ letter-spacing: 2px;
239
+ text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
240
+ }
241
+
242
+ .eva-hero p {
243
+ font-size: 1.1rem;
244
+ max-width: 800px;
245
+ margin-bottom: 2rem;
246
+ line-height: 1.8;
247
+ }
248
+
249
+ .eva-features {
250
+ display: grid;
251
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
252
+ gap: 2rem;
253
+ margin-bottom: 3rem;
254
+ }
255
+
256
+ .eva-feature {
257
+ background-color: var(--eva-terminal-bg);
258
+ border: 1px solid var(--eva-blue);
259
+ padding: 1.5rem;
260
+ border-radius: var(--eva-border-radius);
261
+ transition: var(--eva-transition);
262
+ position: relative;
263
+ overflow: hidden;
264
+ }
265
+
266
+ .eva-feature:hover {
267
+ transform: translateY(-5px);
268
+ box-shadow: 0 10px 20px rgba(0, 102, 255, 0.2);
269
+ }
270
+
271
+ .eva-feature-icon {
272
+ font-size: 2rem;
273
+ margin-bottom: 1rem;
274
+ color: var(--eva-blue);
275
+ }
276
+
277
+ .eva-feature h3 {
278
+ font-size: 1.3rem;
279
+ margin-bottom: 1rem;
280
+ color: var(--eva-blue);
281
+ }
282
+
283
+ .eva-section-title {
284
+ font-size: 2rem;
285
+ color: var(--eva-green);
286
+ margin-bottom: 2rem;
287
+ text-transform: uppercase;
288
+ letter-spacing: 2px;
289
+ text-align: center;
290
+ position: relative;
291
+ padding-bottom: 1rem;
292
+ }
293
+
294
+ .eva-section-title::after {
295
+ content: '';
296
+ position: absolute;
297
+ bottom: 0;
298
+ left: 50%;
299
+ transform: translateX(-50%);
300
+ width: 100px;
301
+ height: 2px;
302
+ background-color: var(--eva-green);
303
+ }
304
+
305
+ .eva-courses {
306
+ display: grid;
307
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
308
+ gap: 2rem;
309
+ }
310
+
311
+ .eva-course {
312
+ background-color: var(--eva-terminal-bg);
313
+ border: 1px solid var(--eva-purple);
314
+ border-radius: var(--eva-border-radius);
315
+ transition: var(--eva-transition);
316
+ position: relative;
317
+ overflow: hidden;
318
+ }
319
+
320
+ .eva-course:hover {
321
+ transform: translateY(-5px);
322
+ box-shadow: 0 10px 20px rgba(154, 30, 179, 0.3);
323
+ }
324
+
325
+ .eva-course::after {
326
+ content: '';
327
+ position: absolute;
328
+ bottom: 0;
329
+ left: 0;
330
+ width: 100%;
331
+ height: 2px;
332
+ background-color: var(--eva-purple);
333
+ animation: scanline 2s linear infinite;
334
+ }
335
+
336
+ .eva-course-header {
337
+ padding: 1.5rem;
338
+ cursor: pointer;
339
+ display: flex;
340
+ justify-content: space-between;
341
+ align-items: center;
342
+ border-bottom: 1px solid rgba(154, 30, 179, 0.3);
343
+ }
344
+
345
+ .eva-course-title {
346
+ font-size: 1.5rem;
347
+ color: var(--eva-purple);
348
+ text-transform: uppercase;
349
+ letter-spacing: 1px;
350
+ margin: 0;
351
+ }
352
+
353
+ .eva-course-toggle {
354
+ color: var(--eva-purple);
355
+ font-size: 1.5rem;
356
+ transition: var(--eva-transition);
357
+ }
358
+
359
+ .eva-course-content {
360
+ padding: 0 1.5rem;
361
+ max-height: 0;
362
+ overflow: hidden;
363
+ transition: var(--eva-transition);
364
+ }
365
+
366
+ .eva-course.active .eva-course-content {
367
+ padding: 1.5rem;
368
+ max-height: 1000px;
369
+ }
370
+
371
+ .eva-course.active .eva-course-toggle {
372
+ transform: rotate(180deg);
373
+ }
374
+
375
+ .eva-course-description {
376
+ margin-bottom: 1.5rem;
377
+ font-size: 0.9rem;
378
+ line-height: 1.6;
379
+ }
380
+
381
+ .eva-notebooks {
382
+ margin-top: 1rem;
383
+ }
384
+
385
+ .eva-notebook {
386
+ margin-bottom: 0.75rem;
387
+ padding: 0.5rem;
388
+ border-left: 2px solid var(--eva-blue);
389
+ transition: var(--eva-transition);
390
+ display: flex;
391
+ align-items: center;
392
+ }
393
+
394
+ .eva-notebook:hover {
395
+ background-color: rgba(0, 102, 255, 0.1);
396
+ padding-left: 1rem;
397
+ }
398
+
399
+ .eva-notebook a {
400
+ color: white;
401
+ text-decoration: none;
402
+ display: block;
403
+ font-size: 0.9rem;
404
+ flex-grow: 1;
405
+ }
406
+
407
+ .eva-notebook a:hover {
408
+ color: var(--eva-blue);
409
+ }
410
+
411
+ .eva-notebook-number {
412
+ color: var(--eva-blue);
413
+ font-size: 0.8rem;
414
+ margin-right: 0.5rem;
415
+ opacity: 0.7;
416
+ min-width: 24px;
417
+ }
418
+
419
+ .eva-button {
420
+ display: inline-block;
421
+ background-color: transparent;
422
+ color: var(--eva-green);
423
+ border: 1px solid var(--eva-green);
424
+ padding: 0.7rem 1.5rem;
425
+ text-decoration: none;
426
+ text-transform: uppercase;
427
+ font-size: 0.9rem;
428
+ letter-spacing: 1px;
429
+ transition: var(--eva-transition);
430
+ cursor: pointer;
431
+ border-radius: var(--eva-border-radius);
432
+ position: relative;
433
+ overflow: hidden;
434
+ }
435
+
436
+ .eva-button:hover {
437
+ background-color: var(--eva-green);
438
+ color: var(--eva-black);
439
+ }
440
+
441
+ .eva-button::after {
442
+ content: '';
443
+ position: absolute;
444
+ top: 0;
445
+ left: -100%;
446
+ width: 100%;
447
+ height: 100%;
448
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
449
+ transition: 0.5s;
450
+ }
451
+
452
+ .eva-button:hover::after {
453
+ left: 100%;
454
+ }
455
+
456
+ .eva-cta {
457
+ background-color: var(--eva-terminal-bg);
458
+ border: 1px solid var(--eva-orange);
459
+ padding: 3rem 2rem;
460
+ margin: 4rem 0;
461
+ text-align: center;
462
+ border-radius: var(--eva-border-radius);
463
+ position: relative;
464
+ overflow: hidden;
465
+ }
466
+
467
+ .eva-cta h2 {
468
+ font-size: 2rem;
469
+ color: var(--eva-orange);
470
+ margin-bottom: 1.5rem;
471
+ text-transform: uppercase;
472
+ }
473
+
474
+ .eva-cta p {
475
+ max-width: 600px;
476
+ margin: 0 auto 2rem;
477
+ font-size: 1.1rem;
478
+ }
479
+
480
+ .eva-cta .eva-button {
481
+ color: var(--eva-orange);
482
+ border-color: var(--eva-orange);
483
+ }
484
+
485
+ .eva-cta .eva-button:hover {
486
+ background-color: var(--eva-orange);
487
+ color: var(--eva-black);
488
+ }
489
+
490
+ .eva-footer {
491
+ margin-top: 4rem;
492
+ padding-top: 2rem;
493
+ border-top: 2px solid var(--eva-green);
494
+ display: flex;
495
+ justify-content: space-between;
496
+ align-items: center;
497
+ flex-wrap: wrap;
498
+ gap: 2rem;
499
+ }
500
+
501
+ .eva-footer-links {
502
+ display: flex;
503
+ gap: 1.5rem;
504
+ }
505
+
506
+ .eva-footer-links a {
507
+ color: var(--eva-text);
508
+ text-decoration: none;
509
+ transition: var(--eva-transition);
510
+ }
511
+
512
+ .eva-footer-links a:hover {
513
+ color: var(--eva-green);
514
+ }
515
+
516
+ .eva-footer-copyright {
517
+ font-size: 0.9rem;
518
+ }
519
+
520
+ .eva-search {
521
+ position: relative;
522
+ margin-bottom: 3rem;
523
+ }
524
+
525
+ .eva-search input {
526
+ width: 100%;
527
+ padding: 1rem;
528
+ background-color: var(--eva-terminal-bg);
529
+ border: 1px solid var(--eva-green);
530
+ color: var(--eva-text);
531
+ font-family: 'Courier New', monospace;
532
+ font-size: 1rem;
533
+ border-radius: var(--eva-border-radius);
534
+ outline: none;
535
+ transition: var(--eva-transition);
536
+ }
537
+
538
+ .eva-search input:focus {
539
+ box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
540
+ }
541
+
542
+ .eva-search input::placeholder {
543
+ color: rgba(224, 224, 224, 0.5);
544
+ }
545
+
546
+ .eva-search-icon {
547
+ position: absolute;
548
+ right: 1rem;
549
+ top: 50%;
550
+ transform: translateY(-50%);
551
+ color: var(--eva-green);
552
+ font-size: 1.2rem;
553
+ }
554
+
555
+ @keyframes scanline {
556
+ 0% {
557
+ transform: translateX(-100%);
558
+ }
559
+ 100% {
560
+ transform: translateX(100%);
561
+ }
562
+ }
563
+
564
+ @keyframes blink {
565
+ 0%, 100% {
566
+ opacity: 1;
567
+ }
568
+ 50% {
569
+ opacity: 0;
570
+ }
571
+ }
572
+
573
+ .eva-cursor {
574
+ display: inline-block;
575
+ width: 10px;
576
+ height: 1.2em;
577
+ background-color: var(--eva-green);
578
+ margin-left: 2px;
579
+ animation: blink 1s infinite;
580
+ vertical-align: middle;
581
+ }
582
+
583
+ @media (max-width: 768px) {
584
+ .eva-courses {
585
+ grid-template-columns: 1fr;
586
+ }
587
+
588
+ .eva-header {
589
+ flex-direction: column;
590
+ align-items: flex-start;
591
+ padding: 1rem;
592
+ }
593
+
594
+ .eva-nav {
595
+ margin-top: 1rem;
596
+ flex-wrap: wrap;
597
+ }
598
+
599
+ .eva-hero {
600
+ padding: 2rem 1rem;
601
+ }
602
+
603
+ .eva-hero h1 {
604
+ font-size: 2rem;
605
+ }
606
+
607
+ .eva-features {
608
+ grid-template-columns: 1fr;
609
+ }
610
+
611
+ .eva-footer {
612
+ flex-direction: column;
613
+ align-items: center;
614
+ text-align: center;
615
+ }
616
+ }
617
+ """
618
+
619
+
620
+ def generate_index(courses: Dict[str, Dict[str, Any]], output_dir: str) -> None:
621
+ """Generate the index.html file with Neon Genesis Evangelion aesthetics."""
622
+ print("Generating index.html")
623
+
624
+ index_path = os.path.join(output_dir, "index.html")
625
+ os.makedirs(output_dir, exist_ok=True)
626
+
627
+ try:
628
+ with open(index_path, "w", encoding="utf-8") as f:
629
+ f.write(
630
+ """<!DOCTYPE html>
631
+ <html lang="en">
632
+ <head>
633
+ <meta charset="UTF-8">
634
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
635
+ <title>Marimo Learn - Interactive Educational Notebooks</title>
636
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
637
+ <style>
638
+ """ + generate_eva_css() + """
639
+ </style>
640
+ </head>
641
+ <body>
642
+ <div class="eva-container">
643
+ <header class="eva-header">
644
+ <div class="eva-logo">MARIMO LEARN</div>
645
+ <nav class="eva-nav">
646
+ <a href="#features">Features</a>
647
+ <a href="#courses">Courses</a>
648
+ <a href="#contribute">Contribute</a>
649
+ <a href="https://docs.marimo.io" target="_blank">Documentation</a>
650
+ <a href="https://github.com/marimo-team/learn" target="_blank">GitHub</a>
651
+ </nav>
652
+ </header>
653
+
654
+ <section class="eva-hero">
655
+ <h1>Interactive Learning with Marimo<span class="eva-cursor"></span></h1>
656
+ <p>
657
+ A curated collection of educational notebooks covering computer science,
658
+ mathematics, data science, and more. Built with marimo - the reactive
659
+ Python notebook that makes data exploration delightful.
660
+ </p>
661
+ <a href="#courses" class="eva-button">Explore Courses</a>
662
+ </section>
663
+
664
+ <section id="features">
665
+ <h2 class="eva-section-title">Why Marimo Learn?</h2>
666
+ <div class="eva-features">
667
+ <div class="eva-feature">
668
+ <div class="eva-feature-icon"><i class="fas fa-bolt"></i></div>
669
+ <h3>Reactive Notebooks</h3>
670
+ <p>Experience the power of reactive programming with marimo notebooks that automatically update when dependencies change.</p>
671
+ </div>
672
+ <div class="eva-feature">
673
+ <div class="eva-feature-icon"><i class="fas fa-code"></i></div>
674
+ <h3>Learn by Doing</h3>
675
+ <p>Interactive examples and exercises help you understand concepts through hands-on practice.</p>
676
+ </div>
677
+ <div class="eva-feature">
678
+ <div class="eva-feature-icon"><i class="fas fa-graduation-cap"></i></div>
679
+ <h3>Comprehensive Courses</h3>
680
+ <p>From Python basics to advanced optimization techniques, our courses cover a wide range of topics.</p>
681
+ </div>
682
+ </div>
683
+ </section>
684
+
685
+ <section id="courses">
686
+ <h2 class="eva-section-title">Explore Courses</h2>
687
+ <div class="eva-search">
688
+ <input type="text" id="courseSearch" placeholder="Search courses and notebooks...">
689
+ <span class="eva-search-icon"><i class="fas fa-search"></i></span>
690
+ </div>
691
+ <div class="eva-courses">
692
+ """
693
+ )
694
+
695
+ # Sort courses alphabetically
696
+ sorted_courses = sorted(courses.values(), key=lambda x: x["title"])
697
+
698
+ for course in sorted_courses:
699
+ # Skip if no notebooks
700
+ if not course["notebooks"]:
701
+ continue
702
+
703
+ f.write(
704
+ f'<div class="eva-course" data-course-id="{course["id"]}">\n'
705
+ f' <div class="eva-course-header">\n'
706
+ f' <h2 class="eva-course-title">{course["title"]}</h2>\n'
707
+ f' <span class="eva-course-toggle"><i class="fas fa-chevron-down"></i></span>\n'
708
+ f' </div>\n'
709
+ f' <div class="eva-course-content">\n'
710
+ f' <p class="eva-course-description">{course["description"]}</p>\n'
711
+ f' <div class="eva-notebooks">\n'
712
+ )
713
+
714
+ for i, notebook in enumerate(course["notebooks"]):
715
+ notebook_number = f"{i+1:02d}"
716
+ f.write(
717
+ f' <div class="eva-notebook">\n'
718
+ f' <span class="eva-notebook-number">{notebook_number}</span>\n'
719
+ f' <a href="{notebook["path"].replace(".py", ".html")}" data-notebook-title="{notebook["display_name"]}">{notebook["display_name"]}</a>\n'
720
+ f' </div>\n'
721
+ )
722
+
723
+ f.write(
724
+ f' </div>\n'
725
+ f' </div>\n'
726
+ f'</div>\n'
727
+ )
728
+
729
+ f.write(
730
+ """ </div>
731
+ </section>
732
+
733
+ <section id="contribute" class="eva-cta">
734
+ <h2>Contribute to Marimo Learn</h2>
735
+ <p>
736
+ Help us expand our collection of educational notebooks. Whether you're an expert in machine learning,
737
+ statistics, or any other field, your contributions are welcome!
738
+ </p>
739
+ <a href="https://github.com/marimo-team/learn" target="_blank" class="eva-button">
740
+ <i class="fab fa-github"></i> Contribute on GitHub
741
+ </a>
742
+ </section>
743
+
744
+ <footer class="eva-footer">
745
+ <div class="eva-footer-copyright">
746
+ © 2024 Marimo Learn. Built with <a href="https://marimo.io" target="_blank" style="color: var(--eva-green);">marimo</a>.
747
+ </div>
748
+ <div class="eva-footer-links">
749
+ <a href="https://marimo.io" target="_blank">Marimo Website</a>
750
+ <a href="https://docs.marimo.io" target="_blank">Documentation</a>
751
+ <a href="https://github.com/marimo-team/learn" target="_blank">GitHub</a>
752
+ </div>
753
+ </footer>
754
+ </div>
755
+
756
+ <script>
757
+ document.addEventListener('DOMContentLoaded', function() {
758
+ // Terminal typing effect for hero text
759
+ const heroTitle = document.querySelector('.eva-hero h1');
760
+ const heroText = document.querySelector('.eva-hero p');
761
+ const cursor = document.querySelector('.eva-cursor');
762
+
763
+ const originalTitle = heroTitle.textContent;
764
+ const originalText = heroText.textContent.trim();
765
+
766
+ heroTitle.textContent = '';
767
+ heroText.textContent = '';
768
+
769
+ let titleIndex = 0;
770
+ let textIndex = 0;
771
+
772
+ function typeTitle() {
773
+ if (titleIndex < originalTitle.length) {
774
+ heroTitle.textContent += originalTitle.charAt(titleIndex);
775
+ titleIndex++;
776
+ setTimeout(typeTitle, 50);
777
+ } else {
778
+ cursor.style.display = 'none';
779
+ setTimeout(typeText, 500);
780
+ }
781
+ }
782
+
783
+ function typeText() {
784
+ if (textIndex < originalText.length) {
785
+ heroText.textContent += originalText.charAt(textIndex);
786
+ textIndex++;
787
+ setTimeout(typeText, 20);
788
+ }
789
+ }
790
+
791
+ typeTitle();
792
+
793
+ // Course toggle functionality
794
+ const courseHeaders = document.querySelectorAll('.eva-course-header');
795
+
796
+ courseHeaders.forEach(header => {
797
+ header.addEventListener('click', () => {
798
+ const course = header.parentElement;
799
+ course.classList.toggle('active');
800
+ });
801
+ });
802
+
803
+ // Search functionality
804
+ const searchInput = document.getElementById('courseSearch');
805
+ const courses = document.querySelectorAll('.eva-course');
806
+ const notebooks = document.querySelectorAll('.eva-notebook');
807
+
808
+ searchInput.addEventListener('input', function() {
809
+ const searchTerm = this.value.toLowerCase();
810
+
811
+ if (searchTerm === '') {
812
+ // Reset all visibility
813
+ courses.forEach(course => {
814
+ course.style.display = 'block';
815
+ course.classList.remove('active');
816
+ });
817
+
818
+ notebooks.forEach(notebook => {
819
+ notebook.style.display = 'flex';
820
+ });
821
+
822
+ return;
823
+ }
824
+
825
+ // First hide all courses
826
+ courses.forEach(course => {
827
+ course.style.display = 'none';
828
+ course.classList.remove('active');
829
+ });
830
+
831
+ // Then show courses and notebooks that match the search
832
+ let hasResults = false;
833
+
834
+ notebooks.forEach(notebook => {
835
+ const notebookTitle = notebook.querySelector('a').getAttribute('data-notebook-title').toLowerCase();
836
+ const matchesSearch = notebookTitle.includes(searchTerm);
837
+
838
+ notebook.style.display = matchesSearch ? 'flex' : 'none';
839
+
840
+ if (matchesSearch) {
841
+ const course = notebook.closest('.eva-course');
842
+ course.style.display = 'block';
843
+ course.classList.add('active');
844
+ hasResults = true;
845
+ }
846
+ });
847
+
848
+ // Also search course titles
849
+ courses.forEach(course => {
850
+ const courseTitle = course.querySelector('.eva-course-title').textContent.toLowerCase();
851
+ const courseDescription = course.querySelector('.eva-course-description').textContent.toLowerCase();
852
+
853
+ if (courseTitle.includes(searchTerm) || courseDescription.includes(searchTerm)) {
854
+ course.style.display = 'block';
855
+ course.classList.add('active');
856
+ hasResults = true;
857
+ }
858
+ });
859
+ });
860
+
861
+ // Open the first course by default
862
+ if (courses.length > 0) {
863
+ courses[0].classList.add('active');
864
+ }
865
+
866
+ // Smooth scrolling for anchor links
867
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
868
+ anchor.addEventListener('click', function(e) {
869
+ e.preventDefault();
870
+
871
+ const targetId = this.getAttribute('href');
872
+ const targetElement = document.querySelector(targetId);
873
+
874
+ if (targetElement) {
875
+ window.scrollTo({
876
+ top: targetElement.offsetTop - 100,
877
+ behavior: 'smooth'
878
+ });
879
+ }
880
+ });
881
+ });
882
+ });
883
+ </script>
884
+ </body>
885
+ </html>"""
886
+ )
887
+ except IOError as e:
888
+ print(f"Error generating index.html: {e}")
889
+
890
+
891
+ def main() -> None:
892
+ parser = argparse.ArgumentParser(description="Build marimo notebooks")
893
+ parser.add_argument(
894
+ "--output-dir", default="_site", help="Output directory for built files"
895
+ )
896
+ parser.add_argument(
897
+ "--course-dirs", nargs="+", default=None,
898
+ help="Specific course directories to build (default: all directories with .py files)"
899
+ )
900
+ args = parser.parse_args()
901
+
902
+ # Find all course directories (directories containing .py files)
903
+ all_notebooks: List[str] = []
904
+
905
+ # Directories to exclude from course detection
906
+ excluded_dirs = ["scripts", "env", "__pycache__", ".git", ".github", "assets"]
907
+
908
+ if args.course_dirs:
909
+ course_dirs = args.course_dirs
910
+ else:
911
+ # Automatically detect course directories (any directory with .py files)
912
+ course_dirs = []
913
+ for item in os.listdir("."):
914
+ if (os.path.isdir(item) and
915
+ not item.startswith(".") and
916
+ not item.startswith("_") and
917
+ item not in excluded_dirs):
918
+ # Check if directory contains .py files
919
+ if list(Path(item).glob("*.py")):
920
+ course_dirs.append(item)
921
+
922
+ print(f"Found course directories: {', '.join(course_dirs)}")
923
+
924
+ for directory in course_dirs:
925
+ dir_path = Path(directory)
926
+ if not dir_path.exists():
927
+ print(f"Warning: Directory not found: {dir_path}")
928
+ continue
929
+
930
+ notebooks = [str(path) for path in dir_path.rglob("*.py")
931
+ if not path.name.startswith("_") and "/__pycache__/" not in str(path)]
932
+ all_notebooks.extend(notebooks)
933
+
934
+ if not all_notebooks:
935
+ print("No notebooks found!")
936
+ return
937
+
938
+ # Export notebooks sequentially
939
+ successful_notebooks = []
940
+ for nb in all_notebooks:
941
+ # Determine if notebook should be exported as app or notebook
942
+ # For now, export all as notebooks
943
+ if export_html_wasm(nb, args.output_dir, as_app=False):
944
+ successful_notebooks.append(nb)
945
+
946
+ # Organize notebooks by course (only include successfully exported notebooks)
947
+ courses = organize_notebooks_by_course(successful_notebooks)
948
+
949
+ # Generate index with organized courses
950
+ generate_index(courses, args.output_dir)
951
+
952
+ # Save course data as JSON for potential use by other tools
953
+ courses_json_path = os.path.join(args.output_dir, "courses.json")
954
+ with open(courses_json_path, "w", encoding="utf-8") as f:
955
+ json.dump(courses, f, indent=2)
956
+
957
+ print(f"Build complete! Site generated in {args.output_dir}")
958
+ print(f"Successfully exported {len(successful_notebooks)} out of {len(all_notebooks)} notebooks")
959
+
960
+
961
+ if __name__ == "__main__":
962
+ main()
scripts/preview.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import subprocess
5
+ import argparse
6
+ import webbrowser
7
+ import time
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(description="Build and preview marimo notebooks site")
13
+ parser.add_argument(
14
+ "--port", default=8000, type=int, help="Port to run the server on"
15
+ )
16
+ parser.add_argument(
17
+ "--no-build", action="store_true", help="Skip building the site (just serve existing files)"
18
+ )
19
+ parser.add_argument(
20
+ "--output-dir", default="_site", help="Output directory for built files"
21
+ )
22
+ args = parser.parse_args()
23
+
24
+ # Store the current directory
25
+ original_dir = os.getcwd()
26
+
27
+ try:
28
+ # Build the site if not skipped
29
+ if not args.no_build:
30
+ print("Building site...")
31
+ build_script = Path("scripts/build.py")
32
+ if not build_script.exists():
33
+ print(f"Error: Build script not found at {build_script}")
34
+ return 1
35
+
36
+ result = subprocess.run(
37
+ [sys.executable, str(build_script), "--output-dir", args.output_dir],
38
+ check=False
39
+ )
40
+ if result.returncode != 0:
41
+ print("Warning: Build process completed with errors.")
42
+
43
+ # Check if the output directory exists
44
+ output_dir = Path(args.output_dir)
45
+ if not output_dir.exists():
46
+ print(f"Error: Output directory '{args.output_dir}' does not exist.")
47
+ return 1
48
+
49
+ # Change to the output directory
50
+ os.chdir(args.output_dir)
51
+
52
+ # Open the browser
53
+ url = f"http://localhost:{args.port}"
54
+ print(f"Opening {url} in your browser...")
55
+ webbrowser.open(url)
56
+
57
+ # Start the server
58
+ print(f"Starting server on port {args.port}...")
59
+ print("Press Ctrl+C to stop the server")
60
+
61
+ # Use the appropriate Python executable
62
+ subprocess.run([sys.executable, "-m", "http.server", str(args.port)])
63
+
64
+ return 0
65
+ except KeyboardInterrupt:
66
+ print("\nServer stopped.")
67
+ return 0
68
+ except Exception as e:
69
+ print(f"Error: {e}")
70
+ return 1
71
+ finally:
72
+ # Always return to the original directory
73
+ os.chdir(original_dir)
74
+
75
+ if __name__ == "__main__":
76
+ sys.exit(main())