Skip to content

Commit

Permalink
docs: new Try-It page based on plain Pyodide (#3058)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jpivarski authored Apr 10, 2024
1 parent 33310d0 commit 87d2191
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 6 deletions.
5 changes: 5 additions & 0 deletions docs/_static/js/awkward.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
document.addEventListener("DOMContentLoaded", function() {
document.querySelectorAll('a[href="_static/try-it.html"]').forEach(a => {
a.target = "_blank";
});
});
307 changes: 307 additions & 0 deletions docs/_static/try-it.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/jquery"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/jquery.terminal.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/unix_formatting.min.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/[email protected]/css/jquery.terminal.min.css"
rel="stylesheet"
/>
<style>
.terminal {
--size: 1.5;
--color: rgba(255, 255, 255, 0.8);
}
.noblink {
--animation: terminal-none;
}
body {
background-color: black;
}
#jquery-terminal-logo {
color: white;
border-color: white;
position: absolute;
top: 7px;
right: 18px;
z-index: 2;
}
#jquery-terminal-logo a {
color: gray;
text-decoration: none;
font-size: 0.7em;
}
#loading {
display: inline-block;
width: 50px;
height: 50px;
position: fixed;
top: 50%;
left: 50%;
border: 3px solid rgba(172, 237, 255, 0.5);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="jquery-terminal-logo">
<a href="https://terminal.jcubic.pl/">jQuery Terminal</a>
</div>
<div id="loading"></div>
<script>
"use strict";

function sleep(s) {
return new Promise((resolve) => setTimeout(resolve, s));
}

async function main() {
let indexURL = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/";
const urlParams = new URLSearchParams(window.location.search);
const buildParam = urlParams.get("build");
if (buildParam) {
if (["full", "debug", "pyc"].includes(buildParam)) {
indexURL = indexURL.replace(
"/full/",
"/" + urlParams.get("build") + "/",
);
} else {
console.warn(
'Invalid URL parameter: build="' +
buildParam +
'". Using default "full".',
);
}
}
const { loadPyodide } = await import(indexURL + "pyodide.mjs");
// to facilitate debugging
globalThis.loadPyodide = loadPyodide;

let term;
globalThis.pyodide = await loadPyodide({
stdin: () => {
let result = prompt();
echo(result);
return result;
},
});
let namespace = pyodide.globals.get("dict")();
pyodide.runPython(
`
import sys
from pyodide.ffi import to_js
from pyodide.console import PyodideConsole, repr_shorten, BANNER
import __main__
BANNER = "Welcome to the Pyodide terminal emulator 🐍\\n" + BANNER
pyconsole = PyodideConsole(__main__.__dict__)
import builtins
async def await_fut(fut):
res = await fut
if res is not None:
builtins._ = res
return to_js([res], depth=1)
def clear_console():
pyconsole.buffer = []
`,
{ globals: namespace },
);
let repr_shorten = namespace.get("repr_shorten");
let banner = namespace.get("BANNER");
let await_fut = namespace.get("await_fut");
let pyconsole = namespace.get("pyconsole");
let clear_console = namespace.get("clear_console");
const echo = (msg, ...opts) =>
term.echo(
msg
.replaceAll("]]", "&rsqb;&rsqb;")
.replaceAll("[[", "&lsqb;&lsqb;"),
...opts,
);
namespace.destroy();

let ps1 = ">>> ",
ps2 = "... ";

async function lock() {
let resolve;
let ready = term.ready;
term.ready = new Promise((res) => (resolve = res));
await ready;
return resolve;
}

async function interpreter(command) {
let unlock = await lock();
term.pause();
// multiline should be split (useful when pasting)
for (const c of command.split("\n")) {
const escaped = c.replaceAll(/\u00a0/g, " ");
let fut = pyconsole.push(escaped);
term.set_prompt(fut.syntax_check === "incomplete" ? ps2 : ps1);
switch (fut.syntax_check) {
case "syntax-error":
term.error(fut.formatted_error.trimEnd());
continue;
case "incomplete":
continue;
case "complete":
break;
default:
throw new Error(`Unexpected type ${ty}`);
}
// In JavaScript, await automatically also awaits any results of
// awaits, so if an async function returns a future, it will await
// the inner future too. This is not what we want so we
// temporarily put it into a list to protect it.
let wrapped = await_fut(fut);
// complete case, get result / error and print it.
try {
let [value] = await wrapped;
if (value !== undefined) {
echo(
repr_shorten.callKwargs(value, {
separator: "\n<long output truncated>\n",
}),
);
}
if (value instanceof pyodide.ffi.PyProxy) {
value.destroy();
}
} catch (e) {
if (e.constructor.name === "PythonError") {
const message = fut.formatted_error || e.message;
term.error(message.trimEnd());
} else {
throw e;
}
} finally {
fut.destroy();
wrapped.destroy();
}
}
term.resume();
await sleep(10);
unlock();
}

term = $("body").terminal(interpreter, {
greetings: banner,
prompt: ps1,
completionEscape: false,
completion: function (command, callback) {
callback(pyconsole.complete(command).toJs()[0]);
},
keymap: {
"CTRL+C": async function (event, original) {
clear_console();
term.enter();
echo("KeyboardInterrupt");
term.set_command("");
term.set_prompt(ps1);
},
TAB: (event, original) => {
const command = term.before_cursor();
// Disable completion for whitespaces.
if (command.trim() === "") {
term.insert("\t");
return false;
}
return original(event);
},
},
});
window.term = term;
pyconsole.stdout_callback = (s) => echo(s, { newline: false });
pyconsole.stderr_callback = (s) => {
term.error(s.trimEnd());
};
term.ready = Promise.resolve();
pyodide._api.on_fatal = async (e) => {
if (e.name === "Exit") {
term.error(e);
term.error("Pyodide exited and can no longer be used.");
} else {
term.error(
"Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.",
);
term.error("The cause of the fatal error was:");
term.error(e);
term.error("Look in the browser console for more details.");
}
await term.ready;
term.pause();
await sleep(15);
term.pause();
};

const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has("noblink")) {
$(".cmd-cursor").addClass("noblink");
}
}
window.console_ready = main();
</script>

<script>
"use strict";

window.console_ready.then(function() {
term.echo("Loading NumPy and Awkward Array...");
document.getElementById("loading").style.zIndex = 1000;
pyodide.loadPackage("micropip").then(function() {
let namespace = pyodide.globals.get("dict")();
pyodide.runPython(
`
import micropip
import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(micropip.install("awkward==2.5.0"))
`,
{ globals: namespace },
).then(function() {
namespace.destroy();
var command = ``;
pyodide.runPython(
`
import numpy as np
import awkward as ak
example = ak.Array([
[{"x": 1.1, "y": [1]}, {"x": 2.2, "y": [1, 2]}, {"x": 3.3, "y": [1, 2, 3]}],
[],
[{"x": 4.4, "y": [1, 2, 3, 4]}, {"x": 5.5, "y": [1, 2, 3, 4, 5]}]
])
`,
);
term.echo(
`>>> import numpy as np
>>> import awkward as ak
>>> example = ak.Array([
... [{"x": 1.1, "y": [1]}, {"x": 2.2, "y": [1, 2]}, {"x": 3.3, "y": [1, 2, 3]}],
... [],
... [{"x": 4.4, "y": [1, 2, 3, 4]}, {"x": 5.5, "y": [1, 2, 3, 4, 5]}]
... ])`,
);
document.getElementById("loading").style.zIndex = -1;
});
});
});
</script>

</body>
</html>
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = ["css/awkward.css"]
html_js_files = ["js/awkward.js"]

# MyST settings
myst_enable_extensions = ["colon_fence"]
Expand Down
11 changes: 5 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ Awkward Array is a library for **nested, variable-sized data**, including arbitr
:class: shield-badge
:::

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

::::

Expand Down

0 comments on commit 87d2191

Please sign in to comment.