/
githubmirror
/
cpython
Обзор
Документация
Войти
/
githubmirror
/
cpython
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
Platforms/emscripten/web_example/index.html
703 строки
28 KB
Stan Ulbrych
gh-148788: Update Emscripten example post move to Platforms dir (#148761)
20 апр 2026, 05:17
Не верифицирован
20 апр 2026, 05:17
a00b24e
Код
Авторство
О чём код?
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="author" content="Katie Bell, Adam Hartz"> <meta name="description" content="Simple REPL for Python WASM"> <title>wasm-python terminal</title> <link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd" > <style> body { font-family: arial; max-width: 800px; margin: 0 auto; } #editor { padding: 5px; border: 1px solid black; width: 100%; height: 300px; } #info { padding-top: 20px; } .error { border: 1px solid red; background-color: #ffd9d9; padding: 5px; margin-top: 20px; } .button-container { display: flex; justify-content: end; height: 50px; align-items: center; gap: 10px; } button { padding: 6px 18px; } </style> <script src="https://unpkg.com/xterm@4.18.0/lib/xterm.js" crossorigin integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+" ></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.1/ace.js" crossorigin integrity="sha512-kmA5vhcxOkZI0ReiKJMGNb8/KKbgbExIlnt6aXuPtl86AgHBEi6OHHOz2wsTazBDGZKxe7fmiE+pIuZJQks4+A==" ></script> <script type="module"> const _magic_ctrlc_string = "__WASM_REPL_CTRLC_" + (Date.now()) + "__"; class WorkerManager { constructor( workerURL, standardIO, readyCallBack, finishedCallback, ) { this.workerURL = workerURL; this.worker = null; this.standardIO = standardIO; this.readyCallBack = readyCallBack; this.finishedCallback = finishedCallback; this.initialiseWorker(); } async initialiseWorker() { if (!this.worker) { this.worker = new Worker(this.workerURL, { type: "module", }); this.worker.addEventListener( "message", this.handleMessageFromWorker, ); } } async run(options) { this.worker.postMessage({ type: "run", args: options.args || [], files: options.files || {}, }); } reset() { if (this.worker) { this.worker.terminate(); this.worker = null; } this.standardIO.message("Worker process terminated."); this.initialiseWorker(); } handleStdinData(inputValue) { if (this.stdinbuffer && this.stdinbufferInt) { let startingIndex = 1; if (this.stdinbufferInt[0] > 0) { startingIndex = this.stdinbufferInt[0]; } const data = new TextEncoder().encode(inputValue); data.forEach((value, index) => { this.stdinbufferInt[startingIndex + index] = value; }); this.stdinbufferInt[0] = startingIndex + data.length - 1; Atomics.notify(this.stdinbufferInt, 0, 1); } } handleMessageFromWorker = (event) => { const type = event.data.type; if (type === "ready") { this.readyCallBack(); } else if (type === "stdout") { this.standardIO.stdout(event.data.stdout); } else if (type === "stderr") { this.standardIO.stderr(event.data.stderr); } else if (type === "stdin") { // Leave it to the terminal to decide whether to chunk it into lines // or send characters depending on the use case. this.stdinbuffer = event.data.buffer; this.stdinbufferInt = new Int32Array(this.stdinbuffer); this.standardIO.stdin().then((inputValue) => { this.handleStdinData(inputValue); }); } else if (type === "finished") { this.standardIO.message( `Exited with status: ${event.data.returnCode}`, ); this.finishedCallback(); } }; } class WasmTerminal { constructor() { try { this.history = JSON.parse(sessionStorage.getItem('__python_wasm_repl.history')); this.historyBuffer = this.history.slice(); } catch(e) { this.history = []; this.historyBuffer = []; } this.reset(); this.xterm = new Terminal({ scrollback: 10000, fontSize: 14, theme: { background: "#1a1c1f" }, cols: 100, }); this.xterm.onKey((keyEvent) => { // Fix for iOS Keyboard Jumping on space if (keyEvent.key === " ") { keyEvent.domEvent.preventDefault(); } }); this.xterm.onData(this.handleTermData); } reset() { this.inputBuffer = new BufferQueue(); this.input = ""; this.resolveInput = null; this.activeInput = false; this.inputStartCursor = null; this.cursorPosition = 0; this.historyIndex = -1; this.beforeHistoryNav = ""; } open(container) { this.xterm.open(container); } handleTermData = (data) => { const ord = data.charCodeAt(0); data = data.replace(/\r(?!\n)/g, "\n"); // Convert lone CRs to LF // Handle pasted data if (data.length > 1 && data.includes("\n")) { let alreadyWrittenChars = 0; // If line already had data on it, merge pasted data with it if (this.input != "") { this.inputBuffer.addData(this.input); alreadyWrittenChars = this.input.length; this.input = ""; } this.inputBuffer.addData(data); // If input is active, write the first line if (this.activeInput) { let line = this.inputBuffer.nextLine(); this.writeLine(line.slice(alreadyWrittenChars)); this.resolveInput(line); this.activeInput = false; } // When input isn't active, add to line buffer } else if (!this.activeInput) { // Skip non-printable characters if (!(ord === 0x1b || ord == 0x7f || ord < 32)) { this.inputBuffer.addData(data); } // TODO: Handle more escape sequences? } else if (ord === 0x1b) { // Handle special characters switch (data.slice(1)) { case "[A": // up this.historyBack(); break; case "[B": // down this.historyForward(); break; case "[C": // right this.cursorRight(); break; case "[D": // left this.cursorLeft(); break; case "[H": // home key this.cursorHome(true); break; case "[F": // end key this.cursorEnd(true); break; case "[3~": // delete key this.deleteAtCursor(); break; default: break; } } else if (ord < 32 || ord === 0x7f) { switch (data) { case "\x0c": // CTRL+L this.clear(); break; case "\n": // ENTER case "\x0a": // CTRL+J case "\x0d": // CTRL+M this.resolveInput( this.input + this.writeLine("\n"), ); this.input = ""; this.cursorPosition = 0; this.activeInput = false; break; case "\x03": // CTRL+C this.input = ""; this.cursorPosition = 0; this.historyIndex = -1; this.resolveInput(_magic_ctrlc_string + "\n"); break; case "\x09": // TAB this.handleTab(); break; case "\x7F": // BACKSPACE case "\x08": // CTRL+H this.handleCursorErase(true); break; case "\x04": // CTRL+D // Send empty input if (this.input === "") { this.resolveInput(""); this.cursorPosition = 0; this.activeInput = false; } } } else { this.handleCursorInsert(data); this.updateHistory(); } }; clearLine() { this.xterm.write("\x1b[K"); } writeLine(line) { this.xterm.write(line.slice(0, -1)); this.xterm.write("\r\n"); return line; } handleCursorInsert(data) { const trailing = this.input.slice(this.cursorPosition); this.input = this.input.slice(0, this.cursorPosition) + data + trailing; this.cursorPosition += data.length; this.xterm.write(data); if (trailing.length !== 0) { this.xterm.write(trailing); this.xterm.write("\x1b[" + trailing.length + "D"); } this.updateHistory(); } handleTab() { // handle tabs: from the current position, add spaces until // this.cursorPosition is a multiple of 4. const prefix = this.input.slice(0, this.cursorPosition); const suffix = this.input.slice(this.cursorPosition); const count = 4 - (this.cursorPosition % 4); const toAdd = " ".repeat(count); this.input = prefix + toAdd + suffix; this.cursorHome(false); this.clearLine(); this.xterm.write(this.input); if (suffix) { this.xterm.write("\x1b[" + suffix.length + "D"); } this.cursorPosition += count; this.updateHistory(); } handleCursorErase() { // Don't delete past the start of input if ( this.xterm.buffer.active.cursorX <= this.inputStartCursor ) { return; } const trailing = this.input.slice(this.cursorPosition); this.input = this.input.slice(0, this.cursorPosition - 1) + trailing; this.cursorLeft(); this.clearLine(); if (trailing.length !== 0) { this.xterm.write(trailing); this.xterm.write("\x1b[" + trailing.length + "D"); } this.updateHistory(); } deleteAtCursor() { if (this.cursorPosition < this.input.length) { const trailing = this.input.slice( this.cursorPosition + 1, ); this.input = this.input.slice(0, this.cursorPosition) + trailing; this.clearLine(); if (trailing.length !== 0) { this.xterm.write(trailing); this.xterm.write("\x1b[" + trailing.length + "D"); } this.updateHistory(); } } cursorRight() { if (this.cursorPosition < this.input.length) { this.cursorPosition += 1; this.xterm.write("\x1b[C"); } } cursorLeft() { if (this.cursorPosition > 0) { this.cursorPosition -= 1; this.xterm.write("\x1b[D"); } } cursorHome(updatePosition) { if (this.cursorPosition > 0) { this.xterm.write("\x1b[" + this.cursorPosition + "D"); if (updatePosition) { this.cursorPosition = 0; } } } cursorEnd() { if (this.cursorPosition < this.input.length) { this.xterm.write( "\x1b[" + (this.input.length - this.cursorPosition) + "C", ); this.cursorPosition = this.input.length; } } updateHistory() { if (this.historyIndex !== -1) { this.historyBuffer[this.historyIndex] = this.input; } else { this.beforeHistoryNav = this.input; } } historyBack() { if (this.history.length === 0) { return; } else if (this.historyIndex === -1) { // we're not currently navigating the history; store // the current command and then look at the end of our // history buffer this.beforeHistoryNav = this.input; this.historyIndex = this.history.length - 1; } else if (this.historyIndex > 0) { this.historyIndex -= 1; } this.input = this.historyBuffer[this.historyIndex]; this.cursorHome(false); this.clearLine(); this.xterm.write(this.input); this.cursorPosition = this.input.length; } historyForward() { if (this.history.length === 0 || this.historyIndex === -1) { // we're not currently navigating the history; NOP. return; } else if (this.historyIndex < this.history.length - 1) { this.historyIndex += 1; this.input = this.historyBuffer[this.historyIndex]; } else if (this.historyIndex == this.history.length - 1) { // we're coming back from the last history value; reset // the input to whatever it was when we started going // through the history this.input = this.beforeHistoryNav; this.historyIndex = -1; } this.cursorHome(false); this.clearLine(); this.xterm.write(this.input); this.cursorPosition = this.input.length; } prompt = async () => { this.activeInput = true; // Hack to allow stdout/stderr to finish before we figure out where input starts setTimeout(() => { this.inputStartCursor = this.xterm.buffer.active.cursorX; }, 1); // If line buffer has a line ready, send it immediately if (this.inputBuffer.hasLineReady()) { return new Promise((resolve, reject) => { resolve( this.writeLine(this.inputBuffer.nextLine()), ); this.activeInput = false; }); // If line buffer has an incomplete line, use it for the active line } else if (this.inputBuffer.lastLineIsIncomplete()) { // Hack to ensure cursor input start doesn't end up after user input setTimeout(() => { this.handleCursorInsert( this.inputBuffer.nextLine() ); }, 1); } return new Promise((resolve, reject) => { this.resolveInput = (value) => { if ( value.replace(/\s/g, "").length != 0 && value != _magic_ctrlc_string + "\n" ) { if (this.historyIndex !== -1) { this.historyBuffer[this.historyIndex] = this.history[this.historyIndex]; } this.history.push(value.slice(0, -1)); this.historyBuffer.push(value.slice(0, -1)); this.historyIndex = -1; this.cursorPosition = 0; try { sessionStorage.setItem('__python_wasm_repl.history', JSON.stringify(this.history)); } catch(e) { } } resolve(value); }; }); }; clear() { this.xterm.clear(); } print(charCode) { let array = [charCode]; if (charCode == 10) { array = [13, 10]; // Replace \n with \r\n } this.xterm.write(new Uint8Array(array)); } } class BufferQueue { constructor(xterm) { this.buffer = []; } isEmpty() { return this.buffer.length == 0; } lastLineIsIncomplete() { return ( !this.isEmpty() && !this.buffer[this.buffer.length - 1].endsWith("\n") ); } hasLineReady() { return !this.isEmpty() && this.buffer[0].endsWith("\n"); } addData(data) { let lines = data.match(/.*(\n|$)/g); if (this.lastLineIsIncomplete()) { this.buffer[this.buffer.length - 1] += lines.shift(); } for (let line of lines) { this.buffer.push(line); } } nextLine() { return this.buffer.shift(); } } const runButton = document.getElementById("run"); const replButton = document.getElementById("repl"); const stopButton = document.getElementById("stop"); const clearButton = document.getElementById("clear"); window.onload = () => { const terminal = new WasmTerminal(); terminal.open(document.getElementById("terminal")); const stdio = { stdout: (charCode) => { terminal.print(charCode); }, stderr: (charCode) => { terminal.print(charCode); }, stdin: async () => { return await terminal.prompt(); }, message: (text) => { terminal.writeLine(`\r\n${text}\r\n`); }, }; const programRunning = (isRunning) => { if (isRunning) { replButton.setAttribute("disabled", true); runButton.setAttribute("disabled", true); stopButton.removeAttribute("disabled"); } else { replButton.removeAttribute("disabled"); runButton.removeAttribute("disabled"); stopButton.setAttribute("disabled", true); } }; runButton.addEventListener("click", (e) => { terminal.clear(); terminal.reset(); // reset the history programRunning(true); const code = editor.getValue(); pythonWorkerManager.run({ args: ["main.py"], files: { "main.py": code }, }); }); replButton.addEventListener("click", (e) => { terminal.clear(); terminal.reset(); // reset the history const REPL = ` class WASMREPLKeyboardInterrupt(KeyboardInterrupt): pass import sys import code import builtins def _interrupt_aware_input(prompt=''): line = builtins.input(prompt) if line.strip() == "${_magic_ctrlc_string}": raise KeyboardInterrupt() return line cprt = 'Type "help", "copyright", "credits" or "license" for more information.' banner = f'Python {sys.version} on {sys.platform}\\n{cprt}' code.interact(banner=banner, readfunc=_interrupt_aware_input, exitmsg='') `; programRunning(true); pythonWorkerManager.run({ args: ["-c", REPL], files: {} }); }); stopButton.addEventListener("click", (e) => { programRunning(false); pythonWorkerManager.reset(); }); clearButton.addEventListener("click", (e) => { terminal.clear(); }); const readyCallback = () => { replButton.removeAttribute("disabled"); runButton.removeAttribute("disabled"); clearButton.removeAttribute("disabled"); }; const finishedCallback = () => { programRunning(false); pythonWorkerManager.reset(); }; const pythonWorkerManager = new WorkerManager( "./python.worker.mjs", stdio, readyCallback, finishedCallback, ); }; var editor; document.addEventListener("DOMContentLoaded", () => { editor = ace.edit("editor"); editor.session.setMode("ace/mode/python"); }); </script> </head> <body> <div id="repldemo"> <h1>Simple REPL for Python WASM</h1> <div id="editor">print('Welcome to WASM!')</div> <div class="button-container"> <button id="run" disabled>Run code</button> <button id="repl" disabled>Start REPL</button> <button id="stop" disabled>Stop</button> <button id="clear" disabled>Clear</button> </div> <div id="terminal"></div> <div id="info"> The simple REPL provides a limited Python experience in the browser. <a href="https://github.com/python/cpython/blob/main/Platforms/emscripten/README.md" > Platforms/emscripten/README.md </a> contains a list of known limitations and issues. Networking, subprocesses, and threading are not available. </div> </div> <div id="buffererror" class="error" style="display: none"> <p> <code>SharedArrayBuffer</code>, which is required for this demo, is not available in your browser environment. One common cause of this failure is loading <code>index.html</code> directly in your browser instead of using <code>server.py</code> as described in <a href="https://github.com/python/cpython/blob/main/Platforms/emscripten/README.md#the-web-example" > Platforms/emscripten/README.md </a>. </p> <p> For more details about security requirements for <code>SharedArrayBuffer</code>, see <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer#security_requirements" >this MDN page</a >. </p> <script> if (typeof SharedArrayBuffer === 'undefined') { document.getElementById('repldemo').style.display = 'none'; document.getElementById('buffererror').style.display = 'block'; } </script> </div> </body> </html>