Skip to content

Commit 85a178a

Browse files
committed
Add official support for Pyodide with multiple code editors
1 parent d86b3dd commit 85a178a

File tree

7 files changed

+234
-45
lines changed

7 files changed

+234
-45
lines changed

_includes/navbar.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="d-flex flex-column flex-shrink-0 sidebar" style="height: 100vh;">
22
<ul class="nav nav-pills nav-flush flex-column mb-auto text-center">
3-
<li class="nav-item">
3+
<li class="nav-item mt-4">
44
{% if page.current_page == 'home' %}
55
<a href="/" class="nav-link active py-3" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-original-title="Home">
66
<i style="font-size: 20px;" class="fas fa-home text-dark"></i>

_layouts/bootstrap.html

+10-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<link rel="icon" href="/assets/img/logo.png" type="image/x-icon">
88
<!-- Bootstrap CSS -->
99
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous">
10-
<!-- <script src="https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js"></script> -->
10+
<script src="https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js"></script>
1111
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script>
1212
<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script>
1313
<script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script>
@@ -21,12 +21,17 @@
2121
</head>
2222
<body>
2323
{{ content }}
24-
<script src="https://pagecdn.io/lib/ace/1.4.12/ace.min.js" crossorigin="anonymous" integrity="sha256-T5QdmsCQO5z8tBAXMrCZ4f3RX8wVdiA0Fu17FGnU1vU=" ></script>
25-
<script src="https://pagecdn.io/lib/ace/1.4.12/theme-monokai.min.js" crossorigin="anonymous" ></script>
26-
<script src="https://pagecdn.io/lib/ace/1.4.12/mode-python.min.js" crossorigin="anonymous" ></script>
27-
<script src="/assets/js/code-editor.js"></script>
24+
25+
2826
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
2927
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
28+
29+
{% if page.code_editor %}
30+
<script src="https://pagecdn.io/lib/ace/1.4.12/ace.min.js" crossorigin="anonymous" integrity="sha256-T5QdmsCQO5z8tBAXMrCZ4f3RX8wVdiA0Fu17FGnU1vU=" ></script>
31+
<script src="https://pagecdn.io/lib/ace/1.4.12/theme-monokai.min.js" crossorigin="anonymous" ></script>
32+
<script src="https://pagecdn.io/lib/ace/1.4.12/mode-python.min.js" crossorigin="anonymous" ></script>
33+
<script type="module" src="/assets/js/code-editor.js"></script>
34+
{% endif %}
3035
<script src="/assets/js/script.js"></script>
3136
</body>
3237
</html>

_posts/2021-10-14-lesson-1.md

+15-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ date: 2021-10-14 00.00.00 -0400
55
categories: group-a
66
group: a
77
current_page: lessons
8+
code_editor: true
89
---
910
# Lesson 1 - October 14 - Conditionals and Loops
1011

@@ -23,8 +24,9 @@ An if is a conditional.
2324

2425
A for loop is a loop.
2526

26-
<div class="editor-container p-3 pt-0">
27-
<button onclick="runit()" class="btn btn-primary my-3">Run</button>
27+
<!-- <div class="editor-container p-3 pt-0">
28+
<button id="run-button" class="btn btn-primary my-3">Run</button>
29+
<button data-bs-toggle="tooltip" data-bs-placement="right" data-bs-original-title="Clicking this will restart the Python interpreter, so be careful!" id="terminate-button" class="btn btn-danger my-3 ms-2">Terminate Pyodide</button>
2830
<div id="editor"></div>
2931
<div class="mt-3 input-output-container">
3032
<div data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Input" class="d-block position-relative input-container">
@@ -34,4 +36,14 @@ A for loop is a loop.
3436
3537
</div>
3638
</div>
37-
</div>
39+
</div> -->
40+
41+
<div is="code-editor" id="editor-1">
42+
for i in range(5):
43+
print(i)
44+
</div>
45+
46+
<div is="code-editor" id="editor-2">
47+
for i in range(5):
48+
print(i)
49+
</div>

assets/css/code-editor.scss

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
@use "variables";
55

6-
#editor {
6+
.editor {
77
position: relative;
88
width: 100%;
99
height: 400px;
@@ -12,6 +12,7 @@
1212
.editor-container {
1313
width: 70%;
1414
background-color: #424242;
15+
margin-bottom: 1rem;
1516
}
1617

1718
.input-output-container {

assets/js/code-editor.js

+112-35
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,127 @@
1-
var editor = ace.edit("editor");
2-
editor.setTheme("ace/theme/monokai");
3-
editor.session.setMode("ace/mode/python");
1+
class CodeEditor extends HTMLDivElement {
2+
constructor() {
3+
super();
4+
5+
let id = this.getAttribute('id');
6+
7+
this.setAttribute("class", "editor-container p-3 pt-0")
8+
9+
this.innerHTML = `
10+
<button id="run-button-${id}" class="btn btn-primary my-3">Run</button>
11+
<button id="terminate-button-${id}" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-original-title="Clicking this will restart the Python interpreter, so be careful!" class="btn btn-danger my-3 ms-2">Terminate Pyodide</button>
12+
<div class="editor" id="ace-editor-${id}">${this.innerHTML}</div>
13+
<div class="mt-3 input-output-container">
14+
<div data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Input" class="d-block position-relative input-container">
15+
<textarea id="input-${id}" class="input"></textarea>
16+
</div>
17+
<div id="output-${id}" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-original-title="Output" class="output-container">
18+
19+
</div>
20+
</div>
21+
`
22+
23+
const editor = ace.edit(`ace-editor-${id}`);
24+
editor.setTheme("ace/theme/monokai");
25+
editor.session.setMode("ace/mode/python");
26+
27+
28+
document.getElementById(`run-button-${id}`).onclick = async () => {
29+
await runPython(editor.getValue(), `input-${id}`, `output-${id}`);
30+
};
31+
32+
document.getElementById(`terminate-button-${id}`).onclick = async () => {
33+
await terminatePyodide();
34+
};
35+
}
36+
}
437

538

639

7-
function outf(text) {
8-
const output_element = document.getElementById("output");
9-
output_element.innerText = output_element.innerText + text;
10-
}
40+
customElements.define('code-editor', CodeEditor, { extends: "div" });
1141

42+
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
43+
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
44+
return new bootstrap.Tooltip(tooltipTriggerEl)
45+
})
1246

13-
var current_inp = 0;
1447

15-
function get_input(prompt) {
16-
return new Promise((resolve, reject) => {
17-
let input = document.getElementById('input-tmp').value.trim().split(/\r?\n/);
18-
if (input[0] == '') {
19-
input = [];
20-
}
21-
if (current_inp >= input.length) {
22-
throw "Place your input(s) in the \"input\" box!";
48+
49+
import { asyncRun, asyncTerminate } from "/assets/js/py-worker.js";
50+
51+
52+
53+
54+
55+
async function runPython(script, input_id, output_id) {
56+
let stdin = document.getElementById(input_id).value;
57+
try {
58+
const { results, error } = await asyncRun(script, stdin);
59+
if (results !== undefined) {
60+
61+
document.getElementById(output_id).innerText = results;
62+
} else if (error) {
63+
document.getElementById(output_id).innerText = error;
2364
}
24-
resolve(input[current_inp]);
25-
current_inp++;
26-
})
65+
} catch (e) {
66+
document.getElementById(output_id).innerText = e.message
67+
}
2768
}
2869

29-
function runit() {
30-
var prog = editor.getValue();
31-
current_inp = 0;
32-
var output_element = document.getElementById("output");
33-
output_element.innerText = '';
34-
Sk.pre = "output";
35-
Sk.configure({ output: outf, inputfun: get_input, inputfunTakesPrompt: true, execLimit: 1000 });
36-
var myPromise = Sk.misceval.asyncToPromise(function () {
37-
return Sk.importMainWithBody("<stdin>", false, prog, true);
38-
});
39-
myPromise.then(function (mod) {
40-
41-
},
42-
function (err) {
43-
output_element.innerText = err.toString();
44-
});
70+
async function terminatePyodide() {
71+
asyncTerminate();
4572
}
4673

4774

75+
76+
// Uncomment the below lines to use Skulpt
77+
78+
// function outf(text) {
79+
// const output_element = document.getElementById("output");
80+
// output_element.innerText = output_element.innerText + text;
81+
// }
82+
83+
84+
// var current_inp = 0;
85+
86+
// function get_input(prompt) {
87+
// return new Promise((resolve, reject) => {
88+
// let input = document.getElementById('input-tmp').value.trim().split(/\r?\n/);
89+
// if (input[0] == '') {
90+
// input = [];
91+
// }
92+
// if (current_inp >= input.length) {
93+
// throw "Place your input(s) in the \"input\" box!";
94+
// }
95+
// resolve(input[current_inp]);
96+
// current_inp++;
97+
// })
98+
// }
99+
100+
// function runit() {
101+
// var prog = editor.getValue();
102+
// current_inp = 0;
103+
// var output_element = document.getElementById("output");
104+
// output_element.innerText = '';
105+
// Sk.pre = "output";
106+
// Sk.configure({
107+
// output: outf,
108+
// inputfun: get_input,
109+
// inputfunTakesPrompt: true,
110+
// execLimit: 1000,
111+
// __future__: Sk.python3
112+
// });
113+
// var myPromise = Sk.misceval.asyncToPromise(function () {
114+
// return Sk.importMainWithBody("<stdin>", false, prog, true);
115+
// });
116+
// myPromise.then(function (mod) {
117+
118+
// },
119+
// function (err) {
120+
// output_element.innerText = err.toString();
121+
// });
122+
// }
123+
124+
48125
// Uncomment below lines to use Pyodide instead of Skulpt!
49126

50127
// async function setup_pyodide() {

assets/js/py-worker.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
var pyodideWorker = new Worker("/assets/js/webworker.js");
2+
3+
var terminated = false;
4+
5+
export function run(script, stdin, onSuccess, onError) {
6+
pyodideWorker.onerror = onError;
7+
pyodideWorker.onmessage = (e) => onSuccess(e.data);
8+
pyodideWorker.postMessage({
9+
stdin,
10+
python: script,
11+
});
12+
}
13+
14+
export function asyncRun(script, stdin) {
15+
16+
if (terminated) {
17+
terminated = false;
18+
pyodideWorker = new Worker("/assets/js/webworker.js");
19+
}
20+
21+
return new Promise(function (onSuccess, onError) {
22+
run(script, stdin, onSuccess, onError);
23+
});
24+
}
25+
26+
export function asyncTerminate() {
27+
if (!terminated) {
28+
pyodideWorker.terminate()
29+
terminated = true;
30+
}
31+
}

assets/js/webworker.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// webworker.js
2+
3+
// Setup your project to serve `py-worker.js`. You should also serve
4+
// `pyodide.js`, and all its associated `.asm.js`, `.data`, `.json`,
5+
// and `.wasm` files as well:
6+
importScripts("https://cdn.jsdelivr.net/pyodide/v0.18.1/full/pyodide.js");
7+
8+
async function loadPyodideAndPackages() {
9+
self.pyodide = await loadPyodide({
10+
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.18.1/full/",
11+
});
12+
}
13+
let pyodideReadyPromise = loadPyodideAndPackages();
14+
15+
async function setup_function() {
16+
await pyodideReadyPromise;
17+
let code = `
18+
import sys, io, traceback
19+
namespace = {} # use separate namespace to hide run_code, modules, etc.
20+
def run_code(code):
21+
"""run specified code and return stdout and stderr"""
22+
out = io.StringIO()
23+
oldout = sys.stdout
24+
olderr = sys.stderr
25+
sys.stdin = io.StringIO(inp)
26+
sys.stdout = sys.stderr = out
27+
try:
28+
# change next line to exec(code, {}) if you want to clear vars each time
29+
exec(code, {})
30+
31+
except:
32+
traceback.print_exc()
33+
34+
sys.stdout = oldout
35+
sys.stderr = olderr
36+
return out.getvalue()
37+
`
38+
self.pyodide.runPythonAsync(code);
39+
}
40+
41+
let setup = setup_function()
42+
43+
self.onmessage = async (event) => {
44+
// make sure loading is done
45+
await pyodideReadyPromise;
46+
await setup;
47+
// Don't bother yet with this line, suppose our API is built in such a way:
48+
const { python, stdin } = event.data;
49+
// The worker copies the context in its own "memory" (an object mapping name to values)
50+
// Now is the easy part, the one that is similar to working in the main thread:
51+
52+
try {
53+
await self.pyodide.loadPackagesFromImports(python);
54+
// let results = await self.pyodide.runPythonAsync(python);
55+
56+
self.pyodide.globals.code_to_run = python
57+
self.pyodide.globals.inp = stdin
58+
let results = await self.pyodide.runPythonAsync('run_code(code_to_run)')
59+
self.postMessage({ results });
60+
} catch (error) {
61+
self.postMessage({ error: error.message });
62+
}
63+
};

0 commit comments

Comments
 (0)