diff --git a/package-lock.json b/package-lock.json index 87e4c2097..ebb8d0290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6179,6 +6179,11 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "codemirror": { + "version": "5.65.12", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.12.tgz", + "integrity": "sha512-z2jlHBocElRnPYysN2HAuhXbO3DNB0bcSKmNz3hcWR2Js2Dkhc1bEOxG93Z3DeUrnm+qx56XOY5wQmbP5KY0sw==" + }, "codepage": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", @@ -6247,6 +6252,11 @@ "delayed-stream": "~1.0.0" } }, + "comlink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz", + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==" + }, "comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", @@ -18790,6 +18800,11 @@ } } }, + "react-codemirror2": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-7.2.1.tgz", + "integrity": "sha512-t7YFmz1AXdlImgHXA9Ja0T6AWuopilub24jRaQdPVbzUJVNKIYuy3uCFZYa7CE5S3UW6SrSa5nAqVQvtzRF9gw==" + }, "react-copy-to-clipboard": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz", diff --git a/package.json b/package.json index 8e4437217..1368f0c75 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "@testing-library/user-event": "^7.2.1", "@tippyjs/react": "^4.2.6", "axios": "^0.26.1", + "codemirror": "^5.59.1", + "comlink": "^4.4.1", "connected-react-router": "^6.8.0", "cors": "^2.8.5", "country-list-with-dial-code-and-flag": "^3.0.2", @@ -63,6 +65,7 @@ "react": "^16.13.1", "react-avatar-edit": "^1.2.0", "react-avatar-editor": "^13.0.0", + "react-codemirror2": "^7.2.1", "react-copy-to-clipboard": "^5.0.4", "react-country-flag": "^3.0.2", "react-csv": "^2.2.2", diff --git a/public/js/react-py/workers/python-console-worker.js b/public/js/react-py/workers/python-console-worker.js new file mode 100644 index 000000000..13570e39c --- /dev/null +++ b/public/js/react-py/workers/python-console-worker.js @@ -0,0 +1,558 @@ +// generated from https://raw.githubusercontent.com/elilambnz/react-py/0b024c44c9b61e6c47fbf852ab90cf15fb80ccdd/src/workers/python-console-worker.ts +// npm install +// and https://github.com/GoogleChromeLabs/comlink/blob/cc560378b10d9bcc2a6324eeb6e0f412065a4045/src/comlink.ts +// npm install then npm run build => used dist/esm/comlink.js +// import { expose } from 'comlink'; + +// import { expose } from 'comlink'; +importScripts('https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js'); + +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const proxyMarker = Symbol("Comlink.proxy"); +const createEndpoint = Symbol("Comlink.endpoint"); +const releaseProxy = Symbol("Comlink.releaseProxy"); +const finalizer = Symbol("Comlink.finalizer"); +const throwMarker = Symbol("Comlink.thrown"); +const isObject = (val) => (typeof val === "object" && val !== null) || typeof val === "function"; +/** + * Internal transfer handle to handle objects marked to proxy. + */ +const proxyTransferHandler = { + canHandle: (val) => isObject(val) && val[proxyMarker], + serialize(obj) { + const { port1, port2 } = new MessageChannel(); + expose(obj, port1); + return [port2, [port2]]; + }, + deserialize(port) { + port.start(); + return wrap(port); + }, +}; +/** + * Internal transfer handler to handle thrown exceptions. + */ +const throwTransferHandler = { + canHandle: (value) => isObject(value) && throwMarker in value, + serialize({ value }) { + let serialized; + if (value instanceof Error) { + serialized = { + isError: true, + value: { + message: value.message, + name: value.name, + stack: value.stack, + }, + }; + } + else { + serialized = { isError: false, value }; + } + return [serialized, []]; + }, + deserialize(serialized) { + if (serialized.isError) { + throw Object.assign(new Error(serialized.value.message), serialized.value); + } + throw serialized.value; + }, +}; +/** + * Allows customizing the serialization of certain values. + */ +const transferHandlers = new Map([ + ["proxy", proxyTransferHandler], + ["throw", throwTransferHandler], +]); +function isAllowedOrigin(allowedOrigins, origin) { + for (const allowedOrigin of allowedOrigins) { + if (origin === allowedOrigin || allowedOrigin === "*") { + return true; + } + if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) { + return true; + } + } + return false; +} +function expose(obj, ep = globalThis, allowedOrigins = ["*"]) { + ep.addEventListener("message", function callback(ev) { + if (!ev || !ev.data) { + return; + } + if (!isAllowedOrigin(allowedOrigins, ev.origin)) { + console.warn(`Invalid origin '${ev.origin}' for comlink proxy`); + return; + } + const { id, type, path } = Object.assign({ path: [] }, ev.data); + const argumentList = (ev.data.argumentList || []).map(fromWireValue); + let returnValue; + try { + const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj); + const rawValue = path.reduce((obj, prop) => obj[prop], obj); + switch (type) { + case "GET" /* MessageType.GET */: + { + returnValue = rawValue; + } + break; + case "SET" /* MessageType.SET */: + { + parent[path.slice(-1)[0]] = fromWireValue(ev.data.value); + returnValue = true; + } + break; + case "APPLY" /* MessageType.APPLY */: + { + returnValue = rawValue.apply(parent, argumentList); + } + break; + case "CONSTRUCT" /* MessageType.CONSTRUCT */: + { + const value = new rawValue(...argumentList); + returnValue = proxy(value); + } + break; + case "ENDPOINT" /* MessageType.ENDPOINT */: + { + const { port1, port2 } = new MessageChannel(); + expose(obj, port2); + returnValue = transfer(port1, [port1]); + } + break; + case "RELEASE" /* MessageType.RELEASE */: + { + returnValue = undefined; + } + break; + default: + return; + } + } + catch (value) { + returnValue = { value, [throwMarker]: 0 }; + } + Promise.resolve(returnValue) + .catch((value) => { + return { value, [throwMarker]: 0 }; + }) + .then((returnValue) => { + const [wireValue, transferables] = toWireValue(returnValue); + ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables); + if (type === "RELEASE" /* MessageType.RELEASE */) { + // detach and deactive after sending release response above. + ep.removeEventListener("message", callback); + closeEndPoint(ep); + if (finalizer in obj && typeof obj[finalizer] === "function") { + obj[finalizer](); + } + } + }) + .catch((error) => { + // Send Serialization Error To Caller + const [wireValue, transferables] = toWireValue({ + value: new TypeError("Unserializable return value"), + [throwMarker]: 0, + }); + ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables); + }); + }); + if (ep.start) { + ep.start(); + } +} +function isMessagePort(endpoint) { + return endpoint.constructor.name === "MessagePort"; +} +function closeEndPoint(endpoint) { + if (isMessagePort(endpoint)) + endpoint.close(); +} +function wrap(ep, target) { + return createProxy(ep, [], target); +} +function throwIfProxyReleased(isReleased) { + if (isReleased) { + throw new Error("Proxy has been released and is not useable"); + } +} +function releaseEndpoint(ep) { + return requestResponseMessage(ep, { + type: "RELEASE" /* MessageType.RELEASE */, + }).then(() => { + closeEndPoint(ep); + }); +} +const proxyCounter = new WeakMap(); +const proxyFinalizers = "FinalizationRegistry" in globalThis && + new FinalizationRegistry((ep) => { + const newCount = (proxyCounter.get(ep) || 0) - 1; + proxyCounter.set(ep, newCount); + if (newCount === 0) { + releaseEndpoint(ep); + } + }); +function registerProxy(proxy, ep) { + const newCount = (proxyCounter.get(ep) || 0) + 1; + proxyCounter.set(ep, newCount); + if (proxyFinalizers) { + proxyFinalizers.register(proxy, ep, proxy); + } +} +function unregisterProxy(proxy) { + if (proxyFinalizers) { + proxyFinalizers.unregister(proxy); + } +} +function createProxy(ep, path = [], target = function () { }) { + let isProxyReleased = false; + const proxy = new Proxy(target, { + get(_target, prop) { + throwIfProxyReleased(isProxyReleased); + if (prop === releaseProxy) { + return () => { + unregisterProxy(proxy); + releaseEndpoint(ep); + isProxyReleased = true; + }; + } + if (prop === "then") { + if (path.length === 0) { + return { then: () => proxy }; + } + const r = requestResponseMessage(ep, { + type: "GET" /* MessageType.GET */, + path: path.map((p) => p.toString()), + }).then(fromWireValue); + return r.then.bind(r); + } + return createProxy(ep, [...path, prop]); + }, + set(_target, prop, rawValue) { + throwIfProxyReleased(isProxyReleased); + // FIXME: ES6 Proxy Handler `set` methods are supposed to return a + // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯ + const [value, transferables] = toWireValue(rawValue); + return requestResponseMessage(ep, { + type: "SET" /* MessageType.SET */, + path: [...path, prop].map((p) => p.toString()), + value, + }, transferables).then(fromWireValue); + }, + apply(_target, _thisArg, rawArgumentList) { + throwIfProxyReleased(isProxyReleased); + const last = path[path.length - 1]; + if (last === createEndpoint) { + return requestResponseMessage(ep, { + type: "ENDPOINT" /* MessageType.ENDPOINT */, + }).then(fromWireValue); + } + // We just pretend that `bind()` didn’t happen. + if (last === "bind") { + return createProxy(ep, path.slice(0, -1)); + } + const [argumentList, transferables] = processArguments(rawArgumentList); + return requestResponseMessage(ep, { + type: "APPLY" /* MessageType.APPLY */, + path: path.map((p) => p.toString()), + argumentList, + }, transferables).then(fromWireValue); + }, + construct(_target, rawArgumentList) { + throwIfProxyReleased(isProxyReleased); + const [argumentList, transferables] = processArguments(rawArgumentList); + return requestResponseMessage(ep, { + type: "CONSTRUCT" /* MessageType.CONSTRUCT */, + path: path.map((p) => p.toString()), + argumentList, + }, transferables).then(fromWireValue); + }, + }); + registerProxy(proxy, ep); + return proxy; +} +function myFlat(arr) { + return Array.prototype.concat.apply([], arr); +} +function processArguments(argumentList) { + const processed = argumentList.map(toWireValue); + return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))]; +} +const transferCache = new WeakMap(); +function transfer(obj, transfers) { + transferCache.set(obj, transfers); + return obj; +} +function proxy(obj) { + return Object.assign(obj, { [proxyMarker]: true }); +} +function windowEndpoint(w, context = globalThis, targetOrigin = "*") { + return { + postMessage: (msg, transferables) => w.postMessage(msg, targetOrigin, transferables), + addEventListener: context.addEventListener.bind(context), + removeEventListener: context.removeEventListener.bind(context), + }; +} +function toWireValue(value) { + for (const [name, handler] of transferHandlers) { + if (handler.canHandle(value)) { + const [serializedValue, transferables] = handler.serialize(value); + return [ + { + type: "HANDLER" /* WireValueType.HANDLER */, + name, + value: serializedValue, + }, + transferables, + ]; + } + } + return [ + { + type: "RAW" /* WireValueType.RAW */, + value, + }, + transferCache.get(value) || [], + ]; +} +function fromWireValue(value) { + switch (value.type) { + case "HANDLER" /* WireValueType.HANDLER */: + return transferHandlers.get(value.name).deserialize(value.value); + case "RAW" /* WireValueType.RAW */: + return value.value; + } +} +function requestResponseMessage(ep, msg, transfers) { + return new Promise((resolve) => { + const id = generateUUID(); + ep.addEventListener("message", function l(ev) { + if (!ev.data || !ev.data.id || ev.data.id !== id) { + return; + } + ep.removeEventListener("message", l); + resolve(ev.data); + }); + if (ep.start) { + ep.start(); + } + ep.postMessage(Object.assign({ id }, msg), transfers); + }); +} +function generateUUID() { + return new Array(4) + .fill(0) + .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)) + .join("-"); +} + +// Begin react-py code + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __values = (this && this.__values) || function(o) { + var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + } + }; + throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +// importScripts('https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js'); +// Monkey patch console.log to prevent the script from outputting logs +// eslint-disable-next-line @typescript-eslint/no-empty-function +console.log = function () { }; +// import { expose } from 'comlink'; +var initConsoleCode = "\nimport sys\nfrom pyodide.ffi import to_js\nfrom pyodide.console import PyodideConsole, repr_shorten, BANNER\nimport __main__\nBANNER = \"Welcome to the Pyodide terminal emulator \uD83D\uDC0D\\n\" + BANNER\npyconsole = PyodideConsole(__main__.__dict__)\nimport builtins\nasync def await_fut(fut):\n res = await fut\n if res is not None:\n builtins._ = res\n return to_js([res], depth=1)\ndef clear_console():\n pyconsole.buffer = []\n"; +var pythonConsole; +var python = { + init: function (stdout, onLoad, packages) { + return __awaiter(this, void 0, void 0, function () { + var _a, micropip, version, namespace, reprShorten, banner, awaitFut, pyconsole, clearConsole; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = self; + return [4 /*yield*/, self.loadPyodide({})]; + case 1: + _a.pyodide = _b.sent(); + if (!(packages[0].length > 0)) return [3 /*break*/, 3]; + return [4 /*yield*/, self.pyodide.loadPackage(packages[0])]; + case 2: + _b.sent(); + _b.label = 3; + case 3: + if (!(packages[1].length > 0)) return [3 /*break*/, 6]; + return [4 /*yield*/, self.pyodide.loadPackage(['micropip'])]; + case 4: + _b.sent(); + micropip = self.pyodide.pyimport('micropip'); + return [4 /*yield*/, micropip.install(packages[1])]; + case 5: + _b.sent(); + _b.label = 6; + case 6: + version = self.pyodide.version; + namespace = self.pyodide.globals.get('dict')(); + return [4 /*yield*/, self.pyodide.runPythonAsync(initConsoleCode, { globals: namespace })]; + case 7: + _b.sent(); + reprShorten = namespace.get('repr_shorten'); + banner = namespace.get('BANNER'); + awaitFut = namespace.get('await_fut'); + pyconsole = namespace.get('pyconsole'); + clearConsole = namespace.get('clear_console'); + namespace.destroy(); + // eslint-disable-next-line camelcase + pyconsole.stdout_callback = stdout; + pythonConsole = { + reprShorten: reprShorten, + awaitFut: awaitFut, + pyconsole: pyconsole, + clearConsole: clearConsole + }; + onLoad({ version: version, banner: banner }); + return [2 /*return*/]; + } + }); + }); + }, + run: function (code) { + return __awaiter(this, void 0, void 0, function () { + var state, _a, _b, line, fut, wrapped, _c, value, error_1, message, e_1_1; + var e_1, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + if (!pythonConsole) { + throw new Error('Console has not been initialised'); + } + if (code === undefined) { + throw new Error('No code to push'); + } + _e.label = 1; + case 1: + _e.trys.push([1, 9, 10, 11]); + _a = __values(code.split('\n')), _b = _a.next(); + _e.label = 2; + case 2: + if (!!_b.done) return [3 /*break*/, 8]; + line = _b.value; + fut = pythonConsole.pyconsole.push(line); + state = fut.syntax_check; + wrapped = pythonConsole.awaitFut(fut); + _e.label = 3; + case 3: + _e.trys.push([3, 5, 6, 7]); + return [4 /*yield*/, wrapped]; + case 4: + _c = __read.apply(void 0, [_e.sent(), 1]), value = _c[0]; + if (self.pyodide.isPyProxy(value)) { + value.destroy(); + } + return [3 /*break*/, 7]; + case 5: + error_1 = _e.sent(); + if (error_1.constructor.name === 'PythonError') { + message = fut.formatted_error || error_1.message; + return [2 /*return*/, { state: state, error: message.trimEnd() }]; + } + else { + throw error_1; + } + return [3 /*break*/, 7]; + case 6: + fut.destroy(); + wrapped.destroy(); + return [7 /*endfinally*/]; + case 7: + _b = _a.next(); + return [3 /*break*/, 2]; + case 8: return [3 /*break*/, 11]; + case 9: + e_1_1 = _e.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 11]; + case 10: + try { + if (_b && !_b.done && (_d = _a.return)) _d.call(_a); + } + finally { if (e_1) throw e_1.error; } + return [7 /*endfinally*/]; + case 11: return [2 /*return*/, { state: state }]; + } + }); + }); + }, + readFile: function (name) { + return self.pyodide.FS.readFile(name, { encoding: 'utf8' }); + }, + writeFile: function (name, data) { + return self.pyodide.FS.writeFile(name, data, { encoding: 'utf8' }); + }, + mkdir: function (name) { + self.pyodide.FS.mkdir(name); + }, + rmdir: function (name) { + self.pyodide.FS.rmdir(name); + } +}; +expose(python); diff --git a/public/js/react-py/workers/python-worker.js b/public/js/react-py/workers/python-worker.js new file mode 100644 index 000000000..1b49150e4 --- /dev/null +++ b/public/js/react-py/workers/python-worker.js @@ -0,0 +1,461 @@ +// generated from +// https://raw.githubusercontent.com/elilambnz/react-py/0b024c44c9b61e6c47fbf852ab90cf15fb80ccdd/src/workers/python-worker.ts +// npm install +// and +// https://github.com/GoogleChromeLabs/comlink/blob/cc560378b10d9bcc2a6324eeb6e0f412065a4045/src/comlink.ts +// npm install then npm run build => used dist/esm/comlink.js +// import { expose } from 'comlink'; +importScripts('https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js'); + + +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const proxyMarker = Symbol("Comlink.proxy"); +const createEndpoint = Symbol("Comlink.endpoint"); +const releaseProxy = Symbol("Comlink.releaseProxy"); +const finalizer = Symbol("Comlink.finalizer"); +const throwMarker = Symbol("Comlink.thrown"); +const isObject = (val) => (typeof val === "object" && val !== null) || typeof val === "function"; +/** + * Internal transfer handle to handle objects marked to proxy. + */ +const proxyTransferHandler = { + canHandle: (val) => isObject(val) && val[proxyMarker], + serialize(obj) { + const { port1, port2 } = new MessageChannel(); + expose(obj, port1); + return [port2, [port2]]; + }, + deserialize(port) { + port.start(); + return wrap(port); + }, +}; +/** + * Internal transfer handler to handle thrown exceptions. + */ +const throwTransferHandler = { + canHandle: (value) => isObject(value) && throwMarker in value, + serialize({ value }) { + let serialized; + if (value instanceof Error) { + serialized = { + isError: true, + value: { + message: value.message, + name: value.name, + stack: value.stack, + }, + }; + } + else { + serialized = { isError: false, value }; + } + return [serialized, []]; + }, + deserialize(serialized) { + if (serialized.isError) { + throw Object.assign(new Error(serialized.value.message), serialized.value); + } + throw serialized.value; + }, +}; +/** + * Allows customizing the serialization of certain values. + */ +const transferHandlers = new Map([ + ["proxy", proxyTransferHandler], + ["throw", throwTransferHandler], +]); +function isAllowedOrigin(allowedOrigins, origin) { + for (const allowedOrigin of allowedOrigins) { + if (origin === allowedOrigin || allowedOrigin === "*") { + return true; + } + if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) { + return true; + } + } + return false; +} +function expose(obj, ep = globalThis, allowedOrigins = ["*"]) { + ep.addEventListener("message", function callback(ev) { + if (!ev || !ev.data) { + return; + } + if (!isAllowedOrigin(allowedOrigins, ev.origin)) { + console.warn(`Invalid origin '${ev.origin}' for comlink proxy`); + return; + } + const { id, type, path } = Object.assign({ path: [] }, ev.data); + const argumentList = (ev.data.argumentList || []).map(fromWireValue); + let returnValue; + try { + const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj); + const rawValue = path.reduce((obj, prop) => obj[prop], obj); + switch (type) { + case "GET" /* MessageType.GET */: + { + returnValue = rawValue; + } + break; + case "SET" /* MessageType.SET */: + { + parent[path.slice(-1)[0]] = fromWireValue(ev.data.value); + returnValue = true; + } + break; + case "APPLY" /* MessageType.APPLY */: + { + returnValue = rawValue.apply(parent, argumentList); + } + break; + case "CONSTRUCT" /* MessageType.CONSTRUCT */: + { + const value = new rawValue(...argumentList); + returnValue = proxy(value); + } + break; + case "ENDPOINT" /* MessageType.ENDPOINT */: + { + const { port1, port2 } = new MessageChannel(); + expose(obj, port2); + returnValue = transfer(port1, [port1]); + } + break; + case "RELEASE" /* MessageType.RELEASE */: + { + returnValue = undefined; + } + break; + default: + return; + } + } + catch (value) { + returnValue = { value, [throwMarker]: 0 }; + } + Promise.resolve(returnValue) + .catch((value) => { + return { value, [throwMarker]: 0 }; + }) + .then((returnValue) => { + const [wireValue, transferables] = toWireValue(returnValue); + ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables); + if (type === "RELEASE" /* MessageType.RELEASE */) { + // detach and deactive after sending release response above. + ep.removeEventListener("message", callback); + closeEndPoint(ep); + if (finalizer in obj && typeof obj[finalizer] === "function") { + obj[finalizer](); + } + } + }) + .catch((error) => { + // Send Serialization Error To Caller + const [wireValue, transferables] = toWireValue({ + value: new TypeError("Unserializable return value"), + [throwMarker]: 0, + }); + ep.postMessage(Object.assign(Object.assign({}, wireValue), { id }), transferables); + }); + }); + if (ep.start) { + ep.start(); + } +} +function isMessagePort(endpoint) { + return endpoint.constructor.name === "MessagePort"; +} +function closeEndPoint(endpoint) { + if (isMessagePort(endpoint)) + endpoint.close(); +} +function wrap(ep, target) { + return createProxy(ep, [], target); +} +function throwIfProxyReleased(isReleased) { + if (isReleased) { + throw new Error("Proxy has been released and is not useable"); + } +} +function releaseEndpoint(ep) { + return requestResponseMessage(ep, { + type: "RELEASE" /* MessageType.RELEASE */, + }).then(() => { + closeEndPoint(ep); + }); +} +const proxyCounter = new WeakMap(); +const proxyFinalizers = "FinalizationRegistry" in globalThis && + new FinalizationRegistry((ep) => { + const newCount = (proxyCounter.get(ep) || 0) - 1; + proxyCounter.set(ep, newCount); + if (newCount === 0) { + releaseEndpoint(ep); + } + }); +function registerProxy(proxy, ep) { + const newCount = (proxyCounter.get(ep) || 0) + 1; + proxyCounter.set(ep, newCount); + if (proxyFinalizers) { + proxyFinalizers.register(proxy, ep, proxy); + } +} +function unregisterProxy(proxy) { + if (proxyFinalizers) { + proxyFinalizers.unregister(proxy); + } +} +function createProxy(ep, path = [], target = function () { }) { + let isProxyReleased = false; + const proxy = new Proxy(target, { + get(_target, prop) { + throwIfProxyReleased(isProxyReleased); + if (prop === releaseProxy) { + return () => { + unregisterProxy(proxy); + releaseEndpoint(ep); + isProxyReleased = true; + }; + } + if (prop === "then") { + if (path.length === 0) { + return { then: () => proxy }; + } + const r = requestResponseMessage(ep, { + type: "GET" /* MessageType.GET */, + path: path.map((p) => p.toString()), + }).then(fromWireValue); + return r.then.bind(r); + } + return createProxy(ep, [...path, prop]); + }, + set(_target, prop, rawValue) { + throwIfProxyReleased(isProxyReleased); + // FIXME: ES6 Proxy Handler `set` methods are supposed to return a + // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯ + const [value, transferables] = toWireValue(rawValue); + return requestResponseMessage(ep, { + type: "SET" /* MessageType.SET */, + path: [...path, prop].map((p) => p.toString()), + value, + }, transferables).then(fromWireValue); + }, + apply(_target, _thisArg, rawArgumentList) { + throwIfProxyReleased(isProxyReleased); + const last = path[path.length - 1]; + if (last === createEndpoint) { + return requestResponseMessage(ep, { + type: "ENDPOINT" /* MessageType.ENDPOINT */, + }).then(fromWireValue); + } + // We just pretend that `bind()` didn’t happen. + if (last === "bind") { + return createProxy(ep, path.slice(0, -1)); + } + const [argumentList, transferables] = processArguments(rawArgumentList); + return requestResponseMessage(ep, { + type: "APPLY" /* MessageType.APPLY */, + path: path.map((p) => p.toString()), + argumentList, + }, transferables).then(fromWireValue); + }, + construct(_target, rawArgumentList) { + throwIfProxyReleased(isProxyReleased); + const [argumentList, transferables] = processArguments(rawArgumentList); + return requestResponseMessage(ep, { + type: "CONSTRUCT" /* MessageType.CONSTRUCT */, + path: path.map((p) => p.toString()), + argumentList, + }, transferables).then(fromWireValue); + }, + }); + registerProxy(proxy, ep); + return proxy; +} +function myFlat(arr) { + return Array.prototype.concat.apply([], arr); +} +function processArguments(argumentList) { + const processed = argumentList.map(toWireValue); + return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))]; +} +const transferCache = new WeakMap(); +function transfer(obj, transfers) { + transferCache.set(obj, transfers); + return obj; +} +function proxy(obj) { + return Object.assign(obj, { [proxyMarker]: true }); +} +function windowEndpoint(w, context = globalThis, targetOrigin = "*") { + return { + postMessage: (msg, transferables) => w.postMessage(msg, targetOrigin, transferables), + addEventListener: context.addEventListener.bind(context), + removeEventListener: context.removeEventListener.bind(context), + }; +} +function toWireValue(value) { + for (const [name, handler] of transferHandlers) { + if (handler.canHandle(value)) { + const [serializedValue, transferables] = handler.serialize(value); + return [ + { + type: "HANDLER" /* WireValueType.HANDLER */, + name, + value: serializedValue, + }, + transferables, + ]; + } + } + return [ + { + type: "RAW" /* WireValueType.RAW */, + value, + }, + transferCache.get(value) || [], + ]; +} +function fromWireValue(value) { + switch (value.type) { + case "HANDLER" /* WireValueType.HANDLER */: + return transferHandlers.get(value.name).deserialize(value.value); + case "RAW" /* WireValueType.RAW */: + return value.value; + } +} +function requestResponseMessage(ep, msg, transfers) { + return new Promise((resolve) => { + const id = generateUUID(); + ep.addEventListener("message", function l(ev) { + if (!ev.data || !ev.data.id || ev.data.id !== id) { + return; + } + ep.removeEventListener("message", l); + resolve(ev.data); + }); + if (ep.start) { + ep.start(); + } + ep.postMessage(Object.assign({ id }, msg), transfers); + }); +} +function generateUUID() { + return new Array(4) + .fill(0) + .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16)) + .join("-"); +} + +// Begin react-py code + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; + +// importScripts('https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js'); +// Monkey patch console.log to prevent the script from outputting logs +// eslint-disable-next-line @typescript-eslint/no-empty-function +console.log = function () { }; +// import { expose } from 'comlink'; +var python = { + init: function (stdout, onLoad, packages) { + return __awaiter(this, void 0, void 0, function () { + var _a, micropip, version; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = self; + return [4 /*yield*/, self.loadPyodide({ + stdout: stdout + })]; + case 1: + _a.pyodide = _b.sent(); + if (!(packages[0].length > 0)) return [3 /*break*/, 3]; + return [4 /*yield*/, self.pyodide.loadPackage(packages[0])]; + case 2: + _b.sent(); + _b.label = 3; + case 3: + if (!(packages[1].length > 0)) return [3 /*break*/, 6]; + return [4 /*yield*/, self.pyodide.loadPackage(['micropip'])]; + case 4: + _b.sent(); + micropip = self.pyodide.pyimport('micropip'); + return [4 /*yield*/, micropip.install(packages[1])]; + case 5: + _b.sent(); + _b.label = 6; + case 6: + version = self.pyodide.version; + onLoad({ version: version }); + return [2 /*return*/]; + } + }); + }); + }, + run: function (code) { + return __awaiter(this, void 0, void 0, function () { + console.log("SP", self.pyodide); + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, self.pyodide.runPythonAsync(code)]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + readFile: function (name) { + return self.pyodide.FS.readFile(name, { encoding: 'utf8' }); + }, + writeFile: function (name, data) { + return self.pyodide.FS.writeFile(name, data, { encoding: 'utf8' }); + }, + mkdir: function (name) { + self.pyodide.FS.mkdir(name); + }, + rmdir: function (name) { + self.pyodide.FS.rmdir(name); + } +}; +expose(python); diff --git a/src/components/CodeEditor/CodeMirror.js b/src/components/CodeEditor/CodeMirror.js new file mode 100644 index 000000000..2211a142a --- /dev/null +++ b/src/components/CodeEditor/CodeMirror.js @@ -0,0 +1,44 @@ +import React from "react"; +import { Box } from "@mui/material"; +import { INDENT } from "../../constant"; + +import "codemirror/lib/codemirror.css"; + +// Themes +import "codemirror/theme/night.css"; + +// Languages +import "codemirror/mode/python/python"; + +import { Controlled as ControlledEditorComponent } from "react-codemirror2"; + +const CodeMirrorEditor = ({ value, setEditorState, disableEditing }) => { + const handleEditorChange = (editor, data, value) => { + setEditorState(value); + }; + + return ( + + { + editor.setSize(null, null); + }} + onBeforeChange={handleEditorChange} + value={value} + className="code-mirror-wrapper" + options={{ + lineWrapping: true, + lint: true, + mode: "python", + lineNumbers: true, + indentUnit: INDENT, + matchBrackets: true, + singleCursorHeightPerLine: false, + readOnly: disableEditing, + }} + /> + + ); +}; + +export default CodeMirrorEditor; diff --git a/src/components/CodeEditor/PythonEditor.js b/src/components/CodeEditor/PythonEditor.js new file mode 100644 index 000000000..312d19314 --- /dev/null +++ b/src/components/CodeEditor/PythonEditor.js @@ -0,0 +1,251 @@ +import React, { useState, useEffect } from "react"; + +// import { usePython } from "./react-py"; + +import CodeMirrorEditor from "./CodeMirror"; + +import { Box, Button, Grid, Typography } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import ArrowRightIcon from "@mui/icons-material/ArrowRight"; +import RestartAltIcon from "@mui/icons-material/RestartAlt"; +import CableIcon from "@mui/icons-material/Cable"; + +/** + * + * @component + * @param {} initialCodeEditorValue Initial Python Code Editor value + * @param {} disableEditing Disable the user's ability to edit the Python Code Editor + * @param {} disableRun Disable the user's ability to run the Python Code Editor; Editing is also disabled when true. + * @return (PythonEditor) + */ + +const PythonEditor = ({ + initialCodeEditorValue, + disableEditing, + disableRun, + pythonRunner +}) => { + const theme = useTheme(); + // const { runPython, stdout, stderr, isLoading, isRunning } = usePython(); + const { runPython, stdout, stderr, isLoading, isRunning } = pythonRunner; + const [pythonEditorCode, setPythonEditorCode] = useState( + initialCodeEditorValue + ); + + return ( + + + + + + {disableRun ? ( + + Code not runnable? It's because this code does not have output + statements and is meant for understanding the code snippet + + ) : ( + + {disableEditing && ( + + Code not editable? It's because we want you to understand the + outcome directly + + )} + + + + + + )} + + + + + + + + + Output + + + + + {stderr} + {stdout} + + + + ); +}; + +export default PythonEditor; diff --git a/src/components/CodeEditor/react-py/hooks/useFilesystem.d.ts b/src/components/CodeEditor/react-py/hooks/useFilesystem.d.ts new file mode 100644 index 000000000..3ae5ebc37 --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/useFilesystem.d.ts @@ -0,0 +1,15 @@ +import { Remote } from "comlink"; +import { Runner } from "../types/Runner"; +interface UseFilesystemProps { + runner: Remote | undefined; +} +export default function useFilesystem(props: UseFilesystemProps): { + readFile: (name: string) => Promise | undefined; + writeFile: (name: string, data: string) => Promise | undefined; + mkdir: (name: string) => Promise | undefined; + rmdir: (name: string) => Promise | undefined; + watchModules: (moduleNames: string[]) => void; + unwatchModules: (moduleNames: string[]) => void; + watchedModules: Set; +}; +export {}; diff --git a/src/components/CodeEditor/react-py/hooks/useFilesystem.js b/src/components/CodeEditor/react-py/hooks/useFilesystem.js new file mode 100644 index 000000000..c5b36f96c --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/useFilesystem.js @@ -0,0 +1,88 @@ +import { useState } from "react"; + +var __read = + (this && this.__read) || + function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), + r, + ar = [], + e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) + ar.push(r.value); + } catch (error) { + e = { error: error }; + } finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } finally { + if (e) throw e.error; + } + } + return ar; + }; +var __spreadArray = + (this && this.__spreadArray) || + function (to, from, pack) { + if (pack || arguments.length === 2) + for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); + }; +export default function useFilesystem(props) { + var runner = props.runner; + var _a = __read(useState(new Set()), 2), + watchedModules = _a[0], + setWatchedModules = _a[1]; + var readFile = function (name) { + return runner === null || runner === void 0 + ? void 0 + : runner.readFile(name); + }; + var writeFile = function (name, data) { + return runner === null || runner === void 0 + ? void 0 + : runner.writeFile(name, data); + }; + var mkdir = function (name) { + return runner === null || runner === void 0 ? void 0 : runner.mkdir(name); + }; + var rmdir = function (name) { + return runner === null || runner === void 0 ? void 0 : runner.rmdir(name); + }; + var watchModules = function (moduleNames) { + setWatchedModules(function (prev) { + return new Set( + __spreadArray( + __spreadArray([], __read(prev), false), + __read(moduleNames), + false + ) + ); + }); + }; + var unwatchModules = function (moduleNames) { + setWatchedModules(function (prev) { + return new Set( + __spreadArray([], __read(prev), false).filter(function (e) { + return !moduleNames.includes(e); + }) + ); + }); + }; + return { + readFile: readFile, + writeFile: writeFile, + mkdir: mkdir, + rmdir: rmdir, + watchModules: watchModules, + unwatchModules: unwatchModules, + watchedModules: watchedModules, + }; +} diff --git a/src/components/CodeEditor/react-py/hooks/usePython.d.ts b/src/components/CodeEditor/react-py/hooks/usePython.d.ts new file mode 100644 index 000000000..de37a5215 --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/usePython.d.ts @@ -0,0 +1,20 @@ +import { Packages } from "../types/Packages"; +interface UsePythonProps { + packages?: Packages; +} +export default function usePython(props?: UsePythonProps): { + runPython: (code: string, preamble?: string) => Promise; + stdout: string; + stderr: string; + isLoading: boolean; + isReady: boolean; + isRunning: boolean; + interruptExecution: () => void; + readFile: (name: string) => Promise | undefined; + writeFile: (name: string, data: string) => Promise | undefined; + mkdir: (name: string) => Promise | undefined; + rmdir: (name: string) => Promise | undefined; + watchModules: (moduleNames: string[]) => void; + unwatchModules: (moduleNames: string[]) => void; +}; +export {}; diff --git a/src/components/CodeEditor/react-py/hooks/usePython.js b/src/components/CodeEditor/react-py/hooks/usePython.js new file mode 100644 index 000000000..9345913f7 --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/usePython.js @@ -0,0 +1,518 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { PythonContext, suppressedMessages } from "../providers/PythonProvider"; +import { proxy, wrap } from "comlink"; +import useFilesystem from "./useFilesystem"; + +var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done + ? resolve(result.value) + : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; +var __generator = + (this && this.__generator) || + function (thisArg, body) { + var _ = { + label: 0, + sent: function () { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [], + }, + f, + y, + t, + g; + return ( + (g = { next: verb(0), throw: verb(1), return: verb(2) }), + typeof Symbol === "function" && + (g[Symbol.iterator] = function () { + return this; + }), + g + ); + function verb(n) { + return function (v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while ((g && ((g = 0), op[0] && (_ = 0)), _)) + try { + if ( + ((f = 1), + y && + (t = + op[0] & 2 + ? y["return"] + : op[0] + ? y["throw"] || ((t = y["return"]) && t.call(y), 0) + : y.next) && + !(t = t.call(y, op[1])).done) + ) + return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if ( + !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && + (op[0] === 6 || op[0] === 2) + ) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; +var __read = + (this && this.__read) || + function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), + r, + ar = [], + e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) + ar.push(r.value); + } catch (error) { + e = { error: error }; + } finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } finally { + if (e) throw e.error; + } + } + return ar; + }; +var __spreadArray = + (this && this.__spreadArray) || + function (to, from, pack) { + if (pack || arguments.length === 2) + for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); + }; + +export default function usePython(props) { + var _this = this; + var _a = (props !== null && props !== void 0 ? props : {}).packages, + packages = _a === void 0 ? {} : _a; + var _b = __read(useState(false), 2), + isLoading = _b[0], + setIsLoading = _b[1]; + var _c = __read(useState(), 2), + pyodideVersion = _c[0], + setPyodideVersion = _c[1]; + var _d = __read(useState(false), 2), + isRunning = _d[0], + setIsRunning = _d[1]; + var _e = __read(useState([]), 2), + output = _e[0], + setOutput = _e[1]; + var _f = __read(useState(""), 2), + stdout = _f[0], + setStdout = _f[1]; + var _g = __read(useState(""), 2), + stderr = _g[0], + setStderr = _g[1]; + var _h = __read(useState(), 2), + pendingCode = _h[0], + setPendingCode = _h[1]; + var _j = __read(useState(false), 2), + hasRun = _j[0], + setHasRun = _j[1]; + var _k = useContext(PythonContext), + globalPackages = _k.packages, + timeout = _k.timeout, + lazy = _k.lazy, + terminateOnCompletion = _k.terminateOnCompletion; + var workerRef = useRef(); + var runnerRef = useRef(); + var _l = useFilesystem({ + runner: + runnerRef === null || runnerRef === void 0 ? void 0 : runnerRef.current, + }), + readFile = _l.readFile, + writeFile = _l.writeFile, + mkdir = _l.mkdir, + rmdir = _l.rmdir, + watchModules = _l.watchModules, + unwatchModules = _l.unwatchModules, + watchedModules = _l.watchedModules; + var createWorker = function () { + var worker = new Worker( + process.env.PUBLIC_URL + "/js/react-py/workers/python-worker.js" + // { type: 'module' } + ); + workerRef.current = worker; + }; + useEffect(function () { + if (!lazy) { + // Spawn worker on mount + createWorker(); + } + // Cleanup worker on unmount + return function () { + cleanup(); + }; + }, []); + var allPackages = useMemo( + function () { + var _a, _b, _c, _d; + var official = __spreadArray( + [], + __read( + new Set( + __spreadArray( + __spreadArray( + [], + __read( + (_a = globalPackages.official) !== null && _a !== void 0 + ? _a + : [] + ), + false + ), + __read( + (_b = packages.official) !== null && _b !== void 0 ? _b : [] + ), + false + ) + ) + ), + false + ); + var micropip = __spreadArray( + [], + __read( + new Set( + __spreadArray( + __spreadArray( + [], + __read( + (_c = globalPackages.micropip) !== null && _c !== void 0 + ? _c + : [] + ), + false + ), + __read( + (_d = packages.micropip) !== null && _d !== void 0 ? _d : [] + ), + false + ) + ) + ), + false + ); + return [official, micropip]; + }, + [globalPackages, packages] + ); + var isReady = !isLoading && !!pyodideVersion; + useEffect( + function () { + if (workerRef.current && !isReady) { + var init = function () { + return __awaiter(_this, void 0, void 0, function () { + var runner, error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, 3, 4]); + setIsLoading(true); + runner = wrap(workerRef.current); + runnerRef.current = runner; + return [ + 4 /*yield*/, + runner.init( + proxy(function (msg) { + // Suppress messages that are not useful for the user + if (suppressedMessages.includes(msg)) { + return; + } + setOutput(function (prev) { + return __spreadArray( + __spreadArray([], __read(prev), false), + [msg], + false + ); + }); + }), + proxy(function (_a) { + var version = _a.version; + // The runner is ready once the Pyodide version has been set + setPyodideVersion(version); + console.debug("Loaded pyodide version:", version); + }), + allPackages + ), + ]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: + error_1 = _a.sent(); + console.error("Error loading Pyodide:", error_1); + return [3 /*break*/, 4]; + case 3: + setIsLoading(false); + return [7 /*endfinally*/]; + case 4: + return [2 /*return*/]; + } + }); + }); + }; + init(); + } + }, + [workerRef.current] + ); + // Immediately set stdout upon receiving new input + useEffect( + function () { + if (output.length > 0) { + setStdout(output.join("\n")); + } + }, + [output] + ); + // React to ready state and run delayed code if pending + useEffect( + function () { + if (pendingCode && isReady) { + var delayedRun = function () { + return __awaiter(_this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4 /*yield*/, runPython(pendingCode)]; + case 1: + _a.sent(); + setPendingCode(undefined); + return [2 /*return*/]; + } + }); + }); + }; + delayedRun(); + } + }, + [pendingCode, isReady] + ); + // React to run completion and run cleanup if worker should terminate on completion + useEffect( + function () { + if (terminateOnCompletion && hasRun && !isRunning) { + cleanup(); + setIsRunning(false); + setPyodideVersion(undefined); + } + }, + [terminateOnCompletion, hasRun, isRunning] + ); + var pythonRunnerCode = + "\nimport sys\n\nsys.tracebacklimit = 0\n\nimport time\ndef sleep(seconds):\n start = now = time.time()\n while now - start < seconds:\n now = time.time()\ntime.sleep = sleep\n\ndef run(code, preamble=''):\n globals_ = {}\n try:\n exec(preamble, globals_)\n code = compile(code, 'code', 'exec')\n exec(code, globals_)\n except Exception:\n type_, value, tracebac = sys.exc_info()\n tracebac = tracebac.tb_next\n raise value.with_traceback(tracebac)\n finally:\n print()\n"; + // prettier-ignore + var moduleReloadCode = function (modules) { return "\nimport importlib\nimport sys\n".concat(Array.from(modules).map(function (name) { return "\nif \"\"\"".concat(name, "\"\"\" in sys.modules:\n importlib.reload(sys.modules[\"\"\"").concat(name, "\"\"\"])\n"); }).join(''), "\ndel importlib\ndel sys\n"); }; + var runPython = useCallback( + function (code, preamble) { + if (preamble === void 0) { + preamble = ""; + } + return __awaiter(_this, void 0, void 0, function () { + var timeoutTimer, error_2; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // Clear stdout and stderr + setStdout(""); + setStderr(""); + if (lazy && !isReady) { + // Spawn worker and set pending code + createWorker(); + setPendingCode(code); + return [2 /*return*/]; + } + code = "" + .concat(pythonRunnerCode, "\n\nrun(") + .concat(JSON.stringify(code), ", ") + .concat(JSON.stringify(preamble), ")"); + if (!isReady) { + throw new Error("Pyodide is not loaded yet"); + } + _a.label = 1; + case 1: + _a.trys.push([1, 5, 6, 7]); + setIsRunning(true); + setHasRun(true); + // Clear output + setOutput([]); + if (!isReady || !runnerRef.current) { + throw new Error("Pyodide is not loaded yet"); + } + if (timeout > 0) { + timeoutTimer = setTimeout(function () { + setStdout(""); + setStderr( + "Execution timed out. Reached limit of ".concat( + timeout, + " ms." + ) + ); + interruptExecution(); + }, timeout); + } + if (!(watchedModules.size > 0)) return [3 /*break*/, 3]; + return [ + 4 /*yield*/, + runnerRef.current.run(moduleReloadCode(watchedModules)), + ]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + return [ + 4 /*yield*/, + runnerRef.current.run(code), + // eslint-disable-next-line + ]; + case 4: + _a.sent(); + return [3 /*break*/, 7]; + case 5: + error_2 = _a.sent(); + setStderr( + "Traceback (most recent call last):\n" + error_2.message + ); + return [3 /*break*/, 7]; + case 6: + setIsRunning(false); + clearTimeout(timeoutTimer); + return [7 /*endfinally*/]; + case 7: + return [2 /*return*/]; + } + }); + }); + }, + [lazy, isReady, timeout, watchedModules] + ); + var interruptExecution = function () { + cleanup(); + setIsRunning(false); + setPyodideVersion(undefined); + setOutput([]); + // Spawn new worker + createWorker(); + }; + var cleanup = function () { + if (!workerRef.current) { + return; + } + console.debug("Terminating worker"); + workerRef.current.terminate(); + }; + return { + runPython: runPython, + stdout: stdout, + stderr: stderr, + isLoading: isLoading, + isReady: isReady, + isRunning: isRunning, + interruptExecution: interruptExecution, + readFile: readFile, + writeFile: writeFile, + mkdir: mkdir, + rmdir: rmdir, + watchModules: watchModules, + unwatchModules: unwatchModules, + }; +} diff --git a/src/components/CodeEditor/react-py/hooks/usePython.test.d.ts b/src/components/CodeEditor/react-py/hooks/usePython.test.d.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/CodeEditor/react-py/hooks/usePython.test.js b/src/components/CodeEditor/react-py/hooks/usePython.test.js new file mode 100644 index 000000000..9a7ae63d8 --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/usePython.test.js @@ -0,0 +1,18 @@ +"use strict"; +// import { test } from "mocha"; +// import { expect } from "chai"; +// import { renderHook, act } from "@testing-library/react-hooks/native"; +// import usePython from "./usePython"; +// import { PythonProvider } from "./PythonProvider"; +// test("should output hello world", async () => { +// const wrapper = ({ children }: any) => { +// return {children}; +// }; +// const { result } = renderHook(() => usePython(), { +// wrapper, +// }); +// act(() => { +// result.current.runPython('print("Hello World")'); +// }); +// expect(result.current.stdout).to.be.equal("Hello World\n"); +// }); diff --git a/src/components/CodeEditor/react-py/hooks/usePythonConsole.d.ts b/src/components/CodeEditor/react-py/hooks/usePythonConsole.d.ts new file mode 100644 index 000000000..64a70231f --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/usePythonConsole.d.ts @@ -0,0 +1,23 @@ +import { Packages } from "../types/Packages"; +import { ConsoleState } from "../types/Console"; +interface UsePythonConsoleProps { + packages?: Packages; +} +export default function usePythonConsole(props?: UsePythonConsoleProps): { + runPython: (code: string) => Promise; + stdout: string; + stderr: string; + isLoading: boolean; + isReady: boolean; + isRunning: boolean; + interruptExecution: () => void; + readFile: (name: string) => Promise | undefined; + writeFile: (name: string, data: string) => Promise | undefined; + mkdir: (name: string) => Promise | undefined; + rmdir: (name: string) => Promise | undefined; + watchModules: (moduleNames: string[]) => void; + unwatchModules: (moduleNames: string[]) => void; + banner: string | undefined; + consoleState: ConsoleState | undefined; +}; +export {}; diff --git a/src/components/CodeEditor/react-py/hooks/usePythonConsole.js b/src/components/CodeEditor/react-py/hooks/usePythonConsole.js new file mode 100644 index 000000000..40d4300df --- /dev/null +++ b/src/components/CodeEditor/react-py/hooks/usePythonConsole.js @@ -0,0 +1,449 @@ +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { PythonContext, suppressedMessages } from "../providers/PythonProvider"; +import { proxy, wrap } from "comlink"; +import useFilesystem from "./useFilesystem"; +import { ConsoleState } from "../types/Console"; + +var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done + ? resolve(result.value) + : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; +var __generator = + (this && this.__generator) || + function (thisArg, body) { + var _ = { + label: 0, + sent: function () { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [], + }, + f, + y, + t, + g; + return ( + (g = { next: verb(0), throw: verb(1), return: verb(2) }), + typeof Symbol === "function" && + (g[Symbol.iterator] = function () { + return this; + }), + g + ); + function verb(n) { + return function (v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while ((g && ((g = 0), op[0] && (_ = 0)), _)) + try { + if ( + ((f = 1), + y && + (t = + op[0] & 2 + ? y["return"] + : op[0] + ? y["throw"] || ((t = y["return"]) && t.call(y), 0) + : y.next) && + !(t = t.call(y, op[1])).done) + ) + return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if ( + !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && + (op[0] === 6 || op[0] === 2) + ) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; +var __read = + (this && this.__read) || + function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), + r, + ar = [], + e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) + ar.push(r.value); + } catch (error) { + e = { error: error }; + } finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } finally { + if (e) throw e.error; + } + } + return ar; + }; +var __spreadArray = + (this && this.__spreadArray) || + function (to, from, pack) { + if (pack || arguments.length === 2) + for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); + }; + +export default function usePythonConsole(props) { + var _this = this; + var _a = (props !== null && props !== void 0 ? props : {}).packages, + packages = _a === void 0 ? {} : _a; + var _b = __read(useState(false), 2), + isLoading = _b[0], + setIsLoading = _b[1]; + var _c = __read(useState(), 2), + pyodideVersion = _c[0], + setPyodideVersion = _c[1]; + var _d = __read(useState(), 2), + banner = _d[0], + setBanner = _d[1]; + var _e = __read(useState(), 2), + consoleState = _e[0], + setConsoleState = _e[1]; + var _f = __read(useState(false), 2), + isRunning = _f[0], + setIsRunning = _f[1]; + var _g = __read(useState(""), 2), + stdout = _g[0], + setStdout = _g[1]; + var _h = __read(useState(""), 2), + stderr = _h[0], + setStderr = _h[1]; + var _j = useContext(PythonContext), + globalPackages = _j.packages, + timeout = _j.timeout; + var workerRef = useRef(); + var runnerRef = useRef(); + var _k = useFilesystem({ + runner: + runnerRef === null || runnerRef === void 0 ? void 0 : runnerRef.current, + }), + readFile = _k.readFile, + writeFile = _k.writeFile, + mkdir = _k.mkdir, + rmdir = _k.rmdir, + watchModules = _k.watchModules, + unwatchModules = _k.unwatchModules, + watchedModules = _k.watchedModules; + var createWorker = function () { + var worker = new Worker( + process.env.PUBLIC_URL + "/js/react-py/workers/python-concole-worker.js" + // { type: 'module' } + ); + workerRef.current = worker; + }; + useEffect(function () { + // Spawn worker on mount + createWorker(); + // Cleanup worker on unmount + return function () { + cleanup(); + }; + }, []); + var allPackages = useMemo( + function () { + var _a, _b, _c, _d; + var official = __spreadArray( + [], + __read( + new Set( + __spreadArray( + __spreadArray( + [], + __read( + (_a = globalPackages.official) !== null && _a !== void 0 + ? _a + : [] + ), + false + ), + __read( + (_b = packages.official) !== null && _b !== void 0 ? _b : [] + ), + false + ) + ) + ), + false + ); + var micropip = __spreadArray( + [], + __read( + new Set( + __spreadArray( + __spreadArray( + [], + __read( + (_c = globalPackages.micropip) !== null && _c !== void 0 + ? _c + : [] + ), + false + ), + __read( + (_d = packages.micropip) !== null && _d !== void 0 ? _d : [] + ), + false + ) + ) + ), + false + ); + return [official, micropip]; + }, + [globalPackages, packages] + ); + var isReady = !isLoading && !!pyodideVersion && !!banner; + useEffect( + function () { + if (workerRef.current && !isReady) { + var init = function () { + return __awaiter(_this, void 0, void 0, function () { + var runner, error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, 3, 4]); + setIsLoading(true); + runner = wrap(workerRef.current); + runnerRef.current = runner; + return [ + 4 /*yield*/, + runner.init( + proxy(function (msg) { + // Suppress messages that are not useful for the user + if (suppressedMessages.includes(msg)) { + return; + } + setStdout(msg); + }), + proxy(function (_a) { + var version = _a.version, + banner = _a.banner; + // The runner is ready once the Pyodide version has been set + setPyodideVersion(version); + setBanner(banner); + console.debug("Loaded pyodide version:", version); + }), + allPackages + ), + ]; + case 1: + _a.sent(); + return [3 /*break*/, 4]; + case 2: + error_1 = _a.sent(); + console.error("Error loading Pyodide:", error_1); + return [3 /*break*/, 4]; + case 3: + setIsLoading(false); + return [7 /*endfinally*/]; + case 4: + return [2 /*return*/]; + } + }); + }); + }; + init(); + } + }, + [workerRef.current] + ); + // prettier-ignore + var moduleReloadCode = function (modules) { return "\nimport importlib\nimport sys\n".concat(Array.from(modules).map(function (name) { return "\nif \"\"\"".concat(name, "\"\"\" in sys.modules:\n importlib.reload(sys.modules[\"\"\"").concat(name, "\"\"\"])\n"); }).join(''), "\ndel importlib\ndel sys\n"); }; + var runPython = useCallback( + function (code) { + return __awaiter(_this, void 0, void 0, function () { + var timeoutTimer, _a, state, error, error_2; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + // Clear stdout and stderr + setStdout(""); + setStderr(""); + if (!isReady) { + throw new Error("Pyodide is not loaded yet"); + } + _b.label = 1; + case 1: + _b.trys.push([1, 5, 6, 7]); + setIsRunning(true); + if (!isReady || !runnerRef.current) { + throw new Error("Pyodide is not loaded yet"); + } + if (timeout > 0) { + timeoutTimer = setTimeout(function () { + setStdout(""); + setStderr( + "Execution timed out. Reached limit of ".concat( + timeout, + " ms." + ) + ); + interruptExecution(); + }, timeout); + } + if (!(watchedModules.size > 0)) return [3 /*break*/, 3]; + return [ + 4 /*yield*/, + runnerRef.current.run(moduleReloadCode(watchedModules)), + ]; + case 2: + _b.sent(); + _b.label = 3; + case 3: + return [4 /*yield*/, runnerRef.current.run(code)]; + case 4: + // eslint-disable-next-line + /*;*/ (_a = _b.sent()), (state = _a.state), (error = _a.error); + setConsoleState(ConsoleState[state]); + if (error) { + setStderr(error); + } + return [3 /*break*/, 7]; + case 5: + error_2 = _b.sent(); + console.error("Error pushing to console:", error_2); + return [3 /*break*/, 7]; + case 6: + setIsRunning(false); + clearTimeout(timeoutTimer); + return [7 /*endfinally*/]; + case 7: + return [2 /*return*/]; + } + }); + }); + }, + [isReady, timeout, watchedModules] + ); + var interruptExecution = function () { + cleanup(); + setIsRunning(false); + setPyodideVersion(undefined); + setBanner(undefined); + setConsoleState(undefined); + // Spawn new worker + createWorker(); + }; + var cleanup = function () { + if (!workerRef.current) { + return; + } + console.debug("Terminating worker"); + workerRef.current.terminate(); + }; + return { + runPython: runPython, + stdout: stdout, + stderr: stderr, + isLoading: isLoading, + isReady: isReady, + isRunning: isRunning, + interruptExecution: interruptExecution, + readFile: readFile, + writeFile: writeFile, + mkdir: mkdir, + rmdir: rmdir, + watchModules: watchModules, + unwatchModules: unwatchModules, + banner: banner, + consoleState: consoleState, + }; +} diff --git a/src/components/CodeEditor/react-py/index.js b/src/components/CodeEditor/react-py/index.js new file mode 100644 index 000000000..e50153065 --- /dev/null +++ b/src/components/CodeEditor/react-py/index.js @@ -0,0 +1,4 @@ +import { PythonProvider } from "./providers/PythonProvider"; +import usePython from "./hooks/usePython"; +import usePythonConsole from "./hooks/usePythonConsole"; +export { PythonProvider, usePython, usePythonConsole }; diff --git a/src/components/CodeEditor/react-py/providers/PythonProvider.d.ts b/src/components/CodeEditor/react-py/providers/PythonProvider.d.ts new file mode 100644 index 000000000..27d933287 --- /dev/null +++ b/src/components/CodeEditor/react-py/providers/PythonProvider.d.ts @@ -0,0 +1,18 @@ +/// +import { Packages } from "../types/Packages"; +declare const PythonContext: import("react").Context<{ + packages: Packages; + timeout: number; + lazy: boolean; + terminateOnCompletion: boolean; +}>; +export declare const suppressedMessages: string[]; +interface PythonProviderProps { + packages?: Packages; + timeout?: number; + lazy?: boolean; + terminateOnCompletion?: boolean; + children: any; +} +declare function PythonProvider(props: PythonProviderProps): JSX.Element; +export { PythonContext, PythonProvider }; diff --git a/src/components/CodeEditor/react-py/providers/PythonProvider.js b/src/components/CodeEditor/react-py/providers/PythonProvider.js new file mode 100644 index 000000000..6d03bf7ac --- /dev/null +++ b/src/components/CodeEditor/react-py/providers/PythonProvider.js @@ -0,0 +1,51 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { createContext } from "react"; + +var __assign = + (this && this.__assign) || + function () { + __assign = + Object.assign || + function (t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) + if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + +var PythonContext = createContext({ + packages: {}, + timeout: 0, + lazy: false, + terminateOnCompletion: false, +}); +export var suppressedMessages = ["Python initialization complete"]; +function PythonProvider(props) { + var _a = props.packages, + packages = _a === void 0 ? {} : _a, + _b = props.timeout, + timeout = _b === void 0 ? 0 : _b, + _c = props.lazy, + lazy = _c === void 0 ? false : _c, + _d = props.terminateOnCompletion, + terminateOnCompletion = _d === void 0 ? false : _d; + return _jsx( + PythonContext.Provider, + __assign( + { + value: { + packages: packages, + timeout: timeout, + lazy: lazy, + terminateOnCompletion: terminateOnCompletion, + }, + }, + props + ) + ); +} +export { PythonContext, PythonProvider }; diff --git a/src/components/CodeEditor/react-py/types/Console.d.ts b/src/components/CodeEditor/react-py/types/Console.d.ts new file mode 100644 index 000000000..9e413873c --- /dev/null +++ b/src/components/CodeEditor/react-py/types/Console.d.ts @@ -0,0 +1,5 @@ +export declare enum ConsoleState { + "complete" = 0, + "incomplete" = 1, + "syntax-error" = 2, +} diff --git a/src/components/CodeEditor/react-py/types/Console.js b/src/components/CodeEditor/react-py/types/Console.js new file mode 100644 index 000000000..5bd5824fc --- /dev/null +++ b/src/components/CodeEditor/react-py/types/Console.js @@ -0,0 +1,6 @@ +export var ConsoleState; +(function (ConsoleState) { + ConsoleState[(ConsoleState["complete"] = 0)] = "complete"; + ConsoleState[(ConsoleState["incomplete"] = 1)] = "incomplete"; + ConsoleState[(ConsoleState["syntax-error"] = 2)] = "syntax-error"; +})(ConsoleState || (ConsoleState = {})); diff --git a/src/components/CodeEditor/react-py/types/Packages.d.ts b/src/components/CodeEditor/react-py/types/Packages.d.ts new file mode 100644 index 000000000..c126e086f --- /dev/null +++ b/src/components/CodeEditor/react-py/types/Packages.d.ts @@ -0,0 +1,4 @@ +export interface Packages { + official?: string[]; + micropip?: string[]; +} diff --git a/src/components/CodeEditor/react-py/types/Packages.js b/src/components/CodeEditor/react-py/types/Packages.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/src/components/CodeEditor/react-py/types/Packages.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/CodeEditor/react-py/types/Runner.d.ts b/src/components/CodeEditor/react-py/types/Runner.d.ts new file mode 100644 index 000000000..9b464426d --- /dev/null +++ b/src/components/CodeEditor/react-py/types/Runner.d.ts @@ -0,0 +1,21 @@ +export interface Runner { + init: ( + stdout: (msg: string) => void, + onLoad: ({ version, banner }: { version: string; banner?: string }) => void, + packages?: string[][] + ) => Promise; + interruptExecution: () => void; + readFile: (name: string) => void; + writeFile: (name: string, data: string) => void; + mkdir: (name: string) => void; + rmdir: (name: string) => void; +} +export interface PythonRunner extends Runner { + run: (code: string) => Promise; +} +export interface PythonConsoleRunner extends Runner { + run: (code: string) => Promise<{ + state: string; + error?: string; + }>; +} diff --git a/src/components/CodeEditor/react-py/types/Runner.js b/src/components/CodeEditor/react-py/types/Runner.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/src/components/CodeEditor/react-py/types/Runner.js @@ -0,0 +1 @@ +export {}; diff --git a/src/components/CodeEditor/react-py/workers/python-console-worker.d.ts b/src/components/CodeEditor/react-py/workers/python-console-worker.d.ts new file mode 100644 index 000000000..a549c7431 --- /dev/null +++ b/src/components/CodeEditor/react-py/workers/python-console-worker.d.ts @@ -0,0 +1,28 @@ +interface Pyodide { + loadPackage: (packages: string[]) => Promise; + pyimport: (pkg: string) => micropip; + runPythonAsync: (code: string, namespace?: any) => Promise; + version: string; + FS: { + readFile: (name: string, options: unknown) => void; + writeFile: (name: string, data: string, options: unknown) => void; + mkdir: (name: string) => void; + rmdir: (name: string) => void; + }; + globals: any; + isPyProxy: (value: unknown) => boolean; +} +interface micropip { + install: (packages: string[]) => Promise; +} +declare global { + interface Window { + loadPyodide: ({ + stdout, + }: { + stdout?: (msg: string) => void; + }) => Promise; + pyodide: Pyodide; + } +} +export {}; diff --git a/src/components/CodeEditor/react-py/workers/python-console-worker.js b/src/components/CodeEditor/react-py/workers/python-console-worker.js new file mode 100644 index 000000000..12af81b7f --- /dev/null +++ b/src/components/CodeEditor/react-py/workers/python-console-worker.js @@ -0,0 +1,330 @@ +var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done + ? resolve(result.value) + : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; +var __generator = + (this && this.__generator) || + function (thisArg, body) { + var _ = { + label: 0, + sent: function () { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [], + }, + f, + y, + t, + g; + return ( + (g = { next: verb(0), throw: verb(1), return: verb(2) }), + typeof Symbol === "function" && + (g[Symbol.iterator] = function () { + return this; + }), + g + ); + function verb(n) { + return function (v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while ((g && ((g = 0), op[0] && (_ = 0)), _)) + try { + if ( + ((f = 1), + y && + (t = + op[0] & 2 + ? y["return"] + : op[0] + ? y["throw"] || ((t = y["return"]) && t.call(y), 0) + : y.next) && + !(t = t.call(y, op[1])).done) + ) + return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if ( + !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && + (op[0] === 6 || op[0] === 2) + ) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; +var __values = + (this && this.__values) || + function (o) { + var s = typeof Symbol === "function" && Symbol.iterator, + m = s && o[s], + i = 0; + if (m) return m.call(o); + if (o && typeof o.length === "number") + return { + next: function () { + if (o && i >= o.length) o = void 0; + return { value: o && o[i++], done: !o }; + }, + }; + throw new TypeError( + s ? "Object is not iterable." : "Symbol.iterator is not defined." + ); + }; +var __read = + (this && this.__read) || + function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), + r, + ar = [], + e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) + ar.push(r.value); + } catch (error) { + e = { error: error }; + } finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } finally { + if (e) throw e.error; + } + } + return ar; + }; +importScripts("https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js"); +// Monkey patch console.log to prevent the script from outputting logs +// eslint-disable-next-line @typescript-eslint/no-empty-function +console.log = function () {}; +import { expose } from "comlink"; +var initConsoleCode = + '\nimport sys\nfrom pyodide.ffi import to_js\nfrom pyodide.console import PyodideConsole, repr_shorten, BANNER\nimport __main__\nBANNER = "Welcome to the Pyodide terminal emulator \uD83D\uDC0D\\n" + BANNER\npyconsole = PyodideConsole(__main__.__dict__)\nimport builtins\nasync def await_fut(fut):\n res = await fut\n if res is not None:\n builtins._ = res\n return to_js([res], depth=1)\ndef clear_console():\n pyconsole.buffer = []\n'; +var pythonConsole; +var python = { + init: function (stdout, onLoad, packages) { + return __awaiter(this, void 0, void 0, function () { + var _a, + micropip, + version, + namespace, + reprShorten, + banner, + awaitFut, + pyconsole, + clearConsole; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = self; + return [4 /*yield*/, self.loadPyodide({})]; + case 1: + _a.pyodide = _b.sent(); + if (!(packages[0].length > 0)) return [3 /*break*/, 3]; + return [4 /*yield*/, self.pyodide.loadPackage(packages[0])]; + case 2: + _b.sent(); + _b.label = 3; + case 3: + if (!(packages[1].length > 0)) return [3 /*break*/, 6]; + return [4 /*yield*/, self.pyodide.loadPackage(["micropip"])]; + case 4: + _b.sent(); + micropip = self.pyodide.pyimport("micropip"); + return [4 /*yield*/, micropip.install(packages[1])]; + case 5: + _b.sent(); + _b.label = 6; + case 6: + version = self.pyodide.version; + namespace = self.pyodide.globals.get("dict")(); + return [ + 4 /*yield*/, + self.pyodide.runPythonAsync(initConsoleCode, { + globals: namespace, + }), + ]; + case 7: + _b.sent(); + reprShorten = namespace.get("repr_shorten"); + banner = namespace.get("BANNER"); + awaitFut = namespace.get("await_fut"); + pyconsole = namespace.get("pyconsole"); + clearConsole = namespace.get("clear_console"); + namespace.destroy(); + // eslint-disable-next-line camelcase + pyconsole.stdout_callback = stdout; + pythonConsole = { + reprShorten: reprShorten, + awaitFut: awaitFut, + pyconsole: pyconsole, + clearConsole: clearConsole, + }; + onLoad({ version: version, banner: banner }); + return [2 /*return*/]; + } + }); + }); + }, + run: function (code) { + return __awaiter(this, void 0, void 0, function () { + var state, _a, _b, line, fut, wrapped, _c, value, error_1, message, e_1_1; + var e_1, _d; + return __generator(this, function (_e) { + switch (_e.label) { + case 0: + if (!pythonConsole) { + throw new Error("Console has not been initialised"); + } + if (code === undefined) { + throw new Error("No code to push"); + } + _e.label = 1; + case 1: + _e.trys.push([1, 9, 10, 11]); + (_a = __values(code.split("\n"))), (_b = _a.next()); + _e.label = 2; + case 2: + if (!!_b.done) return [3 /*break*/, 8]; + line = _b.value; + fut = pythonConsole.pyconsole.push(line); + state = fut.syntax_check; + wrapped = pythonConsole.awaitFut(fut); + _e.label = 3; + case 3: + _e.trys.push([3, 5, 6, 7]); + return [4 /*yield*/, wrapped]; + case 4: + (_c = __read.apply(void 0, [_e.sent(), 1])), (value = _c[0]); + if (self.pyodide.isPyProxy(value)) { + value.destroy(); + } + return [3 /*break*/, 7]; + case 5: + error_1 = _e.sent(); + if (error_1.constructor.name === "PythonError") { + message = fut.formatted_error || error_1.message; + return [2 /*return*/, { state: state, error: message.trimEnd() }]; + } else { + throw error_1; + } + return [3 /*break*/, 7]; + case 6: + fut.destroy(); + wrapped.destroy(); + return [7 /*endfinally*/]; + case 7: + _b = _a.next(); + return [3 /*break*/, 2]; + case 8: + return [3 /*break*/, 11]; + case 9: + e_1_1 = _e.sent(); + e_1 = { error: e_1_1 }; + return [3 /*break*/, 11]; + case 10: + try { + if (_b && !_b.done && (_d = _a.return)) _d.call(_a); + } finally { + if (e_1) throw e_1.error; + } + return [7 /*endfinally*/]; + case 11: + return [2 /*return*/, { state: state }]; + } + }); + }); + }, + readFile: function (name) { + return self.pyodide.FS.readFile(name, { encoding: "utf8" }); + }, + writeFile: function (name, data) { + return self.pyodide.FS.writeFile(name, data, { encoding: "utf8" }); + }, + mkdir: function (name) { + self.pyodide.FS.mkdir(name); + }, + rmdir: function (name) { + self.pyodide.FS.rmdir(name); + }, +}; +expose(python); diff --git a/src/components/CodeEditor/react-py/workers/python-worker.d.ts b/src/components/CodeEditor/react-py/workers/python-worker.d.ts new file mode 100644 index 000000000..a549c7431 --- /dev/null +++ b/src/components/CodeEditor/react-py/workers/python-worker.d.ts @@ -0,0 +1,28 @@ +interface Pyodide { + loadPackage: (packages: string[]) => Promise; + pyimport: (pkg: string) => micropip; + runPythonAsync: (code: string, namespace?: any) => Promise; + version: string; + FS: { + readFile: (name: string, options: unknown) => void; + writeFile: (name: string, data: string, options: unknown) => void; + mkdir: (name: string) => void; + rmdir: (name: string) => void; + }; + globals: any; + isPyProxy: (value: unknown) => boolean; +} +interface micropip { + install: (packages: string[]) => Promise; +} +declare global { + interface Window { + loadPyodide: ({ + stdout, + }: { + stdout?: (msg: string) => void; + }) => Promise; + pyodide: Pyodide; + } +} +export {}; diff --git a/src/components/CodeEditor/react-py/workers/python-worker.js b/src/components/CodeEditor/react-py/workers/python-worker.js new file mode 100644 index 000000000..c0dec34d8 --- /dev/null +++ b/src/components/CodeEditor/react-py/workers/python-worker.js @@ -0,0 +1,204 @@ +var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done + ? resolve(result.value) + : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; +var __generator = + (this && this.__generator) || + function (thisArg, body) { + var _ = { + label: 0, + sent: function () { + if (t[0] & 1) throw t[1]; + return t[1]; + }, + trys: [], + ops: [], + }, + f, + y, + t, + g; + return ( + (g = { next: verb(0), throw: verb(1), return: verb(2) }), + typeof Symbol === "function" && + (g[Symbol.iterator] = function () { + return this; + }), + g + ); + function verb(n) { + return function (v) { + return step([n, v]); + }; + } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while ((g && ((g = 0), op[0] && (_ = 0)), _)) + try { + if ( + ((f = 1), + y && + (t = + op[0] & 2 + ? y["return"] + : op[0] + ? y["throw"] || ((t = y["return"]) && t.call(y), 0) + : y.next) && + !(t = t.call(y, op[1])).done) + ) + return t; + if (((y = 0), t)) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: + case 1: + t = op; + break; + case 4: + _.label++; + return { value: op[1], done: false }; + case 5: + _.label++; + y = op[1]; + op = [0]; + continue; + case 7: + op = _.ops.pop(); + _.trys.pop(); + continue; + default: + if ( + !((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && + (op[0] === 6 || op[0] === 2) + ) { + _ = 0; + continue; + } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { + _.label = op[1]; + break; + } + if (op[0] === 6 && _.label < t[1]) { + _.label = t[1]; + t = op; + break; + } + if (t && _.label < t[2]) { + _.label = t[2]; + _.ops.push(op); + break; + } + if (t[2]) _.ops.pop(); + _.trys.pop(); + continue; + } + op = body.call(thisArg, _); + } catch (e) { + op = [6, e]; + y = 0; + } finally { + f = t = 0; + } + if (op[0] & 5) throw op[1]; + return { value: op[0] ? op[1] : void 0, done: true }; + } + }; +importScripts("https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js"); +// Monkey patch console.log to prevent the script from outputting logs +// eslint-disable-next-line @typescript-eslint/no-empty-function +console.log = function () {}; +import { expose } from "comlink"; +var python = { + init: function (stdout, onLoad, packages) { + return __awaiter(this, void 0, void 0, function () { + var _a, micropip, version; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = self; + return [ + 4 /*yield*/, + self.loadPyodide({ + stdout: stdout, + }), + ]; + case 1: + _a.pyodide = _b.sent(); + if (!(packages[0].length > 0)) return [3 /*break*/, 3]; + return [4 /*yield*/, self.pyodide.loadPackage(packages[0])]; + case 2: + _b.sent(); + _b.label = 3; + case 3: + if (!(packages[1].length > 0)) return [3 /*break*/, 6]; + return [4 /*yield*/, self.pyodide.loadPackage(["micropip"])]; + case 4: + _b.sent(); + micropip = self.pyodide.pyimport("micropip"); + return [4 /*yield*/, micropip.install(packages[1])]; + case 5: + _b.sent(); + _b.label = 6; + case 6: + version = self.pyodide.version; + onLoad({ version: version }); + return [2 /*return*/]; + } + }); + }); + }, + run: function (code) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + return [4 /*yield*/, self.pyodide.runPythonAsync(code)]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }, + readFile: function (name) { + return self.pyodide.FS.readFile(name, { encoding: "utf8" }); + }, + writeFile: function (name, data) { + return self.pyodide.FS.writeFile(name, data, { encoding: "utf8" }); + }, + mkdir: function (name) { + self.pyodide.FS.mkdir(name); + }, + rmdir: function (name) { + self.pyodide.FS.rmdir(name); + }, +}; +expose(python); diff --git a/src/components/PathwayExercise/ExerciseContent/index.js b/src/components/PathwayExercise/ExerciseContent/index.js index b052921df..1fa9655a1 100644 --- a/src/components/PathwayExercise/ExerciseContent/index.js +++ b/src/components/PathwayExercise/ExerciseContent/index.js @@ -29,7 +29,7 @@ import { } from "@mui/material"; // import HiddenContent from "../HiddenContent"; -import { versionCode } from "../../../constant"; +import { INDENT, versionCode, CODE_EDITOR_FIELDS } from "../../../constant"; import useStyles from "../styles"; import ExerciseBatchClass from "../../BatchClassComponents/ExerciseBatchClass/ExerciseBatchClass"; @@ -43,23 +43,8 @@ import ExerciseContentLoading from "./ExerciseContentLoading"; import PersistentDrawerLeft from "./Drawers/Drawer"; import MobileDrawer from "./Drawers/MobileDrawer"; import ContentListText from "./Drawers/ContentListText"; - -const createVisulizeURL = (code, lang, mode) => { - // only support two languages for now - const l = lang == "python" ? "2" : "js"; - const replacedCode = code && code.replace(/
/g, "\n"); - const visualizerCode = replacedCode.replace(/ /g, " "); - const url = `http://pythontutor.com/visualize.html#code=${encodeURIComponent( - visualizerCode - ) - .replace(/%2C|%2F/g, decodeURIComponent) - .replace(/\(/g, "%28") - .replace( - /\)/g, - "%29" - )}&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=${mode}&origin=opt-frontend.js&py=${l}&rawInputLstJSON=%5B%5D&textReferences=false`; - return url; -}; +import PythonEditor from "../../CodeEditor/PythonEditor"; +import { usePython } from "../../CodeEditor/react-py"; function UnsafeHTML(props) { const { html, Container, ...otherProps } = props; @@ -104,7 +89,6 @@ const RenderDoubtClass = ({ data, exercise }) => { {start_time && end_time && ( <> -
{ return null; }; -const RenderContent = ({ data, exercise, pathwayData }) => { +const RenderContent = ({ data, exercise, pathwayData, pythonRunner }) => { const classes = useStyles(); - const isActive = useMediaQuery("(max-width:" + breakpoints.values.sm + "px)"); + // const isActive = useMediaQuery("(max-width:" + breakpoints.values.sm + "px)"); const playerRef = useRef(null); const videoId = data.value.includes("=") @@ -303,8 +287,9 @@ const RenderContent = ({ data, exercise, pathwayData }) => { ); } - if (data.component === "code") { + if (data.component === "code" && data.type !== "python") { const codeContent = DOMPurify.sanitize(get(data, "value")); + return (
@@ -318,26 +303,31 @@ const RenderContent = ({ data, exercise, pathwayData }) => { Code Example {/* */} + - - -
); } + + if (data.component === "code" && data.type === "python") { + const pythonCode = data.value + .replace(/
/g, "\n") + .replace(/ /g, " ".repeat(INDENT)); + return ( + + ); + } // if (data.type === "solution") { // return ( // @@ -360,7 +350,7 @@ function ExerciseContent({ courseTitle, progressTrackId, }) { - const isActive = useMediaQuery("(max-width:" + breakpoints.values.sm + "px)"); + // const isActive = useMediaQuery("(max-width:" + breakpoints.values.sm + "px)"); const user = useSelector(({ User }) => User); const [content, setContent] = useState([]); const [course, setCourse] = useState(); @@ -499,7 +489,18 @@ function ExerciseContent({ function ExerciseContentMain() { const [selected, setSelected] = useState(params.exerciseId); const desktop = useMediaQuery("(min-width: 900px)"); - + const pythonRunner = usePython(); + // const [pythonRunner, setPythonRunner] = useState(null); + + /* + useEffect(() => { + if (!pythonRunner && content?.find(contentItem => contentItem.component === "code" && contentItem.type === "python")) { + // only load Pyodide when there's a Python code component + setPythonRunner(usePython()); + } + }, [content, pythonRunner]); + */ + return ( {!desktop && } @@ -580,6 +581,7 @@ function ExerciseContent({ {content && content.map((contentItem, index) => ( ); } - - const isActive = useMediaQuery("(max-width:" + breakpoints.values.sm + "px)"); - const isActiveIpad = useMediaQuery("(max-width:1300px)"); + + // const isActive = useMediaQuery("(max-width:" + breakpoints.values.sm + "px)"); + // const isActiveIpad = useMediaQuery("(max-width:1300px)"); + // const theme = useTheme(); return ( <> @@ -588,10 +591,12 @@ function PathwayExercise() { @@ -633,22 +638,22 @@ function PathwayExercise() { ) : ( // + {/* */} + {/* */} )} - +