diff --git a/wasm/notebook/package.json b/wasm/notebook/package.json new file mode 100644 index 000000000..46c6aa340 --- /dev/null +++ b/wasm/notebook/package.json @@ -0,0 +1,39 @@ +{ + "name": "app", + "version": "1.0.0", + "description": "Bindings to the RustPython library for WebAssembly", + "main": "index.js", + "dependencies": { + "codemirror": "^5.42.0", + "local-echo": "^0.2.0", + "xterm": "^3.8.0" + }, + "devDependencies": { + "@wasm-tool/wasm-pack-plugin": "^1.1.0", + "clean-webpack-plugin": "^3.0.0", + "css-loader": "^3.4.1", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.9.0", + "raw-loader": "^4.0.0", + "serve": "^11.0.2", + "webpack": "^4.16.3", + "webpack-cli": "^3.1.0", + "webpack-dev-server": "^3.1.5" + }, + "scripts": { + "dev": "webpack-dev-server -d", + "build": "webpack", + "dist": "webpack --mode production", + "test": "webpack --mode production && cd ../tests && pipenv run pytest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RustPython/RustPython.git" + }, + "author": "Ryan Liddle", + "license": "MIT", + "bugs": { + "url": "https://github.com/RustPython/RustPython/issues" + }, + "homepage": "https://github.com/RustPython/RustPython#readme" +} diff --git a/wasm/notebook/src/index.ejs b/wasm/notebook/src/index.ejs new file mode 100644 index 000000000..a50890271 --- /dev/null +++ b/wasm/notebook/src/index.ejs @@ -0,0 +1,57 @@ + + + + + + + + 🐍 RustPython Notebook + + + + + + + +
+
+
+ +
+
+ +
+
+ + + +
+ +
+
+ + +
+
+ errors +
+
+
+ Keyboard Shortcuts: +
+
Run code: 'Ctrl-Enter' or 'Cmd-Enter'
+
Decrease Indent: 'Shift-Tab'
+
Toggle comment: 'Ctrl-/' or 'Cmd-/'
+
+
+
+ + + \ No newline at end of file diff --git a/wasm/notebook/src/index.js b/wasm/notebook/src/index.js new file mode 100644 index 000000000..7931331b4 --- /dev/null +++ b/wasm/notebook/src/index.js @@ -0,0 +1,114 @@ +import './style.css'; +import CodeMirror from 'codemirror'; +import 'codemirror/mode/python/python'; +import 'codemirror/addon/comment/comment'; +import 'codemirror/lib/codemirror.css'; + +let rp; + +// UI elements +const consoleElement = document.getElementById('console'); +const errorElement = document.getElementById('error'); +const fetchbtnElement = document.getElementById("fetch-code"); +const urlConainerElement = document.getElementById('url-container'); + +// A dependency graph that contains any wasm must be imported asynchronously. +import('rustpython') + .then(rustpy => { + rp = rustpy; + // so people can play around with it + window.rp = rustpy; + onReady(); + }) + .catch(e => { + console.error('Error importing `rustpython`:', e); + document.getElementById('error').textContent = e; + }); + +// Code Mirror code editor +const editor = CodeMirror.fromTextArea(document.getElementById('code'), { + extraKeys: { + 'Ctrl-Enter': runCodeFromTextarea, + 'Cmd-Enter': runCodeFromTextarea, + 'Shift-Tab': 'indentLess', + 'Ctrl-/': 'toggleComment', + 'Cmd-/': 'toggleComment', + Tab: editor => { + var spaces = Array(editor.getOption('indentUnit') + 1).join(' '); + editor.replaceSelection(spaces); + } + }, + lineNumbers: true, + mode: 'text/x-python', + indentUnit: 4, + autofocus: true +}); + +// Runs the code the the code editor +function runCodeFromTextarea() { + // Clean the console and errors + consoleElement.innerHTML = ''; + errorElement.textContent = ''; + + const code = editor.getValue(); + try { + rp.pyExec(code, { + stdout: output => { + consoleElement.innerHTML += output; + } + }); + } catch (err) { + if (err instanceof WebAssembly.RuntimeError) { + err = window.__RUSTPYTHON_ERROR || err; + } + errorElement.textContent = err; + console.error(err); + } +} + +function onReady() { + // snippets.addEventListener('change', updateSnippet); + document + .getElementById('run-btn') + .addEventListener('click', runCodeFromTextarea); + + // so that the test knows that we're ready + const readyElement = document.createElement('div'); + readyElement.id = 'rp_loaded'; + document.head.appendChild(readyElement); +} + +// when clicking the import code button +// show a UI with a url input + fetch button +// only accepts api.github.com urls (for now) +// add another function to parse a regular url +fetchbtnElement.addEventListener("click", function () { + // https://developer.github.com/v3/repos/contents/#get-repository-content + // Format: + // https://api.github.com/repos/username/reponame/contents/filename.py + let url = document + .getElementById('snippet-url') + .value; + // minimal js fetch code + // needs better error handling + fetch(url) + .then(res => res.json()) + .then(data => { + // The Python code is in data.content + // it is encoded with Base64. Use atob to decode it. + //https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob + var decodedData = atob(data.content); + // set the value of the code editor + editor.setValue(decodedData); + urlConainerElement.classList.add("d-none"); + }).catch(err => { + document + .getElementById("errors") + .innerHTML = "Couldn't fetch code. Make sure the link is public." + }); + +}); + +document.getElementById("snippet-btn").addEventListener("click", function () { + urlConainerElement.classList.remove("d-none"); +}); \ No newline at end of file diff --git a/wasm/notebook/src/style.css b/wasm/notebook/src/style.css new file mode 100644 index 000000000..c0d776a8a --- /dev/null +++ b/wasm/notebook/src/style.css @@ -0,0 +1,110 @@ +.header { + font-family: 'Sen', sans-serif; + ; +} + +.d-flex { + display: flex; +} + +.d-none { + display: none; +} + +.mr-1 { + margin-right: 1rem; +} + +.mt-1 { + margin-top: 1rem; +} + +.text-small { + font-size: 0.75rem; +} + +mark { + background-color: #000; + color: #fff; + font-size: 0.75rem; +} + +.nav-bar { + border-bottom: 2px solid #F74C00; + display: flex; + justify-content: space-between; + position: sticky; + top: 0px; + background-color: #fff; + height: 20px; + z-index: 99; +} + +.split-view { + display: flex; + justify-content: space-between; +} + +.split-view div { + flex-basis: 50%; +} + +.full-height { + height: calc(90vh - 60px); +} + +#console { + padding: 1rem; +} + +.CodeMirror { + height: 90% !important; + border-right: 1px solid gray; +} + +#run-btn, +.secondary-btn { + border: 0px; + color: white; + cursor: pointer; +} + +#run-btn { + background-color: #F74C00; +} + +input[type="url"] { + border: 1px solid black; + border-radius: 0px; + width: 55%; + margin: 5px 0px 5px 0px; + font-size: 0.7rem; + padding: 4px; + font-family: monospace; +} + +.url-container { + padding: 10px; + background-color: white; + font-size: smaller; +} + +.secondary-btn { + background-color: #000; +} + +.secondary-btn.active, +.secondary-btn:hover { + background-color: #F74C00; +} + +#error { + color: tomato; + margin-top: 10px; + font-family: monospace; + white-space: pre; +} + +#code-wrapper { + position: relative; +} \ No newline at end of file diff --git a/wasm/notebook/webpack.config.js b/wasm/notebook/webpack.config.js new file mode 100644 index 000000000..67e2428f6 --- /dev/null +++ b/wasm/notebook/webpack.config.js @@ -0,0 +1,72 @@ +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); +const { CleanWebpackPlugin } = require('clean-webpack-plugin'); + +const path = require('path'); +const fs = require('fs'); + +const interval = setInterval(() => console.log('keepalive'), 1000 * 60 * 5); + +module.exports = (env = {}) => { + const config = { + entry: './src/index.js', + output: { + path: path.join(__dirname, 'dist'), + filename: 'index.js' + }, + mode: 'development', + resolve: { + alias: { + rustpython: path.resolve( + __dirname, + env.rustpythonPkg || '../lib/pkg' + ) + } + }, + module: { + rules: [ + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'] + } + ] + }, + plugins: [ + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'src/index.ejs', + templateParameters: { + snippets: fs + .readdirSync(path.join(__dirname, 'snippets')) + .map(filename => + path.basename(filename, path.extname(filename)) + ), + defaultSnippetName: 'fibonacci', + defaultSnippet: fs.readFileSync( + path.join(__dirname, 'snippets/fibonacci.py') + ) + } + }), + new MiniCssExtractPlugin({ + filename: 'styles.css' + }), + { + apply(compiler) { + compiler.hooks.done.tap('clearInterval', () => { + clearInterval(interval); + }); + } + } + ] + }; + if (!env.noWasmPack) { + config.plugins.push( + new WasmPackPlugin({ + crateDirectory: path.join(__dirname, '../lib') + }) + ); + } + return config; +};