From a76c6420904ce236e8d54fe9aee125ceaf46c4ee Mon Sep 17 00:00:00 2001 From: Mireille Raad Date: Mon, 21 Sep 2020 22:03:50 -0400 Subject: [PATCH 1/2] add markdown and math support --- wasm/notebook/package.json | 3 + wasm/notebook/src/index.ejs | 7 +- wasm/notebook/src/index.js | 140 +++++++++++++++++++++++++------- wasm/notebook/src/parser.js | 87 ++++++++++++++++++++ wasm/notebook/src/sample.txt | 68 ++++++++++++++++ wasm/notebook/src/style.css | 39 +++++++-- wasm/notebook/webpack.config.js | 18 +++- 7 files changed, 321 insertions(+), 41 deletions(-) create mode 100644 wasm/notebook/src/parser.js create mode 100644 wasm/notebook/src/sample.txt diff --git a/wasm/notebook/package.json b/wasm/notebook/package.json index 46c6aa340..b5c592d05 100644 --- a/wasm/notebook/package.json +++ b/wasm/notebook/package.json @@ -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": { diff --git a/wasm/notebook/src/index.ejs b/wasm/notebook/src/index.ejs index c524722d9..e46243c33 100644 --- a/wasm/notebook/src/index.ejs +++ b/wasm/notebook/src/index.ejs @@ -4,8 +4,7 @@ - - 🐍 RustPython Notebook + @@ -32,7 +31,7 @@
-
+
loading python in your browser...
@@ -41,7 +40,7 @@
Error(s):
-
+
Keyboard Shortcuts:
Run code: 'Ctrl-Enter' or 'Cmd-Enter'
diff --git a/wasm/notebook/src/index.js b/wasm/notebook/src/index.js index e90ae3422..88dc138ea 100644 --- a/wasm/notebook/src/index.js +++ b/wasm/notebook/src/index.js @@ -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"); }); \ No newline at end of file diff --git a/wasm/notebook/src/parser.js b/wasm/notebook/src/parser.js new file mode 100644 index 000000000..7a3111a8b --- /dev/null +++ b/wasm/notebook/src/parser.js @@ -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; + } + \ No newline at end of file diff --git a/wasm/notebook/src/sample.txt b/wasm/notebook/src/sample.txt new file mode 100644 index 000000000..4009c0f1c --- /dev/null +++ b/wasm/notebook/src/sample.txt @@ -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). + +
+ +## 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.") diff --git a/wasm/notebook/src/style.css b/wasm/notebook/src/style.css index efb4f68f2..1f46aed51 100644 --- a/wasm/notebook/src/style.css +++ b/wasm/notebook/src/style.css @@ -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; } \ No newline at end of file diff --git a/wasm/notebook/webpack.config.js b/wasm/notebook/webpack.config.js index 97d81aeee..89ebd9129 100644 --- a/wasm/notebook/webpack.config.js +++ b/wasm/notebook/webpack.config.js @@ -29,7 +29,23 @@ module.exports = (env = {}) => { { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] - } + }, + { + + test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, + use: [ + + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'fonts/' + } + } + + ], + + } ] }, plugins: [ From 737b1c064d2f3701d9a009db5345fd25fcedcfcd Mon Sep 17 00:00:00 2001 From: Mireille Raad Date: Sat, 26 Sep 2020 11:00:07 -0400 Subject: [PATCH 2/2] include woff, woff2 and ttf fonts only for katex/math fonts --- wasm/notebook/webpack.config.js | 41 +++++++++++++-------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/wasm/notebook/webpack.config.js b/wasm/notebook/webpack.config.js index 89ebd9129..2983cb76c 100644 --- a/wasm/notebook/webpack.config.js +++ b/wasm/notebook/webpack.config.js @@ -31,22 +31,13 @@ module.exports = (env = {}) => { use: [MiniCssExtractPlugin.loader, 'css-loader'] }, { - - test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, - use: [ - - { - loader: 'file-loader', - options: { - name: '[name].[ext]', - outputPath: 'fonts/' - } - } - - ], - - } - ] + test: /\.(woff(2)?|ttf)$/, + use: { + loader:"file-loader", + options: { name: "fonts/[name].[ext]" } + }, + } + ] }, plugins: [ new CleanWebpackPlugin(), @@ -54,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({