-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
329 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
goals: {} | ||
goals: | ||
500: | ||
name: PlasmaVac User Guide | ||
features: | ||
- name: Pyodide fence | ||
ref: ../usage/pyodide/ | ||
since: 2023/04/26 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# Pyodide | ||
|
||
[:octicons-heart-fill-24:{ .pulse } Sponsors only](../../insiders){ .insiders } — | ||
[:octicons-tag-24: Insiders 1.0.0](../../insiders/changelog#1.0.0) | ||
|
||
This special `pyodide` fence uses [Pyodide](https://pyodide.org), [Ace](https://ace.c9.io/) | ||
and [Highlight.js](https://highlightjs.org/) to render an interactive Python editor. | ||
Everything runs on the client side. The first time Pyodide is loaded by the browser | ||
can be a bit long, but then it will be cached and the next time you load the page | ||
it will be much faster. | ||
|
||
Click the "Run" button in the top-right corner, or hit ++ctrl+enter++ to run the code. | ||
You can install packages with Micropip: | ||
|
||
````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" | ||
```pyodide | ||
import micropip | ||
|
||
print("Installing cowsay...") | ||
await micropip.install("cowsay") | ||
print("done!") | ||
``` | ||
```` | ||
|
||
Then you can import and use the packages you installed: | ||
|
||
````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" | ||
```pyodide | ||
import cowsay | ||
cowsay.cow("Hello World") | ||
``` | ||
```` | ||
|
||
Packages installed with Micropip are cached by the browser as well, | ||
making future installations much faster. | ||
|
||
## Sessions | ||
|
||
Editors with the same session share the same `globals()` dictionary, | ||
so you can reuse variables, classes, imports, etc., from another editor | ||
within the same session. This is why you can import `cowsay` in this editor, | ||
given you actually installed it in the first. Sessions are ephemeral: | ||
everything is reset when reloading the page. This means you cannot persist | ||
sessions across multiple pages. Try refreshing your page | ||
and running the code of the second editor: you should get a ModuleNotFoundError. | ||
|
||
To use other sessions, simply pass the `session="name"` option to the code block: | ||
|
||
````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" | ||
```pyodide session="something" | ||
something = "hello" | ||
``` | ||
```` | ||
|
||
Now lets print it in another editor with the same session: | ||
|
||
````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" | ||
```pyodide session="something" | ||
print(something) | ||
``` | ||
```` | ||
|
||
And in another editor with the default session: | ||
|
||
````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" | ||
```pyodide | ||
print(something) | ||
``` | ||
```` | ||
|
||
## Pre-installing packages | ||
|
||
In your own documentation pages, you might not want to add | ||
`import micropip; await micropip.install("your-package")` | ||
to every editor to show how to use your package. In this case, | ||
you can use the `install` option to pre-install packages. | ||
The option takes a list of comma-separated package distribution names: | ||
|
||
````md exec="1" source="tabbed-right" tabs="Markdown|Rendered" | ||
```pyodide install="griffe,dependenpy" | ||
import griffe | ||
import dependenpy | ||
print("OK!") | ||
``` | ||
```` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
"""Formatter for creating a Pyodide interactive editor.""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, Any | ||
|
||
if TYPE_CHECKING: | ||
from markdown import Markdown | ||
|
||
play_emoji = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 5.14v14l11-7-11-7Z"></path></svg>' | ||
clear_emoji = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.14 3c-.51 0-1.02.2-1.41.59L2.59 14.73c-.78.77-.78 2.04 0 2.83L5.03 20h7.66l8.72-8.73c.79-.77.79-2.04 0-2.83l-4.85-4.85c-.39-.39-.91-.59-1.42-.59M17 18l-2 2h7v-2"></path></svg>' | ||
|
||
template = """ | ||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.16.0/ace.js"></script> | ||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> | ||
<script type="text/javascript" src="https://cdn.jsdelivr.net/pyodide/v0.23.0/full/pyodide.js"></script> | ||
<link title="light" rel="alternate stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.min.css" disabled="disabled"> | ||
<link title="dark" rel="alternate stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css" disabled="disabled"> | ||
<div class="pyodide"> | ||
<div class="pyodide-editor-bar"> | ||
<span class="pyodide-bar-item">Editor (session: %(session)s)</span><span id="%(id_prefix)srun" title="Run: press Ctrl-Enter" class="pyodide-bar-item pyodide-clickable"><span class="twemoji">%(play_emoji)s</span> Run</span> | ||
</div> | ||
<div id="%(id_prefix)seditor" class="pyodide-editor">%(initial_code)s</div> | ||
<div class="pyodide-editor-bar"> | ||
<span class="pyodide-bar-item">Output</span><span id="%(id_prefix)sclear" class="pyodide-bar-item pyodide-clickable"><span class="twemoji">%(clear_emoji)s</span> Clear</span> | ||
</div> | ||
<pre><code id="%(id_prefix)soutput" class="pyodide-output"></code></pre> | ||
</div> | ||
<script> | ||
document.addEventListener('DOMContentLoaded', (event) => { | ||
setupPyodide('%(id_prefix)s', install=%(install)s, themeLight='%(theme_light)s', themeDark='%(theme_dark)s', session='%(session)s'); | ||
}); | ||
</script> | ||
""" | ||
|
||
_counter = 0 | ||
|
||
|
||
def _format_pyodide(code: str, md: Markdown, session: str, extra: dict, **options: Any) -> str: # noqa: ARG001 | ||
global _counter # noqa: PLW0603 | ||
_counter += 1 | ||
install = extra.pop("install", "") | ||
install = install.split(",") if install else [] | ||
theme = extra.pop("theme", "tomorrow,tomorrow_night") | ||
if "," not in theme: | ||
theme = f"{theme},{theme}" | ||
theme_light, theme_dark = theme.split(",") | ||
data = { | ||
"id_prefix": f"exec-{_counter}--", | ||
"initial_code": code, | ||
"install": install, | ||
"theme_light": theme_light.strip(), | ||
"theme_dark": theme_dark.strip(), | ||
"session": session or "default", | ||
"play_emoji": play_emoji, | ||
"clear_emoji": clear_emoji, | ||
} | ||
return template % data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
html[data-theme="light"] { | ||
@import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow.css" | ||
} | ||
|
||
html[data-theme="dark"] { | ||
@import "https://cdn.jsdelivr.net/npm/highlightjs-themes@1.0.0/tomorrow-night-blue.min.css" | ||
} | ||
|
||
|
||
.ace_gutter { | ||
z-index: 1; | ||
} | ||
|
||
.pyodide-editor { | ||
width: 100%; | ||
min-height: 200px; | ||
max-height: 400px; | ||
font-size: .85em; | ||
} | ||
|
||
.pyodide-editor-bar { | ||
color: var(--md-primary-bg-color); | ||
background-color: var(--md-primary-fg-color); | ||
width: 100%; | ||
font: monospace; | ||
font-size: 0.75em; | ||
padding: 2px 0 2px; | ||
} | ||
|
||
.pyodide-bar-item { | ||
padding: 0 18px 0; | ||
display: inline-block; | ||
width: 50%; | ||
} | ||
|
||
.pyodide pre { | ||
margin: 0; | ||
} | ||
|
||
.pyodide-output { | ||
width: 100%; | ||
margin-bottom: -15px; | ||
max-height: 400px | ||
} | ||
|
||
.pyodide-clickable { | ||
cursor: pointer; | ||
text-align: right; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
var _sessions = {}; | ||
|
||
function getSession(name, pyodide) { | ||
if (!(name in _sessions)) { | ||
_sessions[name] = pyodide.globals.get("dict")(); | ||
} | ||
return _sessions[name]; | ||
} | ||
|
||
function writeOutput(element, string) { | ||
element.innerHTML += string + '\n'; | ||
} | ||
|
||
function clearOutput(element) { | ||
element.innerHTML = ''; | ||
} | ||
|
||
async function evaluatePython(pyodide, editor, output, session) { | ||
pyodide.setStdout({ batched: (string) => { writeOutput(output, string); } }); | ||
let result, code = editor.getValue(); | ||
clearOutput(output); | ||
try { | ||
result = await pyodide.runPythonAsync(code, { globals: getSession(session, pyodide) }); | ||
} catch (error) { | ||
writeOutput(output, error); | ||
} | ||
if (result) writeOutput(output, result); | ||
hljs.highlightElement(output); | ||
} | ||
|
||
async function initPyodide() { | ||
let pyodide = await loadPyodide(); | ||
await pyodide.loadPackage("micropip"); | ||
return pyodide; | ||
} | ||
|
||
function getTheme() { | ||
return document.body.getAttribute('data-md-color-scheme'); | ||
} | ||
|
||
function setTheme(editor, currentTheme, light, dark) { | ||
// https://gist.github.com/RyanNutt/cb8d60997d97905f0b2aea6c3b5c8ee0 | ||
if (currentTheme === "default") { | ||
editor.setTheme("ace/theme/" + light); | ||
document.querySelector(`link[title="light"]`).removeAttribute("disabled"); | ||
document.querySelector(`link[title="dark"]`).setAttribute("disabled", "disabled"); | ||
} else if (currentTheme === "slate") { | ||
editor.setTheme("ace/theme/" + dark); | ||
document.querySelector(`link[title="dark"]`).removeAttribute("disabled"); | ||
document.querySelector(`link[title="light"]`).setAttribute("disabled", "disabled"); | ||
} | ||
} | ||
|
||
function updateTheme(editor, light, dark) { | ||
// Create a new MutationObserver instance | ||
const observer = new MutationObserver((mutations) => { | ||
// Loop through the mutations that occurred | ||
mutations.forEach((mutation) => { | ||
// Check if the mutation was a change to the data-md-color-scheme attribute | ||
if (mutation.attributeName === 'data-md-color-scheme') { | ||
// Get the new value of the attribute | ||
const newColorScheme = mutation.target.getAttribute('data-md-color-scheme'); | ||
// Update the editor theme | ||
setTheme(editor, newColorScheme, light, dark); | ||
} | ||
}); | ||
}); | ||
|
||
// Configure the observer to watch for changes to the data-md-color-scheme attribute | ||
observer.observe(document.body, { | ||
attributes: true, | ||
attributeFilter: ['data-md-color-scheme'], | ||
}); | ||
} | ||
|
||
async function setupPyodide(idPrefix, install = null, themeLight = 'tomorrow', themeDark = 'tomorrow_night', session = null) { | ||
const editor = ace.edit(idPrefix + "editor"); | ||
const run = document.getElementById(idPrefix + "run"); | ||
const clear = document.getElementById(idPrefix + "clear"); | ||
const output = document.getElementById(idPrefix + "output"); | ||
|
||
updateTheme(editor, themeLight, themeDark); | ||
|
||
editor.session.setMode("ace/mode/python"); | ||
setTheme(editor, getTheme(), themeLight, themeDark); | ||
|
||
writeOutput(output, "Initializing..."); | ||
let pyodide = await pyodidePromise; | ||
if (install && install.length) { | ||
micropip = pyodide.pyimport("micropip"); | ||
for (const package of install) | ||
await micropip.install(package); | ||
} | ||
clearOutput(output); | ||
run.onclick = () => evaluatePython(pyodide, editor, output, session); | ||
clear.onclick = () => clearOutput(output); | ||
output.parentElement.parentElement.addEventListener("keydown", (event) => { | ||
if (event.ctrlKey && event.key.toLowerCase() === 'enter') { | ||
event.preventDefault(); | ||
run.click(); | ||
} | ||
}); | ||
} | ||
|
||
var pyodidePromise = initPyodide(); |