Created
May 12, 2026 18:58
-
-
Save panchicore/205fbb2cee2aa3e2d4eb08419227154d to your computer and use it in GitHub Desktop.
Starlark Migration — Spencer/Infra Meeting Slides
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Starlark Migration</title> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: #0a0a0a; | |
| color: #33ff33; | |
| font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace; | |
| font-size: 15px; | |
| line-height: 1.5; | |
| overflow: hidden; | |
| height: 100vh; | |
| } | |
| .slide { | |
| display: none; | |
| width: 100vw; | |
| height: 100vh; | |
| padding: 40px 60px; | |
| position: relative; | |
| } | |
| .slide.active { display: flex; flex-direction: column; justify-content: center; } | |
| .box { | |
| border: 2px solid #33ff33; | |
| padding: 30px 36px; | |
| max-width: 860px; | |
| box-shadow: 0 0 20px rgba(51, 255, 51, 0.08); | |
| } | |
| h1 { font-size: 22px; font-weight: bold; letter-spacing: 2px; margin-bottom: 4px; } | |
| h2 { font-size: 18px; font-weight: bold; margin-bottom: 4px; } | |
| .rule { color: #33ff33; margin-bottom: 16px; } | |
| .subtitle { font-size: 15px; margin-bottom: 20px; } | |
| .label { font-size: 13px; color: #88cc88; margin-bottom: 4px; letter-spacing: 1px; } | |
| ul { list-style: none; padding: 0; } | |
| ul li::before { content: "\25CF "; } | |
| ul li { margin-bottom: 3px; } | |
| .narrative { margin-top: 16px; line-height: 1.6; } | |
| .impact { margin-top: 16px; color: #88cc88; } | |
| .status { margin-top: 8px; } | |
| .status .merged { color: #33ff33; } | |
| .status .ready { color: #ffcc00; } | |
| .highlight { color: #ffffff; } | |
| .dim { color: #88cc88; } | |
| .warn { color: #ffcc00; } | |
| .code { background: #1a1a1a; padding: 2px 6px; border: 1px solid #225522; } | |
| table { border-collapse: collapse; margin: 12px 0; } | |
| td, th { padding: 3px 16px 3px 0; text-align: left; } | |
| th { color: #88cc88; font-weight: normal; } | |
| .nav { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 30px; | |
| font-size: 13px; | |
| color: #556655; | |
| } | |
| @keyframes blink { 50% { opacity: 0; } } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; text-shadow: 0 0 8px rgba(255, 204, 0, 0.6); } | |
| 50% { opacity: 0.5; text-shadow: none; } | |
| } | |
| .pulse { animation: pulse 1.5s ease-in-out infinite; } | |
| @keyframes gpulse { | |
| 0%, 100% { text-shadow: 0 0 10px rgba(51, 255, 51, 0.5); } | |
| 50% { text-shadow: none; } | |
| } | |
| h1, h2 { animation: gpulse 2s ease-in-out infinite; } | |
| @keyframes boxglow { | |
| 0%, 100% { box-shadow: 0 0 15px rgba(51, 255, 51, 0.15), inset 0 0 15px rgba(51, 255, 51, 0.03); border-color: #33ff33; } | |
| 50% { box-shadow: 0 0 30px rgba(51, 255, 51, 0.3), inset 0 0 25px rgba(51, 255, 51, 0.06); border-color: #55ff55; } | |
| } | |
| .box { animation: boxglow 3s ease-in-out infinite; } | |
| @keyframes codeglow { | |
| 0%, 100% { background: #1a1a1a; } | |
| 50% { background: #1a2a1a; } | |
| } | |
| .code { animation: codeglow 2s ease-in-out infinite; } | |
| .merged { text-shadow: 0 0 8px rgba(51, 255, 51, 0.6); } | |
| .label { text-shadow: 0 0 6px rgba(136, 204, 136, 0.4); } | |
| .scanline { | |
| pointer-events: none; | |
| position: fixed; | |
| top: 0; left: 0; right: 0; bottom: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0,0,0,0.08) 2px, | |
| rgba(0,0,0,0.08) 4px | |
| ); | |
| z-index: 999; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="scanline"></div> | |
| <!-- Slide 1: Title --> | |
| <div class="slide active" id="s1"> | |
| <div class="box"> | |
| <h1>STARLARK FORMULA ENGINE MIGRATION</h1> | |
| <div class="rule">________________________________________</div> | |
| <div class="subtitle"> | |
| Replacing RestrictedPython<br> | |
| Calculated Fields Security (SC-15742) | |
| </div> | |
| <div class="label">Achievement:</div> | |
| <ul> | |
| <li>~600K eval pairs <span class="dim">(org × record × formula)</span> across 3 DBs, 0 unexplained mismatches</li> | |
| <li>248 formulas transpiled, 0 failures</li> | |
| <li>Full rollback path at every step</li> | |
| </ul> | |
| <div class="narrative"> | |
| <div class="label">Narrative:</div> | |
| RCE exploit leaked all production secrets on staging.<br> | |
| RestrictedPython's blacklist approach can't be fixed.<br> | |
| <br> | |
| <span class="dim">11 alternatives evaluated:</span><br> | |
| <br> | |
| <span style="font-size: 13px;"> | |
| <span class="dim">bwrap sandbox</span> <span style="color:#ff4444;">✗</span> thicker walls, same leaky bucket<br> | |
| <span class="dim">Formula Builder</span> <span style="color:#ff4444;">✗</span> kills code-authoring persona<br> | |
| <span class="dim">E2B microVMs</span> <span style="color:#ff4444;">✗</span> vendor dependency, infra overhead<br> | |
| <span class="dim">V8/JavaScript</span> <span style="color:#ff4444;">✗</span> full rewrite, different language<br> | |
| <span class="dim">WASM</span> <span style="color:#ff4444;">✗</span> still runs Python inside<br> | |
| <span class="dim">SQL compile</span> <span style="color:#ff4444;">✗</span> very fragile<br> | |
| <span class="dim">Smart Fields</span> <span style="color:#ff4444;">✗</span> removes the moat for power users<br> | |
| <span class="highlight">Starlark</span> <span class="merged">✓</span> sandboxed by construction, no escape<br> | |
| </span> | |
| <br> | |
| 248 formulas, 44 orgs, 0 blockers, 100% auto-transpiled. | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Slide 2: PR #3058 --> | |
| <div class="slide" id="s2"> | |
| <div class="box"> | |
| <h2>PR #3058 — Starlark Engine + LD Flag</h2> | |
| <div class="rule">________________________________________</div> | |
| <ul> | |
| <li>StarlarkExecutor with 9 helper functions behind LD flag<br><span class="dim"> <span class="code">launchdarkly.rollout.formula.starlark</span></span></li> | |
| <li>Flag defaults to <span class="code">"off"</span> — zero behavior change on deploy</li> | |
| <li>StarlarkExecutor never imported unless flag is <span class="code">"starlark"</span></li> | |
| <li>93 new tests, 0 regressions on existing 29 CodeExecutor tests</li> | |
| <li>No DB migrations, no schema changes, no new API contracts</li> | |
| </ul> | |
| <div class="impact">Infra impact: Deploy normally. Nothing changes until flag flip.</div> | |
| <div class="status">Status: <span class="merged">✔ Merged</span></div> | |
| </div> | |
| </div> | |
| <!-- Slide 3: PR #3081 --> | |
| <div class="slide" id="s3"> | |
| <div class="box"> | |
| <h2>PR #3081 — Offline Validation (596K replays)</h2> | |
| <div class="rule">________________________________________</div> | |
| <ul> | |
| <li>Extracted real prod formulas from 3 DBs (dev, prod, vpv)</li> | |
| <li>Replayed both engines side-by-side on historical inputs</li> | |
| <li>596K eval pairs — 0 unexplained mismatches</li> | |
| <li>All 628 "mismatches" were pre-existing formula bugs</li> | |
| <li>Produced the 25+ regex transpiler rules</li> | |
| </ul> | |
| <div class="impact"> | |
| Infra impact: Scripts only. No production code beyond 6 new<br> | |
| helper functions added to StarlarkExecutor. | |
| </div> | |
| <div class="status">Status: <span class="merged">✔ Merged</span></div> | |
| </div> | |
| </div> | |
| <!-- Slide 4: Python vs Starlark (typewriter) --> | |
| <div class="slide" id="s4"> | |
| <div class="box" style="padding: 24px 30px;"> | |
| <pre id="diff-terminal" style="margin:0; font: inherit; white-space: pre; font-size: 13px; line-height: 1.45;"></pre> | |
| <span id="diff-cursor" style="animation: blink 0.6s step-end infinite; color: #33ff33;">█</span> | |
| </div> | |
| </div> | |
| <!-- Slide 5: PR #3090 --> | |
| <div class="slide" id="s5"> | |
| <div class="box"> | |
| <h2>PR #3090 — Bulk Transpilation Tool</h2> | |
| <div class="rule">________________________________________</div> | |
| <ul> | |
| <li>CLI: <span class="code">--org <cuid|all></span> <span class="code">--dry-run</span> <span class="code">--formula <key></span> <span class="code">--rollback</span></li> | |
| <li>Backs up Python source in <span class="code">code.source_python</span> before rewrite</li> | |
| <li>Every formula validated via <span class="code">code_check()</span> before persisting</li> | |
| <li>428 formulas transpiled across 3 DBs, 0 failures</li> | |
| <li>Auto-runs as scrubber on deploy (logs to container stdout)</li> | |
| <li>Python syntax detector returns warnings in <span class="code">/code/test</span> API</li> | |
| </ul> | |
| <div class="impact"> | |
| Infra impact: Scrubber runs post-deploy. Monitor logs for:<br> | |
| grep <span class="code">"Failed:"</span> — should be 0<br> | |
| grep <span class="code">"STARLARK TRANSPILATION SUMMARY"</span> — confirms completion | |
| </div> | |
| <div class="status">Status: <span class="ready pulse">◆ Ready to merge</span></div> | |
| <div class="dim" style="margin-top: 8px;">Frontend PR #2510: Starlark editor UI — deploy together</div> | |
| <pre id="cmd-terminal" style="margin-top: 14px; font: inherit; white-space: pre; font-size: 13px; line-height: 1.45;"></pre> | |
| <span id="cmd-cursor" style="animation: blink 0.6s step-end infinite; color: #33ff33; display: none;">█</span> | |
| </div> | |
| </div> | |
| <!-- Slide 6: Question for Infra --> | |
| <div class="slide" id="s6"> | |
| <div class="box"> | |
| <h2>Question for Infra: How do you want to run this?</h2> | |
| <div class="rule">________________________________________</div> | |
| <div class="dim" style="margin-bottom: 14px;">We have 2 scripts that do the same thing:</div> | |
| <div style="margin-bottom: 14px;"> | |
| <span class="highlight">A) CLI tool</span><br> | |
| <span class="code" style="margin-left: 16px;">$ python scripts/starlark_bulk_transpile.py --org all</span><br> | |
| <span style="margin-left: 16px; color: #33ff33;">+ --dry-run, --rollback, --formula, --verbose, diffs</span><br> | |
| <span style="margin-left: 16px; color: #33ff33;">+ vmconfig DB connection (no --db needed)</span><br> | |
| <span style="margin-left: 16px; color: #ff4444;">- Must be run manually (SSH / exec into pod)</span> | |
| </div> | |
| <div style="margin-bottom: 14px;"> | |
| <span class="highlight">B) Scrubber</span><br> | |
| <span class="code" style="margin-left: 16px;">reusable.starlark_transpile_formulas</span><br> | |
| <span style="margin-left: 16px; color: #33ff33;">+ Fits existing scrubber runner pipeline</span><br> | |
| <span style="margin-left: 16px; color: #33ff33;">+ Logs to container stdout</span><br> | |
| <span style="margin-left: 16px; color: #ff4444;">- No --rollback, no diffs, no --formula filter</span><br> | |
| <span style="margin-left: 16px; color: #ff4444;">- Admin route currently disabled</span> | |
| </div> | |
| <div class="dim">Both are idempotent. Both back up Python source.</div> | |
| <div class="warn" style="margin-top: 8px;">We want to keep one. Which fits your deploy workflow?</div> | |
| </div> | |
| </div> | |
| <!-- Slide 7: Feature Flag --> | |
| <div class="slide" id="s7"> | |
| <div class="box"> | |
| <h2>Feature Flag</h2> | |
| <div class="rule">________________________________________</div> | |
| <table> | |
| <tr><th>Key</th><td><span class="code">launchdarkly.rollout.formula.starlark</span></td></tr> | |
| <tr><th>Values</th><td><span class="code">"off"</span> (default) | <span class="code">"starlark"</span></td></tr> | |
| <tr><th>Scope</th><td><span class="highlight">All orgs at once</span> (not per-org)</td></tr> | |
| </table> | |
| <div class="label" style="margin-top: 16px;">Rollout:</div> | |
| <ul> | |
| <li>Deploy code — scrubber transpiles all formulas automatically</li> | |
| <li>Flip flag to <span class="code">"starlark"</span> globally</li> | |
| <li>Monitor <span class="code">StarlarkExecutor</span> errors in app logs</li> | |
| </ul> | |
| <div class="label" style="margin-top: 16px;">Rollback (if needed):</div> | |
| <ul> | |
| <li>Flip flag back to <span class="code">"off"</span></li> | |
| <li>Run <span class="code">--rollback</span> to restore Python source</li> | |
| </ul> | |
| <div class="label" style="margin-top: 16px;">Schedule (SC-16119):</div> | |
| <ul> | |
| <li>Tuesday: Merge → Deploy US1 → Flip flag globally</li> | |
| <li>Tuesday evening: Promote VPV → Flip flag</li> | |
| <li>CMVM: Goes out in 26.06 release</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Slide 8: Remove RestrictedPython --> | |
| <div class="slide" id="s8"> | |
| <div class="box"> | |
| <h2>Next: Remove RestrictedPython</h2> | |
| <div class="rule">________________________________________</div> | |
| <div class="narrative" style="margin-top: 0; margin-bottom: 16px;"> | |
| Once Starlark is stable in all envs: | |
| </div> | |
| <ul> | |
| <li>Delete <span class="code">CodeExecutor</span> (RestrictedPython)</li> | |
| <li>Delete LD flag — StarlarkExecutor becomes the only path</li> | |
| <li>Delete <span class="code">code.source_python</span> backups</li> | |
| <li>Remove <span class="code">RestrictedPython</span> from dependencies</li> | |
| </ul> | |
| <div class="narrative" style="margin-top: 20px;"> | |
| <span class="highlight">This eliminates the RCE attack surface permanently.</span> | |
| </div> | |
| <div class="dim" style="margin-top: 16px;"> | |
| Bead: backend-83y (PR2: remove RestrictedPython + LD flag)<br> | |
| Blocked by: successful production rollout of PR #3090 | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Slide 9: The Command (typewriter) --> | |
| <div class="slide" id="s9"> | |
| <div class="box"> | |
| <pre id="terminal" style="margin:0; font: inherit; white-space: pre-wrap;"></pre> | |
| <span id="cursor" style="animation: blink 0.6s step-end infinite; color: #33ff33;">█</span> | |
| </div> | |
| </div> | |
| <!-- Slide 10: Black --> | |
| <div class="slide" id="s10"> | |
| </div> | |
| <div class="nav"><span id="counter">1/10</span> ← → to navigate</div> | |
| <script> | |
| const slides = document.querySelectorAll('.slide'); | |
| const counter = document.getElementById('counter'); | |
| let current = 0; | |
| function show(i) { | |
| slides[current].classList.remove('active'); | |
| current = (i + slides.length) % slides.length; | |
| slides[current].classList.add('active'); | |
| counter.textContent = (current + 1) + '/10'; | |
| } | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'ArrowRight' || e.key === ' ') show(current + 1); | |
| if (e.key === 'ArrowLeft') show(current - 1); | |
| }); | |
| document.addEventListener('click', (e) => { | |
| if (e.clientX > window.innerWidth / 2) show(current + 1); | |
| else show(current - 1); | |
| }); | |
| const diffLines = [ | |
| { text: 'Python vs Starlark — Spot the Difference', color: '#33ff33', delay: 6 }, | |
| { text: '\n────────────────────────────────────────', color: '#33ff33', delay: 1 }, | |
| { text: '\n', delay: 60 }, | |
| { text: '\n PYTHON (before) │ STARLARK (after)', color: '#88cc88', delay: 4 }, | |
| { text: '\n ───────────────── │ ──────────────────', color: '#556655', delay: 1 }, | |
| { text: '\n params.riskScore │ params["riskScore"]', color: '#33ff33', delay: 4 }, | |
| { text: '\n vm_today │ today()', color: '#33ff33', delay: 4 }, | |
| { text: '\n date.fromisoformat("2026-01-15") │ safe_parse_date("2026-01-15")', color: '#33ff33', delay: 4 }, | |
| { text: '\n', delay: 40 }, | |
| { text: '\n (due_date - start_date).days │ days_between(start_date,', color: '#33ff33', delay: 4 }, | |
| { text: '\n │ due_date)', color: '#33ff33', delay: 4 }, | |
| { text: '\n', delay: 40 }, | |
| { text: '\n review_date + timedelta(days=90) │ add_days(review_date, 90)', color: '#33ff33', delay: 4 }, | |
| { text: '\n start + relativedelta(months=6) │ add_months(start, 6)', color: '#33ff33', delay: 4 }, | |
| { text: '\n created_at.year │ get_year(created_at)', color: '#33ff33', delay: 4 }, | |
| { text: '\n deadline.isoformat() │ to_iso(deadline)', color: '#33ff33', delay: 4 }, | |
| { text: '\n isinstance(score, str) │ safe_int(score)', color: '#33ff33', delay: 4 }, | |
| { text: '\n findings.count("High") │ list_count(findings, "High")', color: '#33ff33', delay: 4 }, | |
| { text: '\n', delay: 40 }, | |
| { text: '\n ref_id.zfill(4) │ zfill(ref_id, 4)', color: '#33ff33', delay: 4 }, | |
| { text: '\n # "42" → "0042" │ # "42" → "0042"', color: '#556655', delay: 4 }, | |
| { text: '\n', delay: 40 }, | |
| { text: '\n try: │ params.get("riskScore", 0)', color: '#33ff33', delay: 4 }, | |
| { text: '\n x = params.riskScore │', color: '#ff4444', delay: 4 }, | |
| { text: '\n except: │', color: '#ff4444', delay: 4 }, | |
| { text: '\n x = 0 │', color: '#ff4444', delay: 4 }, | |
| { text: '\n', delay: 100 }, | |
| { text: '\n Same logic, same structure, same return values.', color: '#88cc88', delay: 5 }, | |
| { text: '\n Transpiler rewrites automatically — users barely notice.', color: '#88cc88', delay: 5 }, | |
| { text: '\n', delay: 60 }, | |
| { text: '\n Monaco editor instrumented with autocomplete for all', color: '#556655', delay: 4 }, | |
| { text: '\n Starlark helpers, bracket-access fields, and syntax warnings.', color: '#556655', delay: 4 }, | |
| { text: '\n', delay: 60 }, | |
| { text: '\n TODO: Starlark docs for newbies introducing the 15 helpers.', color: '#ffcc00', delay: 4 }, | |
| { text: '\n Helpers are extensible — more can be added as we learn from', color: '#ffcc00', delay: 4 }, | |
| { text: '\n real customer use cases.', color: '#ffcc00', delay: 4 }, | |
| ]; | |
| let diffStarted = false; | |
| async function typeDiff() { | |
| if (diffStarted) return; | |
| diffStarted = true; | |
| const el = document.getElementById('diff-terminal'); | |
| const cur = document.getElementById('diff-cursor'); | |
| el.innerHTML = ''; | |
| for (const line of diffLines) { | |
| if (line.delay === 0) { | |
| el.innerHTML += `<span style="color:${line.color}">${line.text}</span>`; | |
| continue; | |
| } | |
| for (const ch of line.text) { | |
| const safe = ch === ' ' ? ' ' : ch === '<' ? '<' : ch === '>' ? '>' : ch; | |
| el.innerHTML += `<span style="color:${line.color}">${safe}</span>`; | |
| await new Promise(r => setTimeout(r, line.delay)); | |
| } | |
| } | |
| cur.style.display = 'none'; | |
| } | |
| const cmdLines = [ | |
| { text: '$ ', color: '#556655', delay: 0 }, | |
| { text: 'python scripts/starlark_bulk_transpile.py --org all --dry-run', color: '#33ff33', delay: 25 }, | |
| ]; | |
| let cmdStarted = false; | |
| async function typeCmd() { | |
| if (cmdStarted) return; | |
| cmdStarted = true; | |
| const el = document.getElementById('cmd-terminal'); | |
| const cur = document.getElementById('cmd-cursor'); | |
| cur.style.display = 'inline'; | |
| el.innerHTML = ''; | |
| for (const line of cmdLines) { | |
| if (line.delay === 0) { | |
| el.innerHTML += `<span style="color:${line.color}">${line.text}</span>`; | |
| continue; | |
| } | |
| for (const ch of line.text) { | |
| el.innerHTML += `<span style="color:${line.color}">${ch === ' ' ? ' ' : ch}</span>`; | |
| await new Promise(r => setTimeout(r, line.delay)); | |
| } | |
| } | |
| } | |
| const termLines = [ | |
| { text: 'validmind@prod:~$ ', color: '#556655', delay: 0 }, | |
| { text: 'pip uninstall RestrictedPython', color: '#33ff33', delay: 40 }, | |
| { text: '\n', delay: 300 }, | |
| { text: '\nFound existing installation: RestrictedPython', color: '#88cc88', delay: 10 }, | |
| { text: '\nUninstalling RestrictedPython:', color: '#88cc88', delay: 10 }, | |
| { text: '\n Would remove:', color: '#88cc88', delay: 10 }, | |
| { text: '\n RestrictedPython/', color: '#88cc88', delay: 8 }, | |
| { text: '\n compile_restricted()', color: '#88cc88', delay: 8 }, | |
| { text: '\n relativedelta.__init__.__globals__', color: '#88cc88', delay: 8 }, | |
| { text: '\n your_production_secrets', color: '#ff4444', delay: 8 }, | |
| { text: '\n sleepless_nights', color: '#88cc88', delay: 8 }, | |
| { text: '\n', delay: 400 }, | |
| { text: '\nProceed (y/n)? ', color: '#33ff33', delay: 10 }, | |
| { text: '', delay: 600 }, | |
| { text: 'y', color: '#ffffff', delay: 0 }, | |
| { text: '\n', delay: 500 }, | |
| { text: '\nSuccessfully uninstalled RestrictedPython', color: '#33ff33', delay: 10 }, | |
| { text: '\nSuccessfully installed good-nights-sleep-1.0', color: '#33ff33', delay: 10 }, | |
| { text: '\n', delay: 300 }, | |
| { text: '\nCVE count: 248 → 0', color: '#556655', delay: 10 }, | |
| { text: '\nRCE surface: gone', color: '#556655', delay: 10 }, | |
| { text: '\nSecrets leaked: never again', color: '#556655', delay: 10 }, | |
| { text: '\n', delay: 400 }, | |
| { text: '\nQuestions (y/n)? ', color: '#33ff33', delay: 10 }, | |
| { text: '', delay: 600 }, | |
| ]; | |
| let termStarted = false; | |
| async function typeTerminal() { | |
| if (termStarted) return; | |
| termStarted = true; | |
| const el = document.getElementById('terminal'); | |
| const cur = document.getElementById('cursor'); | |
| el.innerHTML = ''; | |
| for (const line of termLines) { | |
| if (line.delay === 0) { | |
| el.innerHTML += `<span style="color:${line.color}">${line.text}</span>`; | |
| continue; | |
| } | |
| for (const ch of line.text) { | |
| el.innerHTML += `<span style="color:${line.color}">${ch === ' ' ? ' ' : ch}</span>`; | |
| el.parentElement.scrollTop = el.parentElement.scrollHeight; | |
| await new Promise(r => setTimeout(r, line.delay)); | |
| } | |
| } | |
| } | |
| const origShow = show; | |
| show = function(i) { | |
| slides[current].classList.remove('active'); | |
| current = (i + slides.length) % slides.length; | |
| slides[current].classList.add('active'); | |
| counter.textContent = (current + 1) + '/10'; | |
| if (current === 3) typeDiff(); | |
| if (current === 4) typeCmd(); | |
| if (current === 8) typeTerminal(); | |
| }; | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment