Update: 添加新板卡 Python 3 Online
This commit is contained in:
3
boards/default_src/python_pyodide/others/loader.js
Normal file
3
boards/default_src/python_pyodide/others/loader.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import NavExt from './nav-ext';
|
||||
|
||||
NavExt.init();
|
||||
43
boards/default_src/python_pyodide/others/nav-ext.js
Normal file
43
boards/default_src/python_pyodide/others/nav-ext.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { app, Nav } from 'mixly';
|
||||
import * as Blockly from 'blockly/core';
|
||||
import PythonShell from './python-shell';
|
||||
|
||||
const NavExt = {};
|
||||
|
||||
NavExt.init = async function () {
|
||||
const nav = app.getNav();
|
||||
|
||||
nav.register({
|
||||
icon: 'icon-play-circled',
|
||||
title: '',
|
||||
id: 'python-run-btn',
|
||||
displayText: Blockly.Msg.MSG['run'],
|
||||
preconditionFn: () => {
|
||||
return true;
|
||||
},
|
||||
callback: () => {
|
||||
PythonShell.run();
|
||||
},
|
||||
scopeType: Nav.Scope.LEFT,
|
||||
weight: 4
|
||||
});
|
||||
|
||||
nav.register({
|
||||
icon: 'icon-cancel',
|
||||
title: '',
|
||||
id: 'python-stop-btn',
|
||||
displayText: Blockly.Msg.MSG['stop'],
|
||||
preconditionFn: () => {
|
||||
return true;
|
||||
},
|
||||
callback: () => {
|
||||
PythonShell.stop();
|
||||
},
|
||||
scopeType: Nav.Scope.LEFT,
|
||||
weight: 5
|
||||
});
|
||||
|
||||
await PythonShell.init();
|
||||
}
|
||||
|
||||
export default NavExt;
|
||||
213
boards/default_src/python_pyodide/others/python-shell.js
Normal file
213
boards/default_src/python_pyodide/others/python-shell.js
Normal file
@@ -0,0 +1,213 @@
|
||||
import * as path from 'path';
|
||||
// import * as dayjs from 'dayjs';
|
||||
import {
|
||||
Workspace,
|
||||
Debug,
|
||||
Env,
|
||||
Msg
|
||||
} from 'mixly';
|
||||
import { KernelLoader } from '@basthon/kernel-loader';
|
||||
import StatusBarImage from './statusbar-image';
|
||||
|
||||
class PythonShell {
|
||||
static {
|
||||
this.pythonShell = null;
|
||||
|
||||
this.init = async function () {
|
||||
StatusBarImage.init();
|
||||
const projectPath = path.relative(Env.indexDirPath, Env.boardDirPath);
|
||||
const loader = new KernelLoader({
|
||||
pyodideURLs: [path.join(projectPath, 'deps/0.62.21/python3/pyodide/pyodide.js')],
|
||||
rootPath: 'http://download.mixlylibs.cloud/web-python3-deps',
|
||||
language: 'python3',
|
||||
});
|
||||
|
||||
const kernel = await loader.kernelAvailable();
|
||||
if (!kernel) {
|
||||
return;
|
||||
}
|
||||
await kernel.init();
|
||||
await kernel.loaded();
|
||||
|
||||
this.loader = loader;
|
||||
this.kernel = kernel;
|
||||
this.pythonShell = new PythonShell();
|
||||
this.pyodide = window.pyodide;
|
||||
this.interruptBuffer = new Uint8Array(new ArrayBuffer(1));
|
||||
this.pyodide.setInterruptBuffer(this.interruptBuffer);
|
||||
}
|
||||
|
||||
this.run = function () {
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const editor = mainWorkspace.getEditorsManager().getActive();
|
||||
const code = editor.getCode();
|
||||
return this.pythonShell.run(code);
|
||||
}
|
||||
|
||||
this.stop = function () {
|
||||
return this.pythonShell.stop();
|
||||
}
|
||||
}
|
||||
|
||||
#statusBarTerminal_ = null;
|
||||
#statusBarImage_ = null;
|
||||
#statusBarsManager_ = null;
|
||||
#cursor_ = {
|
||||
row: 0,
|
||||
column: 0
|
||||
};
|
||||
#prompt_ = '';
|
||||
#inputResolve_ = null;
|
||||
#inputReject_ = null;
|
||||
#waittingForInput_ = false;
|
||||
#running_ = false;
|
||||
#kernel_ = null;
|
||||
#onCursorChangeEvent_ = () => this.#onCursorChange_();
|
||||
#commands_ = [
|
||||
{
|
||||
name: 'REPL-Enter',
|
||||
bindKey: 'Enter',
|
||||
exec: (editor) => {
|
||||
const session = editor.getSession();
|
||||
const cursor = session.selection.getCursor();
|
||||
if (cursor.row === this.#cursor_.row) {
|
||||
const newPos = this.#statusBarTerminal_.getEndPos();
|
||||
let str = this.#statusBarTerminal_.getValueRange(this.#cursor_, newPos);
|
||||
str = str.replace(this.#prompt_, '');
|
||||
this.#inputResolve_?.(str);
|
||||
this.#inputResolve_ = null;
|
||||
this.#inputReject_ = null;
|
||||
this.#statusBarTerminal_.addValue('\n');
|
||||
this.#exitInput_();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, {
|
||||
name: 'REPL-ChangeEditor',
|
||||
bindKey: 'Delete|Ctrl-X|Backspace',
|
||||
exec: (editor) => {
|
||||
const session = editor.getSession();
|
||||
const cursor = session.selection.getCursor();
|
||||
if (cursor.row < this.#cursor_.row || cursor.column <= this.#cursor_.column) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
constructor() {
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
this.#statusBarsManager_ = mainWorkspace.getStatusBarsManager();
|
||||
this.#statusBarTerminal_ = this.#statusBarsManager_.getStatusBarById('output');
|
||||
this.#statusBarImage_ = this.#statusBarsManager_.getStatusBarById('images');
|
||||
this.#kernel_ = PythonShell.kernel;
|
||||
this.#addEventsListener_();
|
||||
}
|
||||
|
||||
#addEventsListener_() {
|
||||
this.#kernel_.addEventListener('eval.finished', () => {
|
||||
this.#running_ = false;
|
||||
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}==`);
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.output', (data) => {
|
||||
this.#statusBarTerminal_.addValue(data.content);
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.error', () => {
|
||||
this.#running_ = false;
|
||||
this.#statusBarTerminal_.addValue(`\n==${Msg.Lang['shell.finish']}==`);
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.input', (data) => {
|
||||
const prompt = String(data?.content?.prompt);
|
||||
this.#statusBarTerminal_.addValue(prompt);
|
||||
this.#prompt_ = prompt;
|
||||
this.#inputResolve_ = data.resolve;
|
||||
this.#inputReject_ = data.reject;
|
||||
this.#enterInput_();
|
||||
});
|
||||
|
||||
this.#kernel_.addEventListener('eval.display', (data) => {
|
||||
this.#statusBarsManager_.changeTo('images');
|
||||
this.#statusBarImage_.display(data);
|
||||
});
|
||||
}
|
||||
|
||||
#onCursorChange_() {
|
||||
const editor = this.#statusBarTerminal_.getEditor();
|
||||
const session = editor.getSession();
|
||||
const cursor = session.selection.getCursor();
|
||||
editor.setReadOnly(
|
||||
cursor.row < this.#cursor_.row || cursor.column < this.#cursor_.column
|
||||
);
|
||||
}
|
||||
|
||||
#enterInput_() {
|
||||
if (!this.#running_) {
|
||||
return;
|
||||
}
|
||||
this.#waittingForInput_ = true;
|
||||
this.#cursor_ = this.#statusBarTerminal_.getEndPos();
|
||||
const editor = this.#statusBarTerminal_.getEditor();
|
||||
editor.setReadOnly(false);
|
||||
editor.focus();
|
||||
const session = editor.getSession();
|
||||
session.selection.on('changeCursor', this.#onCursorChangeEvent_);
|
||||
editor.commands.addCommands(this.#commands_);
|
||||
}
|
||||
|
||||
#exitInput_() {
|
||||
this.#waittingForInput_ = false;
|
||||
const editor = this.#statusBarTerminal_.getEditor();
|
||||
const session = editor.getSession();
|
||||
session.selection.off('changeCursor', this.#onCursorChangeEvent_);
|
||||
editor.commands.removeCommands(this.#commands_);
|
||||
this.#prompt_ = '';
|
||||
this.#inputResolve_?.('');
|
||||
// this.#inputReject_?.({});
|
||||
this.cursor_ = { row: 0, column: 0 };
|
||||
editor.setReadOnly(true);
|
||||
}
|
||||
|
||||
run(code) {
|
||||
this.stop()
|
||||
.then(() => {
|
||||
this.#statusBarsManager_.changeTo('output');
|
||||
this.#statusBarsManager_.show();
|
||||
this.#statusBarTerminal_.setValue(`${Msg.Lang['shell.running']}...\n`);
|
||||
this.#running_ = true;
|
||||
this.#kernel_.dispatchEvent('eval.request', {
|
||||
code,
|
||||
interactive: false,
|
||||
});
|
||||
})
|
||||
.catch(Debug.error);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this.#waittingForInput_) {
|
||||
this.#exitInput_();
|
||||
}
|
||||
if (this.#running_) {
|
||||
const timeout = 5;
|
||||
PythonShell.interruptBuffer[0] = 2;
|
||||
const startTime = Number(new Date());
|
||||
while (Number(new Date()) - startTime < timeout * 1000) {
|
||||
if (this.#running_) {
|
||||
PythonShell.interruptBuffer[0] = 2;
|
||||
await this.sleep(100);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.#running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
export default PythonShell;
|
||||
169
boards/default_src/python_pyodide/others/statusbar-image.js
Normal file
169
boards/default_src/python_pyodide/others/statusbar-image.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import * as Blockly from 'blockly/core';
|
||||
import {
|
||||
PageBase,
|
||||
HTMLTemplate,
|
||||
StatusBarsManager,
|
||||
Workspace
|
||||
} from 'mixly';
|
||||
import $ from 'jquery';
|
||||
import '../language/loader';
|
||||
import STATUS_BAR_IMAGE_TEMPLATE from '../templates/html/statusbar-image.html';
|
||||
|
||||
|
||||
class StatusBarImage extends PageBase {
|
||||
static {
|
||||
HTMLTemplate.add(
|
||||
'html/statusbar/statusbar-image.html',
|
||||
new HTMLTemplate(STATUS_BAR_IMAGE_TEMPLATE)
|
||||
);
|
||||
|
||||
this.init = function () {
|
||||
StatusBarsManager.typesRegistry.register(['images'], StatusBarImage);
|
||||
const mainWorkspace = Workspace.getMain();
|
||||
const statusBarsManager = mainWorkspace.getStatusBarsManager();
|
||||
statusBarsManager.add('images', 'images', Blockly.Msg.PYTHON_PYODIDE_IMAGE);
|
||||
statusBarsManager.changeTo('output');
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const $content = $(HTMLTemplate.get('html/statusbar/statusbar-image.html').render());
|
||||
this.setContent($content);
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
this.hideCloseBtn();
|
||||
}
|
||||
|
||||
clean() {
|
||||
this.getContent().empty();
|
||||
}
|
||||
|
||||
display(data) {
|
||||
const $content = this.getContent();
|
||||
const autoFit = function (node) {
|
||||
node.style.width = 'auto';
|
||||
node.style.height = 'auto';
|
||||
node.style.maxWidth = '100%';
|
||||
node.style.maxHeight = '100%';
|
||||
};
|
||||
this.clean();
|
||||
let root = data.content;
|
||||
let canvas = null;
|
||||
let iframe = null;
|
||||
switch (data.display_type) {
|
||||
case 'p5':
|
||||
root.style.width = '100%';
|
||||
root.style.height = '100%';
|
||||
root.style.display = 'flex';
|
||||
root.style.justifyContent = 'center';
|
||||
root.style.alignItems = 'center';
|
||||
|
||||
// some canvas nodes can be added later so we observe...
|
||||
new MutationObserver(function (mutationsList) {
|
||||
mutationsList.forEach((mutation) =>
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
const elem = node;
|
||||
if (
|
||||
elem.tagName != null &&
|
||||
['canvas', 'video'].includes(elem.tagName.toLowerCase())
|
||||
)
|
||||
autoFit(elem);
|
||||
})
|
||||
);
|
||||
}).observe(root, { childList: true });
|
||||
|
||||
root.querySelectorAll('canvas,video').forEach(autoFit);
|
||||
$content.append(root);
|
||||
break;
|
||||
case 'matplotlib':
|
||||
canvas = root.querySelector('canvas');
|
||||
if (canvas) root = canvas;
|
||||
root.style.width = '';
|
||||
root.style.height = '';
|
||||
root.style.maxWidth = '100%';
|
||||
root.style.maxHeight = '100%';
|
||||
$content.append(root);
|
||||
break;
|
||||
case 'ocaml-canvas':
|
||||
root.style.width = '';
|
||||
root.style.height = '';
|
||||
root.style.maxWidth = '100%';
|
||||
root.style.maxHeight = '100%';
|
||||
$content.append(root);
|
||||
break;
|
||||
case 'turtle':
|
||||
// Turtle result
|
||||
root.setAttribute('width', '100%');
|
||||
root.setAttribute('height', '100%');
|
||||
$content.append(root.outerHTML);
|
||||
break;
|
||||
case 'sympy':
|
||||
$content.append(data.content);
|
||||
if (typeof window.MathJax === 'undefined') {
|
||||
// dynamically loading MathJax
|
||||
console.log('Loading MathJax (Sympy expression needs it).');
|
||||
(function () {
|
||||
let script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src =
|
||||
'https://cdn.jsdelivr.net/npm/mathjax@3.0.5/es5/tex-mml-chtml.js';
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
})();
|
||||
} else {
|
||||
// otherwise, render it
|
||||
window.MathJax.typeset();
|
||||
}
|
||||
break;
|
||||
case 'multiple':
|
||||
/* typically dispached by display() */
|
||||
for (let mime of [
|
||||
'image/svg+xml',
|
||||
'image/png',
|
||||
'text/html',
|
||||
'text/plain',
|
||||
]) {
|
||||
if (mime in data.content) {
|
||||
let content = data.content[mime];
|
||||
if (mime === 'image/png') {
|
||||
content =
|
||||
'<img src="data:image/png;base64,' +
|
||||
content +
|
||||
'" style="max-width: 100%; max-height: 100%;">';
|
||||
}
|
||||
$content.append(content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'tutor':
|
||||
// hacky but iframe.document.body.style require to wait for
|
||||
// iframe loading
|
||||
$content.append($(data.content.replace('overflow-y%3A%20hidden%3B', '')));
|
||||
iframe = this.getContent()[0].getElementsByTagName('iframe')[0];
|
||||
if (iframe == null) return;
|
||||
// trick to avoid taking height update into account
|
||||
iframe.style.maxHeight = iframe.style.minHeight = '100%';
|
||||
|
||||
// force rendering when visible,
|
||||
// otherwise, strange things happends
|
||||
// since PythonTutor check for visibility at some point
|
||||
new IntersectionObserver((entries, observer) => {
|
||||
const entry = entries[0];
|
||||
if (entry && !entry.isIntersecting) return;
|
||||
iframe.contentWindow?.postMessage({ type: 'redraw' }, '*');
|
||||
observer.disconnect();
|
||||
}).observe(iframe);
|
||||
|
||||
break;
|
||||
default:
|
||||
console.error(
|
||||
`Not supported node type '${data.display_type}' in eval.display result processing.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StatusBarImage;
|
||||
Reference in New Issue
Block a user