Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions resource/decker/support/plugins/thebe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Thebe Plugin

For more information on Thebe, see <https://github.com/executablebooks/thebe>.

This **plugin needs configuration in `decker.yaml`**, otherwise it will not start.

## Usage in slides

In your slides simply use, e.g.,
~~~markdown
```{ .no-highlight executable="true" language="python" }
# This is a runnable python cell.
print("Hello World")
```
~~~

Thebe looks for cells with `data-executable="true"`.
If no executable cells are found, the plugin will also not launch Thebe.

This is intentional as one deck may have executable cells, while the other does not, but the might use the same template.

The plugin will also *not* launch if there is no configuration in `decker.yaml`.

The recommended use is with a *local jupyter*, for students it may be nicert to use *binder* or *jupyter-lite*.

## Setup and Configuration

In your template, enable the `thebe` plugin by adding the CSS
```html
<link rel="stylesheet" href="$decker-support-dir$/plugins/thebe/thebe.css">
```
respectively the JavaScript
```js
import thebePlugin from './$decker-support-dir$/plugins/thebe/thebe.js';
```
and enabling the `thebePlugin`.

Add the following to `decker.yaml`:
```yaml
thebe:
bootstrap: true
requestKernel: true
useBinder: true
useJupyterLite: false
serverSettings:
appendToken: true
baseUrl: http://localhost:8888
token: "token-for-your-jupyter"
wsUrl: ws://localhost:8888
mathjaxUrl: false
mountActivateWidget: false
mountStatusWidget: false
mountRunAllButton: true
mountRunAllButton: false
mountRestartButton: false
mountRestartallButton: false
codeMirrorConfig:
lineNumbers: false
scrollbarStyle: native
```

For configuration settings, see <https://thebe.readthedocs.io/en/stable/config_reference.html>.

- `useBinder`: set to false to use jupyterlite or a local jupyter\
Note: binder containers have a 10 minute timeout, a local jupyter is preferrable
- `useJupyterLite`: set to true to use jupyterlite\
this requires loading additional javascript (thebe-lite)
- `serverSettings`: for connecting to a local jupyter
- `mathjaxUrl`: set to false, as we already loaded a newer MathJaX than used by Jupyter
- `mount*`: choose the widgets you like
- `codeMirrorConfig`: configure the editor component\
see <https://codemirror.net/5/doc/manual.html>

## CSS

To set the fonts, customize:

```css
.reveal {
--jp-code-font-family: "Fira Code", monospace;
--jp-content-font-family: "Fira Sans", sans-serif;
}
```

Jupyter adds a *huge* amount of CSS, unfortunately, including its own code highlighting that you may want to customize.

Other themes need additional CSS. We try to map the default theme to the Decker colors.

## Local Jupyter

The most controllable way is to use a local Jupyter lab instead of Binder.

Set `useBinder: false`

Start a local jupyter with the desired token:
```bash
jupyter lab --IdentityProvider.token=token-for-your-jupyter "--ServerApp.allow_origin=*"
```

This makes it easiest to pre-install packages, but it is harder to *share* the slides with students – they need to launch a similar jupyter server on their local machine to run your code.

## Binder

In this mode, a container will be launched on BinderHub. This can take a bit to start up, and the binder containers have inactivity timeouts of as little as 10 minutes.

Right now, **there is no automatic kernel status indicator**. So you cannot easily see if you are connected and/or retry.
<https://github.com/executablebooks/thebe/issues/697>

By default, Binder relies on mybinder.org to provide free containers, operated for example by [OVH](https://www.ovh.com/),
[GESIS](https://notebooks.gesis.org/) or [Curvenote](https://curvenote.com/). But sometimes these services are unavailable or fail, and there might be privacy issues involved with using hosted services.

But this will allow others to also run these cells on your own.

## thebe-lite

Set `useBinder: false` and `useJupyterLite: true`.

Thebe has a mode of operation using Pyodide in the client, without installation. This comes with some limitations, though:

- worse startup, as an entire Python is loaded into the browser
- some packages work, others do not: pytorch for example
- some function such as urlretrieve are limited by the browsers guardrails to prevent cross-site attacks

This requires additional configuration, in paticular if you want a custom installation.

The benefit is that if you share your slide, the users can also execute the cells within their own browser.

## Updating

To update the thebe libraries, first `npm install thebe thebe-lite`.

Right now, I have been using the 0.9.0-rc release candidates. Configuration names have changed across versions, so you may need to make some updates...

Put the assets into `decker/resource/decker/support/vendor/thebe`,
figuring out the right assets to copy is a bit of a mess.

- `*thebe-lite.min.js` from `thebe-lite/dist/lib/`, if you intend to use this (optional)
- `index.js` and `thebe.css` from `thebe/lib/`

The configuration of jupyter-lite is *separate* from Thebe.

Add this to your `decker.yaml` to configure JupyterLite:
```yaml
jupyter:
litePluginSettings:
'@jupyterlite/pyodide-kernel-extension:kernel':
pipliteUrls: ["https://unpkg.com/@jupyterlite/pyodide-kernel@0.0.7/pypi/all.json"]
pipliteWheelUrl: "https://unpkg.com/@jupyterlite/pyodide-kernel@0.0.7/pypi/piplite-0.0.7-py3-none-any.whl"
disabledExtensions: [jupyterlab-kernel-spy, jupyterlab-tour]
terminalsAvailable: false
enableMemoryStorage: true
settingsStorageDrivers: [memoryStorageDriver]
```

Note: memory storage means the state is lost when reloading the slides (likely the desired behavior).

Or use your own copy of pyodide if you do not trust unpkg.com.

## Known issues

1. ~~Input cursor placement is broken, depending on the browser window size.~~

This issue has been largely resolved, by applying a reverse scale to certain layers of the editor.
Once Thebe switches to CodeMirror 6, this should also no longer be an issue.

2. Output cells may overflow.

3. When starting the kernel fails (e.g., Binder not available), only an error is logged to the console, but there is no user feedback. Needs to be fixed in Thebe, <https://github.com/executablebooks/thebe/issues/735>

## TODO

To make this easier usable by non-hackers:

- [x] fix the editor component
- [x] dark mode 😎
- [x] add a kernel status widget via JavaScript to every cell
- [ ] menu configuration to choose local jupyter or binder!
- [ ] figure out how to set max-height to output + scrollable
- [ ] make an example with prerendered output
- [ ] make thebe-lite easier to configure for self-hosted use (GDPR)
- [ ] add invisible, but executed, Python cells for context setup

81 changes: 81 additions & 0 deletions resource/decker/support/plugins/thebe/thebe.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.reveal {
--jp-content-presentation-font-size1: 100%;
--jp-content-font-size1: 100%;
--jp-code-presentation-font-size: 100%;
--jp-code-font-size: 100%;
--jp-ui-font-size1: 100%;
/* TODO: get via some variable from the main style sheet */
--jp-code-font-family: "Fira Code", "Fira Mono", menlo, consolas, monospace;
--jp-content-font-family: "Fira Sans", menlo, consolas, sans-serif;
--jp-rendermime-error-background: var(--accent0-bg);
--jp-content-link-color: var(--accent5-fg);
}

.reveal button { font-size: 100%; } /* fix, should not be necessary? */

.reveal .CodeMirror :is(h1, p, div, ul, ol, table):last-child { margin-bottom: 0 !important; }

.reveal .CodeMirror { border: none; height: auto; }
.reveal .CodeMirror-scroll { padding-bottom: .25ex; scrollbar-width: none; max-width: 100%; }
.reveal .CodeMirror-lines { padding-top: 0px; }

.reveal .CodeMirror { background-color: var(--background-color); color: var(--foreground-color); }
.reveal .CodeMirror-cursor { border-left: .2ex solid var(--accent4-fg); }
.reveal .CodeMirror-focused .CodeMirror-selected { background-color: var(--accent4-bg); }
.reveal .CodeMirror-selected { background-color: var(--accent4-bbg); }
.reveal .cm-s-default { color: var(--foreground-color); }
.reveal .cm-s-default .cm-keyword { color: var(--accent0-fg); }
.reveal .cm-s-default .cm-builtin { color: var(--accent3-fg); }
.reveal .cm-s-default .cm-def { color: var(--accent4-fg); }
.reveal .cm-s-default .cm-variable{ color: var(--accent5-fg); }
.reveal .cm-s-default .cm-property{ color: var(--accent5-fg); }
.reveal .cm-s-default .cm-number { color: var(--accent6-fg); }
.reveal .cm-s-default .cm-string { color: var(--accent7-fg); }
.reveal .cm-s-default .cm-link { color: var(--accent7-fg); }
.reveal .cm-s-default .cm-bracket { color: var(--shade3); }
.reveal .cm-s-default .cm-comment { color: var(--shade3); }
.reveal .jp-RenderedText pre { color: var(--foreground-color); }

.reveal .thebe-cell { font-size: 100%; }

.reveal .thebe-controls {
text-align: left;
font-size: 100%;
}

.reveal .thebe-controls .thebe-button {
display: inline-block;
position: inherit;
margin-right: .5ex;
font-size: 100%;
border-radius: .2ex;
border-width: 0;
background-color: var(--shade1);
box-shadow: .1ex .1ex .3ex var(--shade6);
color: var(--shade4); /* not available */
}
.dark .reveal .thebe-controls .thebe-button {
box-shadow: 0 0 .3ex var(--accent3-bg);
}
.reveal .thebe-controls .thebe-button.thebe-start-button,
.reveal .thebe-controls.session-attached .thebe-button,
.reveal .thebe-controls.session-idle .thebe-button,
.reveal .thebe-controls.session-ready .thebe-button {
background-color: var(--shade2);
color: var(--foreground-color);
}
.reveal .thebe-status { /* custom status indicator */
margin: 0 .5ex;
}

.reveal .thebe-busy { display: inline-block; margin-right: .5ex; vertical-align: middle; } /* stabilize layout */
.reveal .thebe-busy-spinner {
height: 28px;
width: 28px;
border-color: var(--shade2);
border-width: 4px;
border-top-color: var(--color-info);
border-top-width: 5px;
border-bottom-width: 3px;
}

114 changes: 114 additions & 0 deletions resource/decker/support/plugins/thebe/thebe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @author Erich Schubert
*/
const Plugin = {
id: "thebe",
init: (deck) => {
if (!Decker.meta["thebe"]) return; // not configured
return new Promise(function (resolve) {
if (document.querySelectorAll(Decker.meta.thebe["selector"] || '[data-executable]').length > 0 && window.thebe === undefined) {
console.log("Loading Thebe.", document.querySelectorAll('[data-executable]').length, "cells.");
// Thebe-lite needs to be loaded before core, if enabled
if (Decker.meta.thebe["useJupyterLite"]) {
// Jupyterlite configuration for thebe-lite
console.log("Loading thebe-lite");
let conf = document.createElement("script");
conf.setAttribute("type", "application/json");
conf.setAttribute("id", "jupyter-config-data");
conf.text=JSON.stringify(Decker.meta["jupyter"]);
document.body.append(conf);
// Load thebe-lite
let script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("src", Decker.meta.supportPath + "/vendor/thebe/thebe-lite.min.js");
document.body.append(script);
}
// Load Thebe
let script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("src", Decker.meta.supportPath + "/vendor/thebe/index.js");
document.body.append(script);
// Thebe CSS
let style = document.createElement("link");
style.setAttribute("rel", "stylesheet");
style.setAttribute("href", Decker.meta.supportPath + "/vendor/thebe/thebe.css");
document.body.append(style);
// Workaround for CodeMirror 5 scaling issues:
let scaleCodeMirror = function(cm) {
let scale = deck.getScale();
cm.CodeMirror.display.sizer.style.transform="scale("+(1./scale)+")";
cm.CodeMirror.display.sizer.style.transformOrigin="0 0 0";
cm.CodeMirror.display.sizer.style.fontSize=(100*scale)+"%";
let minh = parseFloat(cm.CodeMirror.display.sizer.style.minHeight);
if (minh > 0) {
cm.CodeMirror.display.sizer.style.minHeight=""; //
cm.CodeMirror.display.sizer.style.height=Math.ceil(minh / scale)+"px"; // not when NaN
}
cm.CodeMirror.display.heightForcer.style.height="0"; // Disable, breaks short fields
cm.CodeMirror.display.heightForcer.style.top="0"; // Disable, breaks short fields
};
// Bootstrap thebe on finished loading
script.addEventListener('load', () => {
if (document.querySelectorAll('div.thebe-cell').length > 0) return; // Already started
thebe.bootstrap(Decker.meta["thebe"]);
// Add custom buttons and status widget
// Add a status indicator to each input box
document.querySelectorAll('.thebe-controls').forEach((e) => {
if (Decker.meta["thebe"]["requestKernel"]===false) {
let start_button = document.createElement("button");
start_button.classList.add('thebe-button', 'thebe-start-button')
start_button.innerText = "start";
start_button.setAttribute("title", "Start the kernel.");
start_button.onclick = async() => {
if (!window.thebe.session) {
await window.thebe.server.ready;
const session = await window.thebe.server.startNewSession(window.thebe.notebook.rendermime);
if (session != null) {
window.thebe.notebook.attachSession(session);
window.thebe.session = session;
}
}
};
e.prepend(start_button);
}
let kernel_status = document.createElement("div");
kernel_status.setAttribute("class", "thebe-status");
e.append(kernel_status);
});
// Status indicator
thebe.on("status", function(evt, data) {
// To show/hide buttons that are not applicable
document.querySelectorAll('.thebe-controls').forEach((e) => {
e.classList.forEach((c) => { if (c.startsWith("session-")) { e.classList.remove(c) }} );
e.classList.add("session-"+data.status);
});
// Update status indicator
document.querySelectorAll('.thebe-status').forEach((e) => {
e.innerText = data["status"];
if (data["status"] == "server-ready") e.innerText = "no session started";
});
if (data["status"] == "attached") {
document.querySelectorAll('.thebe-start-button').forEach((e) => { e.style.display = "none"; });
}
// Apply CodeMirror scaling hack
if (data.status == "launching") {
// In case a cell gets the DOM recreated:
document.querySelectorAll('.CodeMirror').forEach((x)=>{
scaleCodeMirror(x);
x.CodeMirror.on("update", (event) => scaleCodeMirror(x));
});
}
});
// Reveal resize events
deck.on('resize', (event) => document.querySelectorAll('.CodeMirror').forEach(scaleCodeMirror));
});
} else {
console.log("Not enabling thebe: no data-executable fragments found");
}
resolve();
});
},
};

export default Plugin;

Loading