Skip to content

Commit 87d2191

Browse files
authored
docs: new Try-It page based on plain Pyodide (#3058)
* docs: new Try-It page based on plain Pyodide * fix link; no idea how to make it open in a new tab * make the 'try-it' button open a new tab * why wasn't it found? * was it the Unicode? * no, it was me not shift-reloading the page (to clear cache) * the arrow also has to be in the badge, not just alt-text
1 parent 33310d0 commit 87d2191

File tree

4 files changed

+318
-6
lines changed

4 files changed

+318
-6
lines changed

docs/_static/js/awkward.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
document.addEventListener("DOMContentLoaded", function() {
2+
document.querySelectorAll('a[href="_static/try-it.html"]').forEach(a => {
3+
a.target = "_blank";
4+
});
5+
});

docs/_static/try-it.html

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<script src="https://cdn.jsdelivr.net/npm/jquery"></script>
6+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/jquery.terminal.min.js"></script>
7+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/unix_formatting.min.js"></script>
8+
<link
9+
href="https://cdn.jsdelivr.net/npm/[email protected]/css/jquery.terminal.min.css"
10+
rel="stylesheet"
11+
/>
12+
<style>
13+
.terminal {
14+
--size: 1.5;
15+
--color: rgba(255, 255, 255, 0.8);
16+
}
17+
.noblink {
18+
--animation: terminal-none;
19+
}
20+
body {
21+
background-color: black;
22+
}
23+
#jquery-terminal-logo {
24+
color: white;
25+
border-color: white;
26+
position: absolute;
27+
top: 7px;
28+
right: 18px;
29+
z-index: 2;
30+
}
31+
#jquery-terminal-logo a {
32+
color: gray;
33+
text-decoration: none;
34+
font-size: 0.7em;
35+
}
36+
#loading {
37+
display: inline-block;
38+
width: 50px;
39+
height: 50px;
40+
position: fixed;
41+
top: 50%;
42+
left: 50%;
43+
border: 3px solid rgba(172, 237, 255, 0.5);
44+
border-radius: 50%;
45+
border-top-color: #fff;
46+
animation: spin 1s ease-in-out infinite;
47+
-webkit-animation: spin 1s ease-in-out infinite;
48+
}
49+
50+
@keyframes spin {
51+
to {
52+
-webkit-transform: rotate(360deg);
53+
}
54+
}
55+
@-webkit-keyframes spin {
56+
to {
57+
-webkit-transform: rotate(360deg);
58+
}
59+
}
60+
</style>
61+
</head>
62+
<body>
63+
<div id="jquery-terminal-logo">
64+
<a href="https://terminal.jcubic.pl/">jQuery Terminal</a>
65+
</div>
66+
<div id="loading"></div>
67+
<script>
68+
"use strict";
69+
70+
function sleep(s) {
71+
return new Promise((resolve) => setTimeout(resolve, s));
72+
}
73+
74+
async function main() {
75+
let indexURL = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/";
76+
const urlParams = new URLSearchParams(window.location.search);
77+
const buildParam = urlParams.get("build");
78+
if (buildParam) {
79+
if (["full", "debug", "pyc"].includes(buildParam)) {
80+
indexURL = indexURL.replace(
81+
"/full/",
82+
"/" + urlParams.get("build") + "/",
83+
);
84+
} else {
85+
console.warn(
86+
'Invalid URL parameter: build="' +
87+
buildParam +
88+
'". Using default "full".',
89+
);
90+
}
91+
}
92+
const { loadPyodide } = await import(indexURL + "pyodide.mjs");
93+
// to facilitate debugging
94+
globalThis.loadPyodide = loadPyodide;
95+
96+
let term;
97+
globalThis.pyodide = await loadPyodide({
98+
stdin: () => {
99+
let result = prompt();
100+
echo(result);
101+
return result;
102+
},
103+
});
104+
let namespace = pyodide.globals.get("dict")();
105+
pyodide.runPython(
106+
`
107+
import sys
108+
from pyodide.ffi import to_js
109+
from pyodide.console import PyodideConsole, repr_shorten, BANNER
110+
import __main__
111+
BANNER = "Welcome to the Pyodide terminal emulator 🐍\\n" + BANNER
112+
pyconsole = PyodideConsole(__main__.__dict__)
113+
import builtins
114+
async def await_fut(fut):
115+
res = await fut
116+
if res is not None:
117+
builtins._ = res
118+
return to_js([res], depth=1)
119+
def clear_console():
120+
pyconsole.buffer = []
121+
`,
122+
{ globals: namespace },
123+
);
124+
let repr_shorten = namespace.get("repr_shorten");
125+
let banner = namespace.get("BANNER");
126+
let await_fut = namespace.get("await_fut");
127+
let pyconsole = namespace.get("pyconsole");
128+
let clear_console = namespace.get("clear_console");
129+
const echo = (msg, ...opts) =>
130+
term.echo(
131+
msg
132+
.replaceAll("]]", "&rsqb;&rsqb;")
133+
.replaceAll("[[", "&lsqb;&lsqb;"),
134+
...opts,
135+
);
136+
namespace.destroy();
137+
138+
let ps1 = ">>> ",
139+
ps2 = "... ";
140+
141+
async function lock() {
142+
let resolve;
143+
let ready = term.ready;
144+
term.ready = new Promise((res) => (resolve = res));
145+
await ready;
146+
return resolve;
147+
}
148+
149+
async function interpreter(command) {
150+
let unlock = await lock();
151+
term.pause();
152+
// multiline should be split (useful when pasting)
153+
for (const c of command.split("\n")) {
154+
const escaped = c.replaceAll(/\u00a0/g, " ");
155+
let fut = pyconsole.push(escaped);
156+
term.set_prompt(fut.syntax_check === "incomplete" ? ps2 : ps1);
157+
switch (fut.syntax_check) {
158+
case "syntax-error":
159+
term.error(fut.formatted_error.trimEnd());
160+
continue;
161+
case "incomplete":
162+
continue;
163+
case "complete":
164+
break;
165+
default:
166+
throw new Error(`Unexpected type ${ty}`);
167+
}
168+
// In JavaScript, await automatically also awaits any results of
169+
// awaits, so if an async function returns a future, it will await
170+
// the inner future too. This is not what we want so we
171+
// temporarily put it into a list to protect it.
172+
let wrapped = await_fut(fut);
173+
// complete case, get result / error and print it.
174+
try {
175+
let [value] = await wrapped;
176+
if (value !== undefined) {
177+
echo(
178+
repr_shorten.callKwargs(value, {
179+
separator: "\n<long output truncated>\n",
180+
}),
181+
);
182+
}
183+
if (value instanceof pyodide.ffi.PyProxy) {
184+
value.destroy();
185+
}
186+
} catch (e) {
187+
if (e.constructor.name === "PythonError") {
188+
const message = fut.formatted_error || e.message;
189+
term.error(message.trimEnd());
190+
} else {
191+
throw e;
192+
}
193+
} finally {
194+
fut.destroy();
195+
wrapped.destroy();
196+
}
197+
}
198+
term.resume();
199+
await sleep(10);
200+
unlock();
201+
}
202+
203+
term = $("body").terminal(interpreter, {
204+
greetings: banner,
205+
prompt: ps1,
206+
completionEscape: false,
207+
completion: function (command, callback) {
208+
callback(pyconsole.complete(command).toJs()[0]);
209+
},
210+
keymap: {
211+
"CTRL+C": async function (event, original) {
212+
clear_console();
213+
term.enter();
214+
echo("KeyboardInterrupt");
215+
term.set_command("");
216+
term.set_prompt(ps1);
217+
},
218+
TAB: (event, original) => {
219+
const command = term.before_cursor();
220+
// Disable completion for whitespaces.
221+
if (command.trim() === "") {
222+
term.insert("\t");
223+
return false;
224+
}
225+
return original(event);
226+
},
227+
},
228+
});
229+
window.term = term;
230+
pyconsole.stdout_callback = (s) => echo(s, { newline: false });
231+
pyconsole.stderr_callback = (s) => {
232+
term.error(s.trimEnd());
233+
};
234+
term.ready = Promise.resolve();
235+
pyodide._api.on_fatal = async (e) => {
236+
if (e.name === "Exit") {
237+
term.error(e);
238+
term.error("Pyodide exited and can no longer be used.");
239+
} else {
240+
term.error(
241+
"Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.",
242+
);
243+
term.error("The cause of the fatal error was:");
244+
term.error(e);
245+
term.error("Look in the browser console for more details.");
246+
}
247+
await term.ready;
248+
term.pause();
249+
await sleep(15);
250+
term.pause();
251+
};
252+
253+
const searchParams = new URLSearchParams(window.location.search);
254+
if (searchParams.has("noblink")) {
255+
$(".cmd-cursor").addClass("noblink");
256+
}
257+
}
258+
window.console_ready = main();
259+
</script>
260+
261+
<script>
262+
"use strict";
263+
264+
window.console_ready.then(function() {
265+
term.echo("Loading NumPy and Awkward Array...");
266+
document.getElementById("loading").style.zIndex = 1000;
267+
pyodide.loadPackage("micropip").then(function() {
268+
let namespace = pyodide.globals.get("dict")();
269+
pyodide.runPython(
270+
`
271+
import micropip
272+
import asyncio
273+
loop = asyncio.get_event_loop()
274+
loop.run_until_complete(micropip.install("awkward==2.5.0"))
275+
`,
276+
{ globals: namespace },
277+
).then(function() {
278+
namespace.destroy();
279+
var command = ``;
280+
pyodide.runPython(
281+
`
282+
import numpy as np
283+
import awkward as ak
284+
example = ak.Array([
285+
[{"x": 1.1, "y": [1]}, {"x": 2.2, "y": [1, 2]}, {"x": 3.3, "y": [1, 2, 3]}],
286+
[],
287+
[{"x": 4.4, "y": [1, 2, 3, 4]}, {"x": 5.5, "y": [1, 2, 3, 4, 5]}]
288+
])
289+
`,
290+
);
291+
term.echo(
292+
`>>> import numpy as np
293+
>>> import awkward as ak
294+
>>> example = ak.Array([
295+
... [{"x": 1.1, "y": [1]}, {"x": 2.2, "y": [1, 2]}, {"x": 3.3, "y": [1, 2, 3]}],
296+
... [],
297+
... [{"x": 4.4, "y": [1, 2, 3, 4]}, {"x": 5.5, "y": [1, 2, 3, 4, 5]}]
298+
... ])`,
299+
);
300+
document.getElementById("loading").style.zIndex = -1;
301+
});
302+
});
303+
});
304+
</script>
305+
306+
</body>
307+
</html>

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
# so a file named "default.css" will overwrite the builtin "default.css".
138138
html_static_path = ["_static"]
139139
html_css_files = ["css/awkward.css"]
140+
html_js_files = ["js/awkward.js"]
140141

141142
# MyST settings
142143
myst_enable_extensions = ["colon_fence"]

docs/index.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@ Awkward Array is a library for **nested, variable-sized data**, including arbitr
2828
:class: shield-badge
2929
:::
3030

31-
% % Unfortunately, `target` does not support document references
32-
% :::{image} https://img.shields.io/badge/-Try%20It%21-orange?style=for-the-badge
33-
% :alt: Try It!
34-
% :target: getting-started/try-awkward-array.html
35-
% :class: shield-badge
36-
% :::
31+
:::{image} https://img.shields.io/badge/-Try%20It%21%20%E2%86%97-orange?style=for-the-badge
32+
:alt: Try It! ⭷
33+
:target: _static/try-it.html
34+
:class: shield-badge
35+
:::
3736

3837
::::
3938

0 commit comments

Comments
 (0)