Skip to content

Commit 6b8aea0

Browse files
committed
notebook example inspired by the now abandoned alpha.iodide.io, wasm + rustpython
1 parent b569ccf commit 6b8aea0

File tree

5 files changed

+392
-0
lines changed

5 files changed

+392
-0
lines changed

wasm/notebook/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "app",
3+
"version": "1.0.0",
4+
"description": "Bindings to the RustPython library for WebAssembly",
5+
"main": "index.js",
6+
"dependencies": {
7+
"codemirror": "^5.42.0",
8+
"local-echo": "^0.2.0",
9+
"xterm": "^3.8.0"
10+
},
11+
"devDependencies": {
12+
"@wasm-tool/wasm-pack-plugin": "^1.1.0",
13+
"clean-webpack-plugin": "^3.0.0",
14+
"css-loader": "^3.4.1",
15+
"html-webpack-plugin": "^3.2.0",
16+
"mini-css-extract-plugin": "^0.9.0",
17+
"raw-loader": "^4.0.0",
18+
"serve": "^11.0.2",
19+
"webpack": "^4.16.3",
20+
"webpack-cli": "^3.1.0",
21+
"webpack-dev-server": "^3.1.5"
22+
},
23+
"scripts": {
24+
"dev": "webpack-dev-server -d",
25+
"build": "webpack",
26+
"dist": "webpack --mode production",
27+
"test": "webpack --mode production && cd ../tests && pipenv run pytest"
28+
},
29+
"repository": {
30+
"type": "git",
31+
"url": "git+https://github.com/RustPython/RustPython.git"
32+
},
33+
"author": "Ryan Liddle",
34+
"license": "MIT",
35+
"bugs": {
36+
"url": "https://github.com/RustPython/RustPython/issues"
37+
},
38+
"homepage": "https://github.com/RustPython/RustPython#readme"
39+
}

wasm/notebook/src/index.ejs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<link defer async href="https://fonts.googleapis.com/css?family=Fira+Sans|Sen:800&display=swap" rel="stylesheet">
8+
<title>🐍 RustPython Notebook</title>
9+
</head>
10+
11+
<body>
12+
<!-- navigation bar -->
13+
<div class="nav-bar">
14+
<div>
15+
<button id="run-btn">run notebook &#9658; </button>
16+
<button id="snippet-btn" class="secondary-btn mr-1">import code</button>
17+
</div>
18+
<div class="header">RustPython 🐍 😱 🤘</div>
19+
<div><a href="https://github.com/RustPython/">github</a></div>
20+
</div>
21+
22+
<!-- user interface elements for importing code -->
23+
<div id="url-container" class="d-none">
24+
<div><label for="url">public url: (uses github api file url for now)</label></div>
25+
<div>
26+
<input type="url" name="url" id="snippet-url">
27+
</div>
28+
<div>
29+
<button id="fetch-code" class="secondary-btn">fetch</button>
30+
</div>
31+
</div>
32+
33+
<!-- code editor and output display -->
34+
<!-- split view -->
35+
<div class="split-view full-height">
36+
<textarea id="code"></textarea>
37+
<div id="console"></div>
38+
</div>
39+
40+
<!-- errors and keyboard shortcuts -->
41+
<div class="split-view">
42+
<div>
43+
<mark>errors</mark>
44+
<div id="error"></div>
45+
</div>
46+
<div class="text-small mt-1">
47+
<mark>Keyboard Shortcuts:</mark>
48+
<div>
49+
<div>Run code: 'Ctrl-Enter' or 'Cmd-Enter'</div>
50+
<div>Decrease Indent: 'Shift-Tab' </div>
51+
<div>Toggle comment: 'Ctrl-/' or 'Cmd-/'</div>
52+
</div>
53+
</div>
54+
</div>
55+
</body>
56+
57+
</html>

wasm/notebook/src/index.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import './style.css';
2+
import CodeMirror from 'codemirror';
3+
import 'codemirror/mode/python/python';
4+
import 'codemirror/addon/comment/comment';
5+
import 'codemirror/lib/codemirror.css';
6+
7+
let rp;
8+
9+
// UI elements
10+
const consoleElement = document.getElementById('console');
11+
const errorElement = document.getElementById('error');
12+
const fetchbtnElement = document.getElementById("fetch-code");
13+
const urlConainerElement = document.getElementById('url-container');
14+
15+
// A dependency graph that contains any wasm must be imported asynchronously.
16+
import('rustpython')
17+
.then(rustpy => {
18+
rp = rustpy;
19+
// so people can play around with it
20+
window.rp = rustpy;
21+
onReady();
22+
})
23+
.catch(e => {
24+
console.error('Error importing `rustpython`:', e);
25+
document.getElementById('error').textContent = e;
26+
});
27+
28+
// Code Mirror code editor
29+
const editor = CodeMirror.fromTextArea(document.getElementById('code'), {
30+
extraKeys: {
31+
'Ctrl-Enter': runCodeFromTextarea,
32+
'Cmd-Enter': runCodeFromTextarea,
33+
'Shift-Tab': 'indentLess',
34+
'Ctrl-/': 'toggleComment',
35+
'Cmd-/': 'toggleComment',
36+
Tab: editor => {
37+
var spaces = Array(editor.getOption('indentUnit') + 1).join(' ');
38+
editor.replaceSelection(spaces);
39+
}
40+
},
41+
lineNumbers: true,
42+
mode: 'text/x-python',
43+
indentUnit: 4,
44+
autofocus: true
45+
});
46+
47+
// Runs the code the the code editor
48+
function runCodeFromTextarea() {
49+
// Clean the console and errors
50+
consoleElement.innerHTML = '';
51+
errorElement.textContent = '';
52+
53+
const code = editor.getValue();
54+
try {
55+
rp.pyExec(code, {
56+
stdout: output => {
57+
consoleElement.innerHTML += output;
58+
}
59+
});
60+
} catch (err) {
61+
if (err instanceof WebAssembly.RuntimeError) {
62+
err = window.__RUSTPYTHON_ERROR || err;
63+
}
64+
errorElement.textContent = err;
65+
console.error(err);
66+
}
67+
}
68+
69+
function onReady() {
70+
// snippets.addEventListener('change', updateSnippet);
71+
document
72+
.getElementById('run-btn')
73+
.addEventListener('click', runCodeFromTextarea);
74+
75+
// so that the test knows that we're ready
76+
const readyElement = document.createElement('div');
77+
readyElement.id = 'rp_loaded';
78+
document.head.appendChild(readyElement);
79+
}
80+
81+
// when clicking the import code button
82+
// show a UI with a url input + fetch button
83+
// only accepts api.github.com urls (for now)
84+
// add another function to parse a regular url
85+
fetchbtnElement.addEventListener("click", function () {
86+
// https://developer.github.com/v3/repos/contents/#get-repository-content
87+
// Format:
88+
// https://api.github.com/repos/username/reponame/contents/filename.py
89+
let url = document
90+
.getElementById('snippet-url')
91+
.value;
92+
// minimal js fetch code
93+
// needs better error handling
94+
fetch(url)
95+
.then(res => res.json())
96+
.then(data => {
97+
// The Python code is in data.content
98+
// it is encoded with Base64. Use atob to decode it.
99+
//https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob
100+
var decodedData = atob(data.content);
101+
// set the value of the code editor
102+
editor.setValue(decodedData);
103+
urlConainerElement.classList.add("d-none");
104+
}).catch(err => {
105+
document
106+
.getElementById("errors")
107+
.innerHTML = "Couldn't fetch code. Make sure the link is public."
108+
});
109+
110+
});
111+
112+
document.getElementById("snippet-btn").addEventListener("click", function () {
113+
urlConainerElement.classList.remove("d-none");
114+
});

wasm/notebook/src/style.css

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
.header {
2+
font-family: 'Sen', sans-serif;
3+
;
4+
}
5+
6+
.d-flex {
7+
display: flex;
8+
}
9+
10+
.d-none {
11+
display: none;
12+
}
13+
14+
.mr-1 {
15+
margin-right: 1rem;
16+
}
17+
18+
.mt-1 {
19+
margin-top: 1rem;
20+
}
21+
22+
.text-small {
23+
font-size: 0.75rem;
24+
}
25+
26+
mark {
27+
background-color: #000;
28+
color: #fff;
29+
font-size: 0.75rem;
30+
}
31+
32+
.nav-bar {
33+
border-bottom: 2px solid #F74C00;
34+
display: flex;
35+
justify-content: space-between;
36+
position: sticky;
37+
top: 0px;
38+
background-color: #fff;
39+
height: 20px;
40+
z-index: 99;
41+
}
42+
43+
.split-view {
44+
display: flex;
45+
justify-content: space-between;
46+
}
47+
48+
.split-view div {
49+
flex-basis: 50%;
50+
}
51+
52+
.full-height {
53+
height: calc(90vh - 60px);
54+
}
55+
56+
#console {
57+
padding: 1rem;
58+
}
59+
60+
.CodeMirror {
61+
height: 90% !important;
62+
border-right: 1px solid gray;
63+
}
64+
65+
#run-btn,
66+
.secondary-btn {
67+
border: 0px;
68+
color: white;
69+
cursor: pointer;
70+
}
71+
72+
#run-btn {
73+
background-color: #F74C00;
74+
}
75+
76+
input[type="url"] {
77+
border: 1px solid black;
78+
border-radius: 0px;
79+
width: 55%;
80+
margin: 5px 0px 5px 0px;
81+
font-size: 0.7rem;
82+
padding: 4px;
83+
font-family: monospace;
84+
}
85+
86+
.url-container {
87+
padding: 10px;
88+
background-color: white;
89+
font-size: smaller;
90+
}
91+
92+
.secondary-btn {
93+
background-color: #000;
94+
}
95+
96+
.secondary-btn.active,
97+
.secondary-btn:hover {
98+
background-color: #F74C00;
99+
}
100+
101+
#error {
102+
color: tomato;
103+
margin-top: 10px;
104+
font-family: monospace;
105+
white-space: pre;
106+
}
107+
108+
#code-wrapper {
109+
position: relative;
110+
}

wasm/notebook/webpack.config.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
const HtmlWebpackPlugin = require('html-webpack-plugin');
2+
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3+
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
4+
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5+
6+
const path = require('path');
7+
const fs = require('fs');
8+
9+
const interval = setInterval(() => console.log('keepalive'), 1000 * 60 * 5);
10+
11+
module.exports = (env = {}) => {
12+
const config = {
13+
entry: './src/index.js',
14+
output: {
15+
path: path.join(__dirname, 'dist'),
16+
filename: 'index.js'
17+
},
18+
mode: 'development',
19+
resolve: {
20+
alias: {
21+
rustpython: path.resolve(
22+
__dirname,
23+
env.rustpythonPkg || '../lib/pkg'
24+
)
25+
}
26+
},
27+
module: {
28+
rules: [
29+
{
30+
test: /\.css$/,
31+
use: [MiniCssExtractPlugin.loader, 'css-loader']
32+
}
33+
]
34+
},
35+
plugins: [
36+
new CleanWebpackPlugin(),
37+
new HtmlWebpackPlugin({
38+
filename: 'index.html',
39+
template: 'src/index.ejs',
40+
templateParameters: {
41+
snippets: fs
42+
.readdirSync(path.join(__dirname, 'snippets'))
43+
.map(filename =>
44+
path.basename(filename, path.extname(filename))
45+
),
46+
defaultSnippetName: 'fibonacci',
47+
defaultSnippet: fs.readFileSync(
48+
path.join(__dirname, 'snippets/fibonacci.py')
49+
)
50+
}
51+
}),
52+
new MiniCssExtractPlugin({
53+
filename: 'styles.css'
54+
}),
55+
{
56+
apply(compiler) {
57+
compiler.hooks.done.tap('clearInterval', () => {
58+
clearInterval(interval);
59+
});
60+
}
61+
}
62+
]
63+
};
64+
if (!env.noWasmPack) {
65+
config.plugins.push(
66+
new WasmPackPlugin({
67+
crateDirectory: path.join(__dirname, '../lib')
68+
})
69+
);
70+
}
71+
return config;
72+
};

0 commit comments

Comments
 (0)