Merge pull request #2230 from mainsail-org/mir/notebook-with-md

rustpython notebook, add markdown and math support
This commit is contained in:
Noah authored and GitHub committed 2020-09-29 11:30:01 -05:00
commit dd41295390
7 files changed
+321 -50

No files matched your search

+3
View File
@@ -5,7 +5,10 @@
"main": "index.js",
"dependencies": {
"codemirror": "^5.42.0",
"file-loader": "^6.1.0",
"katex": "^0.12.0",
"local-echo": "^0.2.0",
"marked": "^1.1.1",
"xterm": "^3.8.0"
},
"devDependencies": {
+3 -4
View File
@@ -4,8 +4,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link defer async href="https://fonts.googleapis.com/css?family=Fira+Sans|Sen:800&display=swap" rel="stylesheet">
<title>🐍 RustPython Notebook</title>
<link defer async href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans&family=Sen:wght@800&display=swap" rel="stylesheet">
</head>
<body>
@@ -32,7 +31,7 @@
<!-- split view -->
<div class="split-view full-height">
<textarea id="code"></textarea>
<div id="console"></div>
<div id="rp-notebook">loading python in your browser...</div>
</div>
<!-- errors and keyboard shortcuts -->
@@ -41,7 +40,7 @@
<div class="header">Error(s):</div>
<div id="error"></div>
</div>
<div class="mt-1">
<div class="mt-1 gray">
<div class="header">Keyboard Shortcuts:</div>
<div>
<div>Run code: 'Ctrl-Enter' or 'Cmd-Enter'</div>
+112 -28
View File
@@ -1,16 +1,32 @@
import './style.css';
// Code Mirror (https://codemirror.net/)
// https://github.com/codemirror/codemirror
import CodeMirror from 'codemirror';
import 'codemirror/mode/python/python';
import 'codemirror/addon/comment/comment';
import 'codemirror/lib/codemirror.css';
// MarkedJs (https://marked.js.org/)
// Renders Markdown
// https://github.com/markedjs/marked
import marked from 'marked'
// KaTex (https://katex.org/)
// Renders Math
// https://github.com/KaTeX/KaTeX
import katex from "katex";
import "katex/dist/katex.min.css";
// Parses the code and splits it to chunks
// uses %% keyword for separators
// copied from iodide project
// https://github.com/iodide-project/iodide/blob/master/src/editor/iomd-tools/iomd-parser.js
import { iomdParser } from "./parser";
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');
const notebook = document.getElementById('rp-notebook');
const error = document.getElementById('error');
// A dependency graph that contains any wasm must be imported asynchronously.
import('rustpython')
@@ -25,11 +41,11 @@ import('rustpython')
document.getElementById('error').textContent = e;
});
// Code Mirror code editor
// Code Editor
const editor = CodeMirror.fromTextArea(document.getElementById('code'), {
extraKeys: {
'Ctrl-Enter': runCodeFromTextarea,
'Cmd-Enter': runCodeFromTextarea,
'Ctrl-Enter': parseCodeFromEditor,
'Cmd-Enter': parseCodeFromEditor,
'Shift-Tab': 'indentLess',
'Ctrl-/': 'toggleComment',
'Cmd-/': 'toggleComment',
@@ -41,61 +57,127 @@ const editor = CodeMirror.fromTextArea(document.getElementById('code'), {
lineNumbers: true,
mode: 'text/x-python',
indentUnit: 4,
autofocus: true
autofocus: true,
lineWrapping: true
});
// Runs the code the the code editor
function runCodeFromTextarea() {
// Parses what is the code editor
// either runs python or renders math or markdown
function parseCodeFromEditor() {
// Clean the console and errors
consoleElement.innerHTML = '';
errorElement.textContent = '';
notebook.innerHTML = '';
error.textContent = '';
const code = editor.getValue();
// gets the code from code editor
let code = editor.getValue();
/*
Split code into chunks.
Uses %%keyword or %% keyword as separator
Implemented %%py %%md %%math for python, markdown and math.
Returned object has:
- chunkContent, chunkType, chunkId,
- evalFlags, startLine, endLine
*/
let parsed_code = iomdParser(code);
parsed_code.forEach(chunk => {
// For each type of chunk, do somthing
// so far have py for python, md for markdown and math for math ;p
let content = chunk.chunkContent;
switch (chunk.chunkType) {
case 'py':
runPython(content);
break;
case 'md':
notebook.innerHTML += renderMarkdown(content);
break;
case 'math':
notebook.innerHTML += renderMath(content, true);
break;
case 'math-inline':
notebook.innerHTML += renderMath(content, false )
break;
default:
// by default assume this is python code
// so users don't have to type py manually
runPython(code);
}
});
}
// Run Python code
function runPython(code) {
try {
rp.pyExec(code, {
stdout: output => {
consoleElement.innerHTML += output;
notebook.innerHTML += output;
}
});
} catch (err) {
if (err instanceof WebAssembly.RuntimeError) {
err = window.__RUSTPYTHON_ERROR || err;
}
errorElement.textContent = err;
console.error(err);
error.textContent = err;
}
}
function onReady() {
document
.getElementById('run-btn')
.addEventListener('click', runCodeFromTextarea);
// Render Markdown with imported marked compiler
function renderMarkdown(md) {
// TODO: add error handling and output sanitization
let settings = {
headerIds: true,
breaks: true
}
// so that the test knows that we're ready
return marked(md , settings );
}
// Render Math with Katex
function renderMath(math , display_mode) {
// TODO: definetly add error handling.
return katex.renderToString(math, {
displayMode: display_mode,
"macros": { "\\f": "#1f(#2)" }
});
}
function onReady() {
/* By default the notebook has the keyword "loading"
once python and doc is ready:
create an empty div and set the id to 'rp_loaded'
so that the test knows that we're ready */
const readyElement = document.createElement('div');
readyElement.id = 'rp_loaded';
document.head.appendChild(readyElement);
// set the notebook to empty
notebook.innerHTML = "";
}
// on click, parse the code
document.getElementById('run-btn').addEventListener('click', parseCodeFromEditor);
// import button
// show a url input + fetch button
// takes a url where there is raw code
fetchbtnElement.addEventListener("click", function () {
document.getElementById("fetch-code").addEventListener("click", function () {
let url = document
.getElementById('snippet-url')
.value;
// minimal js fetch code
// needs better error handling
// TODO: better error handling
fetch(url)
.then( response => {
.then(response => {
if (!response.ok) { throw response }
return response.text()
return response.text()
})
.then(text => {
// set the value of the code editor
editor.setValue(text);
// hide the ui
urlConainerElement.classList.add("d-none");
document.getElementById('url-container').classList.add("d-none");
}).catch(err => {
// show the error as is for troubleshooting.
document
@@ -105,6 +187,8 @@ fetchbtnElement.addEventListener("click", function () {
});
// UI for the fetch button
// after clicking fetch, hide the UI
document.getElementById("snippet-btn").addEventListener("click", function () {
urlConainerElement.classList.remove("d-none");
document.getElementById('url-container').classList.remove("d-none");
});
+87
View File
@@ -0,0 +1,87 @@
// The parser is from Mozilla's iodide project:
// https://github.com/iodide-project/iodide/blob/master/src/editor/iomd-tools/iomd-parser.js
function hashCode(str) {
// this is an implementation of java's hashcode method
// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
let hash = 0;
let chr;
if (str.length !== 0) {
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr; // eslint-disable-line
hash |= 0; // eslint-disable-line
}
}
return hash.toString();
}
export function iomdParser(fullIomd) {
const iomdLines = fullIomd.split("\n");
const chunks = [];
let currentChunkLines = [];
let currentEvalType = "";
let evalFlags = [];
let currentChunkStartLine = 1;
const newChunkId = str => {
const hash = hashCode(str);
let hashNum = "0";
for (const chunk of chunks) {
const [prevHash, prevHashNum] = chunk.chunkId.split("_");
if (hash === prevHash) {
hashNum = (parseInt(prevHashNum, 10) + 1).toString();
}
}
return `${hash}_${hashNum}`;
};
const pushChunk = endLine => {
const chunkContent = currentChunkLines.join("\n");
chunks.push({
chunkContent,
chunkType: currentEvalType,
chunkId: newChunkId(chunkContent),
evalFlags,
startLine: currentChunkStartLine,
endLine
});
};
for (const [i, line] of iomdLines.entries()) {
const lineNum = i + 1; // uses 1-based indexing
if (line.slice(0, 2) === "%%") {
// if line start with '%%', a new chunk has started
// push the current chunk (unless it's on line 1), then reset
if (lineNum !== 1) {
// DON'T push a chunk if we're only on line 1
pushChunk(lineNum - 1);
}
// reset the currentChunk state
currentChunkStartLine = lineNum;
currentChunkLines = [];
evalFlags = [];
// find the first char on this line that isn't '%'
let lineColNum = 0;
while (line[lineColNum] === "%") {
lineColNum += 1;
}
const chunkFlags = line
.slice(lineColNum)
.split(/[ \t]+/)
.filter(s => s !== "");
if (chunkFlags.length > 0) {
// if there is a captured group, update the eval type
[currentEvalType, ...evalFlags] = chunkFlags;
}
} else {
// if there is no match, then the line is not a
// chunk delimiter line, so add the line to the currentChunk
currentChunkLines.push(line);
}
}
// this is what's left over in the final chunk
pushChunk(iomdLines.length);
return chunks;
}
+68
View File
@@ -0,0 +1,68 @@
%%py
print("Javascript called. It wants its DOM elements back.")
%%md
## RustPython Notebook with Markdown and Math
Look! I can write markdown in the notebook. It messes up code highligting, but at least it works.
1. i can make a list
1. i can haz many lines and numbers
1. I can paste emojis 🐍 😱 🤘
In the notebook, you can write markdown, math and python. use %% then:
- md for markdown
- py for python
- math for large, centered math blocks
- math-inline for an inline math block
I **can** bold things and _italicize_ them. You got the gist, this is standard markdown syntax that you can [google](google.com).
<br>
## Math
Supported TeX functions is [here](https://katex.org/docs/supported.html). For example:
%%math
\tau(u_f)*l_f
\newline
H(t) \xrightarrow{write} \Big[A(t+1),\ H(t+1)\Big]
\newline
\newline
% \f is defined as #1f(#2) using the macro
\f\relax{x} = \int_{-\infty}^\infty
\f\hat\xi,e^{2 \pi i \xi x}
\,d\xi
\newline
\begin{Bmatrix}
a & b \\
c & d
\end{Bmatrix}
\dbinom{n}{k}
\sqrt[3]{x}
%%py
print("RustPython called back and said people need to have")
# You can run regular python code here.
# Here is a random number generator.
import random
rnd = random.randint(1,5)
print("nice " * rnd + "things.")
+31 -8
View File
@@ -1,9 +1,10 @@
body {
font-family: 'Fira Sans', sans-serif;
font-family: 'IBM Plex Sans', sans-serif;
}
.header {
font-family: 'Sen', sans-serif;;
font-family: 'Sen', sans-serif;
;
}
.d-flex {
@@ -48,7 +49,7 @@ mark {
width: 300px;
}
.item-center {
.item-center {
flex-grow: 1;
text-align: center;
}
@@ -70,10 +71,28 @@ mark {
height: calc(90vh - 60px);
}
#console {
padding: 1rem;
font-family: monospace;
font-size: 1.1rem;
#rp-notebook {
font-size: 1rem;
padding: 10px;
overflow-y: scroll;
}
h1,
h2,
h3,
h4,
h5,
h6,
p {
margin: 0 0 10px;
}
#rp-notebook p {
margin: 0;
}
.mspace.newline {
height: 20px;
}
.CodeMirror {
@@ -110,7 +129,7 @@ input[type="url"] {
background-color: white;
}
.btn-secondary{
.btn-secondary {
background-color: #000;
}
@@ -123,4 +142,8 @@ input[type="url"] {
#code-wrapper {
position: relative;
}
.gray {
color: gray;
}
+17 -10
View File
@@ -29,8 +29,15 @@ module.exports = (env = {}) => {
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(woff(2)?|ttf)$/,
use: {
loader:"file-loader",
options: { name: "fonts/[name].[ext]" }
},
}
]
]
},
plugins: [
new CleanWebpackPlugin(),
@@ -38,15 +45,15 @@ module.exports = (env = {}) => {
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')
// )
// 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({