Skip to content

Commit 0be5820

Browse files
updatd
1 parent 8865cce commit 0be5820

File tree

4 files changed

+196
-11
lines changed

4 files changed

+196
-11
lines changed

docs/_static/custom.css

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,51 @@
11
/* Custom styles for Python Bro Code docs */
2+
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;
10+
font-size: 0.78rem;
11+
font-weight: 600;
12+
color: #fff;
13+
background: #306998;
14+
border: none;
15+
border-radius: 4px;
16+
cursor: pointer;
17+
opacity: 0;
18+
transition: opacity 0.2s;
19+
}
20+
.highlight:hover .pyodide-run-btn,
21+
.pyodide-run-btn:focus {
22+
opacity: 1;
23+
}
24+
.pyodide-run-btn:hover {
25+
background: #FFD43B;
26+
color: #306998;
27+
}
28+
.pyodide-run-btn:disabled {
29+
opacity: 0.9;
30+
cursor: wait;
31+
}
32+
33+
/* ── Output area below code cells ────────────────────────────────────── */
34+
.pyodide-output {
35+
display: none;
36+
margin: 0;
37+
padding: 0.75rem 1rem;
38+
font-family: var(--font-stack--monospace, monospace);
39+
font-size: 0.85rem;
40+
line-height: 1.5;
41+
white-space: pre-wrap;
42+
word-break: break-word;
43+
background: var(--color-background-secondary, #f7f7f7);
44+
border-top: 2px solid #306998;
45+
border-radius: 0 0 4px 4px;
46+
color: var(--color-foreground-primary, #333);
47+
}
48+
.pyodide-output.pyodide-error {
49+
border-top-color: #e74c3c;
50+
color: #e74c3c;
51+
}

docs/_static/pyodide-runner.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* Pyodide-powered "▶ Run" button for code cells.
3+
*
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.
7+
*/
8+
(function () {
9+
"use strict";
10+
11+
const PYODIDE_CDN =
12+
"https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js";
13+
14+
let pyodidePromise = null;
15+
16+
/** Lazy-load Pyodide the first time someone clicks Run. */
17+
function loadPyodide() {
18+
if (pyodidePromise) return pyodidePromise;
19+
20+
pyodidePromise = new Promise((resolve, reject) => {
21+
const script = document.createElement("script");
22+
script.src = PYODIDE_CDN;
23+
script.onload = async () => {
24+
try {
25+
/* global loadPyodide – provided by the CDN script */
26+
const py = await globalThis.loadPyodide();
27+
resolve(py);
28+
} catch (err) {
29+
reject(err);
30+
}
31+
};
32+
script.onerror = reject;
33+
document.head.appendChild(script);
34+
});
35+
return pyodidePromise;
36+
}
37+
38+
/** Run a code string and return { stdout, stderr }. */
39+
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+
try {
47+
const result = pyodide.runPython(code);
48+
const stdout = pyodide.runPython("sys.stdout.getvalue()");
49+
const stderr = pyodide.runPython("sys.stderr.getvalue()");
50+
let output = stdout;
51+
if (stderr) output += stderr;
52+
if (result !== undefined && result !== null && String(result) !== "None") {
53+
output += String(result);
54+
}
55+
return { output: output, error: false };
56+
} catch (err) {
57+
const stderr = pyodide.runPython("sys.stderr.getvalue()");
58+
return { output: stderr + "\n" + err.message, error: true };
59+
} finally {
60+
pyodide.runPython(`
61+
sys.stdout = sys.__stdout__
62+
sys.stderr = sys.__stderr__
63+
`);
64+
}
65+
}
66+
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);
74+
}
75+
outputEl.textContent = text || "(no output)";
76+
outputEl.classList.toggle("pyodide-error", isError);
77+
outputEl.style.display = "block";
78+
}
79+
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+
87+
btn.addEventListener("click", async () => {
88+
const codeEl = container.querySelector("pre code, pre");
89+
if (!codeEl) return;
90+
const code = codeEl.textContent;
91+
92+
btn.disabled = true;
93+
btn.textContent = "⏳ Loading…";
94+
95+
try {
96+
const pyodide = await loadPyodide();
97+
btn.textContent = "⏳ Running…";
98+
const { output, error } = await runCode(pyodide, code);
99+
showOutput(container, output, error);
100+
} catch (err) {
101+
showOutput(container, "Failed to load Pyodide:\n" + err.message, true);
102+
} finally {
103+
btn.disabled = false;
104+
btn.textContent = "▶ Run";
105+
}
106+
});
107+
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+
}
116+
}
117+
118+
/** Find all code-cell blocks and attach Run buttons. */
119+
function init() {
120+
// 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);
123+
124+
// Also target plain highlighted python blocks produced by myst-nb
125+
if (cells.length === 0) {
126+
document
127+
.querySelectorAll('div.highlight-python, div.highlight-default')
128+
.forEach(function (block) {
129+
// Skip if already handled
130+
if (block.closest(".cell_input")) return;
131+
addRunButton(block);
132+
});
133+
}
134+
}
135+
136+
if (document.readyState === "loading") {
137+
document.addEventListener("DOMContentLoaded", init);
138+
} else {
139+
init();
140+
}
141+
})();

docs/conf.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"sphinx.ext.viewcode",
1212
"sphinx_copybutton",
1313
"sphinx_design",
14-
"sphinx_thebe",
1514
]
1615

1716
myst_enable_extensions = [
@@ -53,6 +52,7 @@
5352
html_title = "Python Bro Code"
5453
html_static_path = ["_static"]
5554
html_css_files = ["custom.css"]
55+
html_js_files = ["pyodide-runner.js"]
5656

5757
html_theme_options = {
5858
"light_css_variables": {
@@ -77,13 +77,8 @@
7777
nb_execution_raise_on_error = False
7878

7979
# ---------------------------------------------------------------------------
80-
# sphinx-thebe: live "Run" button (connects to Binder for a free kernel)
80+
# In-browser code execution (Pyodide via _static/pyodide-runner.js)
8181
# ---------------------------------------------------------------------------
82-
thebe_config = {
83-
"repository_url": "https://github.com/PavanMudigonda/python-bro-code",
84-
"repository_branch": "main",
85-
"path_to_docs": "docs/",
86-
}
87-
88-
# sphinx-copybutton already provides the "Copy" button on every code block.
89-
# No extra config needed — both Run and Copy are now available.
82+
# - "Copy" button: provided by sphinx-copybutton on every code block.
83+
# - "Run" button: provided by pyodide-runner.js on every {code-cell} block.
84+
# Loads Pyodide (WebAssembly CPython) on first click — no server needed.

requirements-docs.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ furo>=2024.8.6
33
myst-nb>=1.1.0
44
sphinx-copybutton>=0.5.2
55
sphinx-design>=0.6.0
6-
sphinx-thebe>=0.3.0
76
linkify-it-py>=2.0.0

0 commit comments

Comments
 (0)