Skip to content

Commit 3c4d273

Browse files
updatd
1 parent 153da9d commit 3c4d273

File tree

1 file changed

+113
-87
lines changed

1 file changed

+113
-87
lines changed

docs/_static/pyodide-runner.js

Lines changed: 113 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,67 +4,72 @@
44
* Replaces static <pre> blocks with editable <textarea> elements.
55
* Users can modify code, click Run to execute via Pyodide (in-browser
66
* WebAssembly Python), and click Reset to restore the original code.
7+
*
8+
* Compatible with Chrome, Edge, Firefox, Safari.
79
*/
810
(function () {
911
"use strict";
1012

11-
const PYODIDE_CDN =
13+
var PYODIDE_CDN =
1214
"https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js";
1315

1416
// 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);
17+
try {
18+
var prelink = document.createElement("link");
19+
prelink.rel = "preload";
20+
prelink.href = PYODIDE_CDN;
21+
prelink.as = "script";
22+
prelink.crossOrigin = "anonymous";
23+
document.head.appendChild(prelink);
24+
} catch (e) { /* preload is optional */ }
2025

21-
let pyodidePromise = null;
26+
var pyodideReady = null;
2227

2328
/** Lazy-load Pyodide the first time someone clicks Run. */
24-
function loadPyodide() {
25-
if (pyodidePromise) return pyodidePromise;
29+
function ensurePyodide() {
30+
if (pyodideReady) return pyodideReady;
2631

27-
pyodidePromise = new Promise((resolve, reject) => {
28-
const script = document.createElement("script");
32+
pyodideReady = new Promise(function (resolve, reject) {
33+
var script = document.createElement("script");
2934
script.src = PYODIDE_CDN;
30-
script.onload = async () => {
31-
try {
32-
const py = await globalThis.loadPyodide();
33-
resolve(py);
34-
} catch (err) {
35-
reject(err);
36-
}
35+
script.crossOrigin = "anonymous";
36+
script.onload = function () {
37+
// Pyodide script sets window.loadPyodide
38+
var loader = (typeof globalThis !== "undefined" && globalThis.loadPyodide) || window.loadPyodide;
39+
if (!loader) { reject(new Error("Pyodide loader not found")); return; }
40+
loader().then(resolve).catch(reject);
3741
};
38-
script.onerror = reject;
42+
script.onerror = function () { reject(new Error("Failed to load Pyodide CDN")); };
3943
document.head.appendChild(script);
4044
});
41-
return pyodidePromise;
45+
return pyodideReady;
4246
}
4347

44-
/** Run a code string and return { output, error }. */
45-
async function runCode(pyodide, code) {
48+
/** Run Python code and capture stdout/stderr. */
49+
function runCode(pyodide, code) {
4650
pyodide.runPython(
4751
"import sys, io\nsys.stdout = io.StringIO()\nsys.stderr = io.StringIO()"
4852
);
4953
try {
50-
const result = pyodide.runPython(code);
51-
const stdout = pyodide.runPython("sys.stdout.getvalue()");
52-
const stderr = pyodide.runPython("sys.stderr.getvalue()");
53-
let output = stdout;
54+
var result = pyodide.runPython(code);
55+
var stdout = pyodide.runPython("sys.stdout.getvalue()");
56+
var stderr = pyodide.runPython("sys.stderr.getvalue()");
57+
var output = stdout;
5458
if (stderr) output += stderr;
5559
if (result !== undefined && result !== null && String(result) !== "None") {
5660
output += String(result);
5761
}
5862
return { output: output, error: false };
5963
} catch (err) {
60-
const stderr = pyodide.runPython("sys.stderr.getvalue()");
61-
return { output: stderr + "\n" + err.message, error: true };
64+
var errStderr = "";
65+
try { errStderr = pyodide.runPython("sys.stderr.getvalue()"); } catch (e) { /* ignore */ }
66+
return { output: errStderr + "\n" + err.message, error: true };
6267
} finally {
63-
pyodide.runPython("sys.stdout = sys.__stdout__\nsys.stderr = sys.__stderr__");
68+
try { pyodide.runPython("sys.stdout = sys.__stdout__\nsys.stderr = sys.__stderr__"); } catch (e) { /* ignore */ }
6469
}
6570
}
6671

67-
/** Show or update the output area below the editor. */
72+
/** Show or update the output area. */
6873
function showOutput(wrapper, text, isError) {
6974
var el = wrapper.querySelector(".pyodide-output");
7075
if (!el) {
@@ -73,45 +78,74 @@
7378
wrapper.appendChild(el);
7479
}
7580
el.textContent = text || "(no output)";
76-
el.classList.toggle("pyodide-error", isError);
81+
if (isError) { el.classList.add("pyodide-error"); } else { el.classList.remove("pyodide-error"); }
7782
el.style.display = "block";
7883
}
7984

80-
/** Auto-resize textarea to fit content. */
81-
function autoResize(textarea) {
82-
textarea.style.height = "auto";
83-
textarea.style.height = textarea.scrollHeight + "px";
85+
/** Auto-resize textarea to fit its content. */
86+
function autoResize(ta) {
87+
ta.style.height = "auto";
88+
ta.style.height = ta.scrollHeight + "px";
8489
}
8590

86-
/** Convert a code block into an editable cell with Run/Reset/Copy buttons. */
91+
/** Cross-browser copy to clipboard. */
92+
function copyToClipboard(text, btn) {
93+
function onSuccess() {
94+
btn.textContent = "✓ Copied!";
95+
setTimeout(function () { btn.textContent = "\uD83D\uDCCB Copy"; }, 1500);
96+
}
97+
function onFail() {
98+
// Fallback: select a hidden textarea
99+
var tmp = document.createElement("textarea");
100+
tmp.value = text;
101+
tmp.style.position = "fixed";
102+
tmp.style.opacity = "0";
103+
document.body.appendChild(tmp);
104+
tmp.select();
105+
try { document.execCommand("copy"); onSuccess(); } catch (e) { /* silent */ }
106+
document.body.removeChild(tmp);
107+
}
108+
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
109+
navigator.clipboard.writeText(text).then(onSuccess).catch(onFail);
110+
} else {
111+
onFail();
112+
}
113+
}
114+
115+
/** Convert a code block into an editable cell with Run / Reset / Copy. */
87116
function makeEditable(container) {
88117
var preEl = container.querySelector("pre");
89118
if (!preEl) return;
119+
// Avoid double-init
120+
if (container.querySelector(".pyodide-cell")) return;
90121

91122
var codeEl = preEl.querySelector("code") || preEl;
92123
var originalCode = codeEl.textContent;
93124

94-
// Create wrapper
125+
// Build wrapper
95126
var wrapper = document.createElement("div");
96127
wrapper.className = "pyodide-cell";
97128

98-
// Toolbar with buttons
129+
// Toolbar
99130
var toolbar = document.createElement("div");
100131
toolbar.className = "pyodide-toolbar";
101132

102133
var runBtn = document.createElement("button");
103134
runBtn.className = "pyodide-run-btn";
104-
runBtn.textContent = "▶ Run";
135+
runBtn.type = "button";
136+
runBtn.textContent = "\u25B6 Run";
105137
runBtn.title = "Run this code (Shift+Enter)";
106138

107139
var resetBtn = document.createElement("button");
108140
resetBtn.className = "pyodide-reset-btn";
109-
resetBtn.textContent = "↺ Reset";
141+
resetBtn.type = "button";
142+
resetBtn.textContent = "\u21BA Reset";
110143
resetBtn.title = "Restore original code";
111144

112145
var copyBtn = document.createElement("button");
113146
copyBtn.className = "pyodide-copy-btn";
114-
copyBtn.textContent = "📋 Copy";
147+
copyBtn.type = "button";
148+
copyBtn.textContent = "\uD83D\uDCCB Copy";
115149
copyBtn.title = "Copy code to clipboard";
116150

117151
toolbar.appendChild(runBtn);
@@ -123,98 +157,90 @@
123157
textarea.className = "pyodide-editor";
124158
textarea.value = originalCode;
125159
textarea.spellcheck = false;
126-
textarea.autocomplete = "off";
127-
textarea.autocorrect = "off";
128-
textarea.autocapitalize = "off";
160+
textarea.setAttribute("autocomplete", "off");
161+
textarea.setAttribute("autocorrect", "off");
162+
textarea.setAttribute("autocapitalize", "off");
129163

130-
// Replace the original pre block
164+
// Replace original content
131165
wrapper.appendChild(toolbar);
132166
wrapper.appendChild(textarea);
133167
container.innerHTML = "";
134168
container.appendChild(wrapper);
135169

136-
// Auto-size on load and input
170+
// Size & events
137171
autoResize(textarea);
138172
textarea.addEventListener("input", function () { autoResize(textarea); });
139173

140-
// Shift+Enter to run
141174
textarea.addEventListener("keydown", function (e) {
142175
if (e.key === "Enter" && e.shiftKey) {
143176
e.preventDefault();
144177
runBtn.click();
145178
}
146-
// Tab inserts spaces instead of moving focus
147179
if (e.key === "Tab") {
148180
e.preventDefault();
149-
var start = textarea.selectionStart;
181+
var s = textarea.selectionStart;
150182
var end = textarea.selectionEnd;
151-
textarea.value = textarea.value.substring(0, start) + " " + textarea.value.substring(end);
152-
textarea.selectionStart = textarea.selectionEnd = start + 4;
183+
textarea.value = textarea.value.substring(0, s) + " " + textarea.value.substring(end);
184+
textarea.selectionStart = textarea.selectionEnd = s + 4;
153185
}
154186
});
155187

156-
// Run button
157-
runBtn.addEventListener("click", async function () {
188+
runBtn.addEventListener("click", function () {
158189
var code = textarea.value;
159190
runBtn.disabled = true;
160-
runBtn.textContent = "⏳ Loading…";
161-
try {
162-
var pyodide = await loadPyodide();
163-
runBtn.textContent = "⏳ Running…";
164-
var result = await runCode(pyodide, code);
191+
runBtn.textContent = "\u23F3 Loading\u2026";
192+
ensurePyodide().then(function (pyodide) {
193+
runBtn.textContent = "\u23F3 Running\u2026";
194+
var result = runCode(pyodide, code);
165195
showOutput(wrapper, result.output, result.error);
166-
} catch (err) {
196+
}).catch(function (err) {
167197
showOutput(wrapper, "Failed to load Pyodide:\n" + err.message, true);
168-
} finally {
198+
}).finally(function () {
169199
runBtn.disabled = false;
170-
runBtn.textContent = " Run";
171-
}
200+
runBtn.textContent = "\u25B6 Run";
201+
});
172202
});
173203

174-
// Reset button
175204
resetBtn.addEventListener("click", function () {
176205
textarea.value = originalCode;
177206
autoResize(textarea);
178-
var outputEl = wrapper.querySelector(".pyodide-output");
179-
if (outputEl) outputEl.style.display = "none";
207+
var out = wrapper.querySelector(".pyodide-output");
208+
if (out) out.style.display = "none";
180209
});
181210

182-
// Copy button
183211
copyBtn.addEventListener("click", function () {
184-
navigator.clipboard.writeText(textarea.value).then(function () {
185-
copyBtn.textContent = "✓ Copied!";
186-
setTimeout(function () { copyBtn.textContent = "📋 Copy"; }, 1500);
187-
});
212+
copyToClipboard(textarea.value, copyBtn);
188213
});
189214
}
190215

191-
/** Find all code-cell blocks and make them editable. */
216+
/** Find code-cell blocks and make them editable. */
192217
function init() {
193-
// myst-nb renders {code-cell} as <div class="cell ..."><div class="cell_input">...
194218
var cells = document.querySelectorAll(".cell .cell_input");
195-
cells.forEach(makeEditable);
219+
console.log("[pyodide-runner] Found " + cells.length + " code cells");
220+
221+
for (var i = 0; i < cells.length; i++) {
222+
try { makeEditable(cells[i]); } catch (e) { console.error("[pyodide-runner]", e); }
223+
}
196224

197-
// Fallback: target plain highlighted python blocks
225+
// Fallback for pages without {code-cell} blocks
198226
if (cells.length === 0) {
199-
document
200-
.querySelectorAll("div.highlight-python, div.highlight-default")
201-
.forEach(function (block) {
202-
if (block.closest(".cell_input")) return;
203-
makeEditable(block);
204-
});
227+
var blocks = document.querySelectorAll("div.highlight-python, div.highlight-default");
228+
for (var j = 0; j < blocks.length; j++) {
229+
if (blocks[j].parentElement && blocks[j].parentElement.classList.contains("cell_input")) continue;
230+
try { makeEditable(blocks[j]); } catch (e) { console.error("[pyodide-runner]", e); }
231+
}
205232
}
206233

207234
// 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-
}
235+
setTimeout(function () { ensurePyodide(); }, 2000);
213236
}
214237

238+
// Run after DOM is ready and copybutton.js has finished (it uses setTimeout 250ms)
239+
function scheduleInit() { setTimeout(init, 500); }
240+
215241
if (document.readyState === "loading") {
216-
document.addEventListener("DOMContentLoaded", init);
242+
document.addEventListener("DOMContentLoaded", scheduleInit);
217243
} else {
218-
init();
244+
scheduleInit();
219245
}
220246
})();

0 commit comments

Comments
 (0)