Skip to content

Commit 153da9d

Browse files
updatd
1 parent aaa8231 commit 153da9d

File tree

2 files changed

+200
-75
lines changed

2 files changed

+200
-75
lines changed

docs/_static/custom.css

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,81 @@
11
/* Custom styles for Python Bro Code docs */
22

3-
/* ── Pyodide Run button ─────────────────────────────────────────────── */
4-
.pyodide-run-btn {
5-
position: absolute;
6-
top: 0.35rem;
7-
right: 2.6rem; /* leave room for the Copy button */
8-
z-index: 10;
9-
padding: 0.15rem 0.55rem;
3+
/* ── Pyodide cell wrapper ────────────────────────────────────────────── */
4+
.pyodide-cell {
5+
border: 1px solid var(--color-background-border, #d0d0d0);
6+
border-radius: 6px;
7+
overflow: hidden;
8+
margin-bottom: 0.5rem;
9+
}
10+
11+
/* ── Toolbar with Run / Reset / Copy ─────────────────────────────────── */
12+
.pyodide-toolbar {
13+
display: flex;
14+
gap: 0.4rem;
15+
padding: 0.3rem 0.5rem;
16+
background: var(--color-background-secondary, #f0f0f0);
17+
border-bottom: 1px solid var(--color-background-border, #d0d0d0);
18+
}
19+
.pyodide-toolbar button {
20+
padding: 0.2rem 0.6rem;
1021
font-size: 0.78rem;
1122
font-weight: 600;
12-
color: #fff;
13-
background: #306998;
1423
border: none;
1524
border-radius: 4px;
1625
cursor: pointer;
17-
opacity: 0;
18-
transition: opacity 0.2s;
26+
transition: background 0.15s, color 0.15s;
1927
}
20-
.highlight:hover .pyodide-run-btn,
21-
.pyodide-run-btn:focus {
22-
opacity: 1;
28+
.pyodide-run-btn {
29+
color: #fff;
30+
background: #306998;
2331
}
2432
.pyodide-run-btn:hover {
2533
background: #FFD43B;
2634
color: #306998;
2735
}
2836
.pyodide-run-btn:disabled {
29-
opacity: 0.9;
37+
opacity: 0.7;
3038
cursor: wait;
3139
}
40+
.pyodide-reset-btn {
41+
color: var(--color-foreground-primary, #333);
42+
background: var(--color-background-primary, #e8e8e8);
43+
}
44+
.pyodide-reset-btn:hover {
45+
background: #ccc;
46+
}
47+
.pyodide-copy-btn {
48+
color: var(--color-foreground-primary, #333);
49+
background: var(--color-background-primary, #e8e8e8);
50+
margin-left: auto;
51+
}
52+
.pyodide-copy-btn:hover {
53+
background: #ccc;
54+
}
55+
56+
/* ── Editable code textarea ──────────────────────────────────────────── */
57+
.pyodide-editor {
58+
display: block;
59+
width: 100%;
60+
box-sizing: border-box;
61+
min-height: 3rem;
62+
padding: 0.75rem 1rem;
63+
margin: 0;
64+
font-family: var(--font-stack--monospace, "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace);
65+
font-size: 0.85rem;
66+
line-height: 1.6;
67+
color: var(--color-foreground-primary, #333);
68+
background: var(--color-code-background, #f7f7f7);
69+
border: none;
70+
outline: none;
71+
resize: none;
72+
overflow: hidden;
73+
tab-size: 4;
74+
white-space: pre;
75+
}
76+
.pyodide-editor:focus {
77+
box-shadow: inset 0 0 0 2px #306998;
78+
}
3279

3380
/* ── Output area below code cells ────────────────────────────────────── */
3481
.pyodide-output {
@@ -42,7 +89,6 @@
4289
word-break: break-word;
4390
background: var(--color-background-secondary, #f7f7f7);
4491
border-top: 2px solid #306998;
45-
border-radius: 0 0 4px 4px;
4692
color: var(--color-foreground-primary, #333);
4793
}
4894
.pyodide-output.pyodide-error {

docs/_static/pyodide-runner.js

Lines changed: 138 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
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. */
@@ -22,7 +29,6 @@
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) {
@@ -35,14 +41,11 @@
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

Comments
 (0)