mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
Merge pull request #2230 from mainsail-org/mir/notebook-with-md
rustpython notebook, add markdown and math support
This commit is contained in:
7 files changed
+321
-50
No files matched your search
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user