diff --git a/package-lock.json b/package-lock.json index 55b1270..3493796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blog-cells", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "blog-cells", - "version": "0.4.1", + "version": "0.5.0", "dependencies": { "@babel/standalone": "^7.22.13", "@codemirror/lang-javascript": "^6.1.9", @@ -19,7 +19,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@jest/globals": "^29.6.4", + "@codemirror/lang-python": "^6.1.3", "@types/jest": "^29.5.4", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", @@ -642,6 +642,17 @@ "@lezer/javascript": "^1.0.0" } }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "dev": true, + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, "node_modules/@codemirror/language": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.0.tgz", @@ -1454,6 +1465,16 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@lezer/python": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.8.tgz", + "integrity": "sha512-1T/XsmeF57ijrjpC0Zmrf9YeO5mn2zC1XeSNrOnc0KB+6PgxJ5m7kWKt0CnwyS74oHQXbJxUUL+QDQJR26c1Gw==", + "dev": true, + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10601,6 +10622,17 @@ "@lezer/javascript": "^1.0.0" } }, + "@codemirror/lang-python": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.3.tgz", + "integrity": "sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==", + "dev": true, + "requires": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@lezer/python": "^1.1.4" + } + }, "@codemirror/language": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.0.tgz", @@ -11237,6 +11269,16 @@ "@lezer/common": "^1.0.0" } }, + "@lezer/python": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.8.tgz", + "integrity": "sha512-1T/XsmeF57ijrjpC0Zmrf9YeO5mn2zC1XeSNrOnc0KB+6PgxJ5m7kWKt0CnwyS74oHQXbJxUUL+QDQJR26c1Gw==", + "dev": true, + "requires": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index bc53943..5787ddc 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "homepage": "https://rameshvarun.github.io/blog-cells/", "devDependencies": { + "@codemirror/lang-python": "^6.1.3", "@types/jest": "^29.5.4", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7", diff --git a/src/blog-cells.tsx b/src/blog-cells.tsx index d05c7ea..b9b96d0 100644 --- a/src/blog-cells.tsx +++ b/src/blog-cells.tsx @@ -13,13 +13,19 @@ const editors: any[] = []; const events = new EventTarget(); import { JavaScriptKernel } from "./javascript-kernel"; -const kernel = new JavaScriptKernel(); +import { PythonKernel } from "./python-kernel"; +import { Kernel } from "./kernel"; + +const KERNELS: Map = new Map(); +KERNELS.set("javascript", new JavaScriptKernel()); +KERNELS.set("python", new PythonKernel()); class Cell extends React.Component< { code: string; autoRun: boolean; hideable: boolean; + kernel: Kernel; onMount?: () => void; }, any @@ -129,7 +135,7 @@ class Cell extends React.Component< extensions: [ history(), lineNumbers(), - kernel.getSyntaxHighlighter(), + this.props.kernel.getSyntaxHighlighter(), oneDark, keymap.of([...defaultKeymap, ...historyKeymap]), ], @@ -160,7 +166,7 @@ class Cell extends React.Component< setTimeout(() => resolve(), 500); }); - kernel.run( + this.props.kernel.run( code, (line) => { this.setState((state) => { @@ -196,6 +202,10 @@ domLoaded.then(() => { for (const script of scripts) { const code = script.textContent?.trim() || ""; + + const kernelName = script.dataset.kernel || "javascript"; + const kernel = KERNELS.get(kernelName)!; + const autoRun = script.dataset.autorun === "true"; const hidden = script.dataset.hidden === "true"; @@ -208,6 +218,7 @@ domLoaded.then(() => { code={code} autoRun={autoRun} hideable={hidden} + kernel={kernel} onMount={() => { // Remove the script tag once the cell succesfully mounts. script.remove(); diff --git a/src/index.html b/src/index.html index 04634fa..e970931 100644 --- a/src/index.html +++ b/src/index.html @@ -94,6 +94,10 @@

blog-cells demo

console.assert(3 == 4, "3 does not equal 4."); +

You can use other languages like Python.

+ + diff --git a/src/javascript-kernel/index.ts b/src/javascript-kernel/index.ts index 5120968..d3f828a 100644 --- a/src/javascript-kernel/index.ts +++ b/src/javascript-kernel/index.ts @@ -6,14 +6,9 @@ import { javascript } from "@codemirror/lang-javascript"; import { Kernel, OutputLine } from "../kernel"; import { ExecutionRequest, ExecutionResponse } from "./types"; -export class JavaScriptKernel implements Kernel { +export class JavaScriptKernel extends Kernel { worker: Worker = new Worker(URL.createObjectURL(blob)); - requestID: number = 0; - getRequestID() { - return this.requestID++; - } - run(code: string, onOutput: (line: OutputLine) => void, onDone: () => void) { // Generate a unique ID to track this execution request. const requestID = this.getRequestID(); diff --git a/src/kernel.ts b/src/kernel.ts index 75b6506..683f52e 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -1,11 +1,16 @@ import { LanguageSupport } from "@codemirror/language"; export type OutputLine = { - type: string; + type: 'log' | 'error' | 'warn'; line: string; }; -export interface Kernel { - run(code: string, onOutput: (line: OutputLine) => void, onDone: () => void); - getSyntaxHighlighter(): LanguageSupport; +export abstract class Kernel { + requestID: number = 0; + getRequestID() { + return this.requestID++; + } + + abstract run(code: string, onOutput: (line: OutputLine) => void, onDone: () => void); + abstract getSyntaxHighlighter(): LanguageSupport; } diff --git a/src/python-kernel/index.ts b/src/python-kernel/index.ts new file mode 100644 index 0000000..6311257 --- /dev/null +++ b/src/python-kernel/index.ts @@ -0,0 +1,40 @@ +// @ts-ignore - Load worker source code. +import WORKER_SRC from "!raw-loader!ts-loader!./worker.ts"; +const blob = new Blob([WORKER_SRC], { type: "application/javascript" }); + +import { python } from "@codemirror/lang-python"; +import { Kernel, OutputLine } from "../kernel"; + +export class PythonKernel extends Kernel { + worker: Worker = new Worker(URL.createObjectURL(blob)); + + run(code: string, onOutput: (line: OutputLine) => void, onDone: () => void) { + // Generate a unique ID to track this execution request. + const requestID = this.getRequestID(); + + const messageHandler = (e: MessageEvent) => { + if (e.data.requestID != requestID) return; + + if (e.data.kind === "run-code-output") { + onOutput(e.data.output); + } else if (e.data.kind === "run-code-done") { + this.worker.removeEventListener("message", messageHandler); + onDone(); + } + }; + + this.worker.addEventListener("message", messageHandler); + + // Post the code to the worker. + this.worker.postMessage({ + kind: "run-code", + code: code, + requestID: requestID, + }); + } + + + getSyntaxHighlighter() { + return python(); + } +} \ No newline at end of file diff --git a/src/python-kernel/worker.ts b/src/python-kernel/worker.ts new file mode 100644 index 0000000..5388709 --- /dev/null +++ b/src/python-kernel/worker.ts @@ -0,0 +1,52 @@ +importScripts("https://cdn.jsdelivr.net/npm/pyodide@0.23.2/pyodide.min.js"); +declare var loadPyodide; + +let onStdout: ((str) => void) | null = null; + +// Start loading Pyodide asynchronously. +const loadPython = (async () => { + return await loadPyodide({ + stdout: (msg) => { + if (msg === "Python initialization complete") return; + if (onStdout) onStdout(msg); + }, + }); +})(); + +self.onmessage = async (e: MessageEvent) => { + console.log("Worker received message: %o", e); + const requestID = e.data.requestID; + + if (e.data.kind === "run-code") { + // Register stdout callback. + onStdout = (msg) => { + self.postMessage({ + kind: "run-code-output", + requestID: requestID, + output: {type: "log", line: msg}, + }); + }; + + try { + // Wait for Pyodide to load. + const pyodide = await loadPython; + + // Run code in a new namespace. + pyodide.runPython(e.data.code); + } catch (error) { + self.postMessage({ + kind: "run-code-output", + requestID: requestID, + output: {type: "error", line: error.toString()}, + }); + } finally { + // Unregister stdout callback. + onStdout = null; + + self.postMessage({ + kind: "run-code-done", + requestID: requestID, + }); + } + } +}; \ No newline at end of file