11/**
22 * Pyodide-powered "▶ Run" button for code cells.
33 *
4- * Adds a Run button next to the Copy button on every code-cell block .
5- * Clicking Run loads Pyodide (WebAssembly CPython) on first use, then
6- * executes the cell and shows output inline .
4+ * Replaces static <pre> blocks with editable <textarea> elements .
5+ * Users can modify code, click Run to execute via Pyodide (in-browser
6+ * WebAssembly Python), and click Reset to restore the original code .
77 */
88( function ( ) {
99 "use strict" ;
1010
1111 const PYODIDE_CDN =
1212 "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js" ;
1313
14+ // Hint the browser to start fetching the Pyodide script immediately.
15+ var prelink = document . createElement ( "link" ) ;
16+ prelink . rel = "preload" ;
17+ prelink . href = PYODIDE_CDN ;
18+ prelink . as = "script" ;
19+ document . head . appendChild ( prelink ) ;
20+
1421 let pyodidePromise = null ;
1522
1623 /** Lazy-load Pyodide the first time someone clicks Run. */
2229 script . src = PYODIDE_CDN ;
2330 script . onload = async ( ) => {
2431 try {
25- /* global loadPyodide – provided by the CDN script */
2632 const py = await globalThis . loadPyodide ( ) ;
2733 resolve ( py ) ;
2834 } catch ( err ) {
3541 return pyodidePromise ;
3642 }
3743
38- /** Run a code string and return { stdout, stderr }. */
44+ /** Run a code string and return { output, error }. */
3945 async function runCode ( pyodide , code ) {
40- // Redirect stdout / stderr so we can capture them.
41- pyodide . runPython ( `
42- import sys, io
43- sys.stdout = io.StringIO()
44- sys.stderr = io.StringIO()
45- ` ) ;
46+ pyodide . runPython (
47+ "import sys, io\nsys.stdout = io.StringIO()\nsys.stderr = io.StringIO()"
48+ ) ;
4649 try {
4750 const result = pyodide . runPython ( code ) ;
4851 const stdout = pyodide . runPython ( "sys.stdout.getvalue()" ) ;
@@ -57,80 +60,156 @@ sys.stderr = io.StringIO()
5760 const stderr = pyodide . runPython ( "sys.stderr.getvalue()" ) ;
5861 return { output : stderr + "\n" + err . message , error : true } ;
5962 } finally {
60- pyodide . runPython ( `
61- sys.stdout = sys.__stdout__
62- sys.stderr = sys.__stderr__
63- ` ) ;
63+ pyodide . runPython ( "sys.stdout = sys.__stdout__\nsys.stderr = sys.__stderr__" ) ;
6464 }
6565 }
6666
67- /** Create or update the output area below a code block . */
68- function showOutput ( container , text , isError ) {
69- let outputEl = container . querySelector ( ".pyodide-output" ) ;
70- if ( ! outputEl ) {
71- outputEl = document . createElement ( "pre" ) ;
72- outputEl . className = "pyodide-output" ;
73- container . appendChild ( outputEl ) ;
67+ /** Show or update the output area below the editor . */
68+ function showOutput ( wrapper , text , isError ) {
69+ var el = wrapper . querySelector ( ".pyodide-output" ) ;
70+ if ( ! el ) {
71+ el = document . createElement ( "pre" ) ;
72+ el . className = "pyodide-output" ;
73+ wrapper . appendChild ( el ) ;
7474 }
75- outputEl . textContent = text || "(no output)" ;
76- outputEl . classList . toggle ( "pyodide-error" , isError ) ;
77- outputEl . style . display = "block" ;
75+ el . textContent = text || "(no output)" ;
76+ el . classList . toggle ( "pyodide-error" , isError ) ;
77+ el . style . display = "block" ;
78+ }
79+
80+ /** Auto-resize textarea to fit content. */
81+ function autoResize ( textarea ) {
82+ textarea . style . height = "auto" ;
83+ textarea . style . height = textarea . scrollHeight + "px" ;
7884 }
7985
80- /** Inject a Run button into a code-cell container. */
81- function addRunButton ( container ) {
82- const btn = document . createElement ( "button" ) ;
83- btn . className = "pyodide-run-btn" ;
84- btn . title = "Run this code (Pyodide)" ;
85- btn . textContent = "▶ Run" ;
86+ /** Convert a code block into an editable cell with Run/Reset/Copy buttons. */
87+ function makeEditable ( container ) {
88+ var preEl = container . querySelector ( "pre" ) ;
89+ if ( ! preEl ) return ;
90+
91+ var codeEl = preEl . querySelector ( "code" ) || preEl ;
92+ var originalCode = codeEl . textContent ;
93+
94+ // Create wrapper
95+ var wrapper = document . createElement ( "div" ) ;
96+ wrapper . className = "pyodide-cell" ;
97+
98+ // Toolbar with buttons
99+ var toolbar = document . createElement ( "div" ) ;
100+ toolbar . className = "pyodide-toolbar" ;
101+
102+ var runBtn = document . createElement ( "button" ) ;
103+ runBtn . className = "pyodide-run-btn" ;
104+ runBtn . textContent = "▶ Run" ;
105+ runBtn . title = "Run this code (Shift+Enter)" ;
106+
107+ var resetBtn = document . createElement ( "button" ) ;
108+ resetBtn . className = "pyodide-reset-btn" ;
109+ resetBtn . textContent = "↺ Reset" ;
110+ resetBtn . title = "Restore original code" ;
111+
112+ var copyBtn = document . createElement ( "button" ) ;
113+ copyBtn . className = "pyodide-copy-btn" ;
114+ copyBtn . textContent = "📋 Copy" ;
115+ copyBtn . title = "Copy code to clipboard" ;
116+
117+ toolbar . appendChild ( runBtn ) ;
118+ toolbar . appendChild ( resetBtn ) ;
119+ toolbar . appendChild ( copyBtn ) ;
120+
121+ // Editable textarea
122+ var textarea = document . createElement ( "textarea" ) ;
123+ textarea . className = "pyodide-editor" ;
124+ textarea . value = originalCode ;
125+ textarea . spellcheck = false ;
126+ textarea . autocomplete = "off" ;
127+ textarea . autocorrect = "off" ;
128+ textarea . autocapitalize = "off" ;
86129
87- btn . addEventListener ( "click" , async ( ) => {
88- const codeEl = container . querySelector ( "pre code, pre" ) ;
89- if ( ! codeEl ) return ;
90- const code = codeEl . textContent ;
130+ // Replace the original pre block
131+ wrapper . appendChild ( toolbar ) ;
132+ wrapper . appendChild ( textarea ) ;
133+ container . innerHTML = "" ;
134+ container . appendChild ( wrapper ) ;
91135
92- btn . disabled = true ;
93- btn . textContent = "⏳ Loading…" ;
136+ // Auto-size on load and input
137+ autoResize ( textarea ) ;
138+ textarea . addEventListener ( "input" , function ( ) { autoResize ( textarea ) ; } ) ;
94139
140+ // Shift+Enter to run
141+ textarea . addEventListener ( "keydown" , function ( e ) {
142+ if ( e . key === "Enter" && e . shiftKey ) {
143+ e . preventDefault ( ) ;
144+ runBtn . click ( ) ;
145+ }
146+ // Tab inserts spaces instead of moving focus
147+ if ( e . key === "Tab" ) {
148+ e . preventDefault ( ) ;
149+ var start = textarea . selectionStart ;
150+ var end = textarea . selectionEnd ;
151+ textarea . value = textarea . value . substring ( 0 , start ) + " " + textarea . value . substring ( end ) ;
152+ textarea . selectionStart = textarea . selectionEnd = start + 4 ;
153+ }
154+ } ) ;
155+
156+ // Run button
157+ runBtn . addEventListener ( "click" , async function ( ) {
158+ var code = textarea . value ;
159+ runBtn . disabled = true ;
160+ runBtn . textContent = "⏳ Loading…" ;
95161 try {
96- const pyodide = await loadPyodide ( ) ;
97- btn . textContent = "⏳ Running…" ;
98- const { output , error } = await runCode ( pyodide , code ) ;
99- showOutput ( container , output , error ) ;
162+ var pyodide = await loadPyodide ( ) ;
163+ runBtn . textContent = "⏳ Running…" ;
164+ var result = await runCode ( pyodide , code ) ;
165+ showOutput ( wrapper , result . output , result . error ) ;
100166 } catch ( err ) {
101- showOutput ( container , "Failed to load Pyodide:\n" + err . message , true ) ;
167+ showOutput ( wrapper , "Failed to load Pyodide:\n" + err . message , true ) ;
102168 } finally {
103- btn . disabled = false ;
104- btn . textContent = "▶ Run" ;
169+ runBtn . disabled = false ;
170+ runBtn . textContent = "▶ Run" ;
105171 }
106172 } ) ;
107173
108- // Insert at top of the container, next to the copy button area
109- const highlight = container . querySelector ( ".highlight" ) ;
110- if ( highlight ) {
111- highlight . style . position = "relative" ;
112- highlight . appendChild ( btn ) ;
113- } else {
114- container . prepend ( btn ) ;
115- }
174+ // Reset button
175+ resetBtn . addEventListener ( "click" , function ( ) {
176+ textarea . value = originalCode ;
177+ autoResize ( textarea ) ;
178+ var outputEl = wrapper . querySelector ( ".pyodide-output" ) ;
179+ if ( outputEl ) outputEl . style . display = "none" ;
180+ } ) ;
181+
182+ // Copy button
183+ copyBtn . addEventListener ( "click" , function ( ) {
184+ navigator . clipboard . writeText ( textarea . value ) . then ( function ( ) {
185+ copyBtn . textContent = "✓ Copied!" ;
186+ setTimeout ( function ( ) { copyBtn . textContent = "📋 Copy" ; } , 1500 ) ;
187+ } ) ;
188+ } ) ;
116189 }
117190
118- /** Find all code-cell blocks and attach Run buttons . */
191+ /** Find all code-cell blocks and make them editable . */
119192 function init ( ) {
120193 // myst-nb renders {code-cell} as <div class="cell ..."><div class="cell_input">...
121- const cells = document . querySelectorAll ( ".cell .cell_input" ) ;
122- cells . forEach ( addRunButton ) ;
194+ var cells = document . querySelectorAll ( ".cell .cell_input" ) ;
195+ cells . forEach ( makeEditable ) ;
123196
124- // Also target plain highlighted python blocks produced by myst-nb
197+ // Fallback: target plain highlighted python blocks
125198 if ( cells . length === 0 ) {
126199 document
127- . querySelectorAll ( ' div.highlight-python, div.highlight-default' )
200+ . querySelectorAll ( " div.highlight-python, div.highlight-default" )
128201 . forEach ( function ( block ) {
129- // Skip if already handled
130202 if ( block . closest ( ".cell_input" ) ) return ;
131- addRunButton ( block ) ;
203+ makeEditable ( block ) ;
132204 } ) ;
133205 }
206+
207+ // Preload Pyodide in the background
208+ if ( document . querySelectorAll ( ".cell .cell_input, div.highlight-python" ) . length > 0 ) {
209+ ( typeof requestIdleCallback === "function" ? requestIdleCallback : function ( cb ) { setTimeout ( cb , 2000 ) ; } ) (
210+ function ( ) { loadPyodide ( ) ; }
211+ ) ;
212+ }
134213 }
135214
136215 if ( document . readyState === "loading" ) {
0 commit comments