import { basicSetup, EditorView } from 'codemirror'; import { python } from '@codemirror/lang-python'; import { indentUnit } from '@codemirror/language' import { Compartment, StateCommand } from '@codemirror/state'; import { keymap } from '@codemirror/view'; import { defaultKeymap } from '@codemirror/commands'; import { oneDarkTheme } from '@codemirror/theme-one-dark'; import { getAttribute, addClasses, htmlDecode } from '../utils'; import { BaseEvalElement } from './base'; import type { Runtime } from '../runtime'; import { getLogger } from '../logger'; const logger = getLogger('py-repl'); export function make_PyRepl(runtime: Runtime) { function createCmdHandler(el: PyRepl): StateCommand { // Creates a codemirror cmd handler that calls the el.evaluate when an event // triggers that specific cmd return () => { void el.evaluate(runtime); return true; }; } let initialTheme: string; function getEditorTheme(el: BaseEvalElement): string { const theme = getAttribute(el, 'theme'); if( !initialTheme && theme){ initialTheme = theme; } return initialTheme; } class PyRepl extends BaseEvalElement { editor: EditorView; editorNode: HTMLElement; constructor() { super(); // add an extra div where we can attach the codemirror editor this.editorNode = document.createElement('div'); addClasses(this.editorNode, ['editor-box']); this.shadow.appendChild(this.wrapper); } connectedCallback() { this.checkId(); this.code = htmlDecode(this.innerHTML); this.innerHTML = ''; const languageConf = new Compartment(); const extensions = [ indentUnit.of(" "), basicSetup, languageConf.of(python()), keymap.of([ ...defaultKeymap, { key: 'Ctrl-Enter', run: createCmdHandler(this) }, { key: 'Shift-Enter', run: createCmdHandler(this) }, ]), ]; if (getEditorTheme(this) === 'dark') { extensions.push(oneDarkTheme); } this.editor = new EditorView({ doc: this.code.trim(), extensions, parent: this.editorNode, }); const mainDiv = document.createElement('div'); addClasses(mainDiv, ['py-repl-box']); // Styles that we use to hide the labels whilst also keeping it accessible for screen readers const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px'; // Code editor Label this.editorNode.id = 'code-editor'; const editorLabel = document.createElement('label'); editorLabel.innerHTML = 'Python Script Area'; editorLabel.setAttribute('style', labelStyle); editorLabel.htmlFor = 'code-editor'; mainDiv.append(editorLabel); // add Editor to main PyScript div mainDiv.appendChild(this.editorNode); // Play Button this.btnRun = document.createElement('button'); this.btnRun.id = 'btnRun'; this.btnRun.innerHTML = ''; addClasses(this.btnRun, ['absolute', 'repl-play-button']); // Play Button Label const btnLabel = document.createElement('label'); btnLabel.innerHTML = 'Python Script Run Button'; btnLabel.setAttribute('style', labelStyle); btnLabel.htmlFor = 'btnRun'; this.editorNode.appendChild(btnLabel); this.editorNode.appendChild(this.btnRun); this.btnRun.addEventListener('click', () => { void this.evaluate(runtime); }); if (!this.id) { logger.warn( "WARNING: defined without an id. should always have an id, otherwise multiple in the same page will not work!" ); } if (!this.hasAttribute('exec-id')) { this.setAttribute('exec-id', '1'); } if (!this.hasAttribute('root')) { this.setAttribute('root', this.id); } const output = getAttribute(this, "output") if (output) { const el = document.getElementById(output); if(el){ this.errorElement = el; this.outputElement = el } } else { const stdOut = getAttribute(this, "std-out"); if (stdOut) { const el = document.getElementById(stdOut); if(el){ this.outputElement = el } } else { // In this case neither output or std-out have been provided so we need // to create a new output div to output to this.outputElement = document.createElement('div'); this.outputElement.classList.add('output'); this.outputElement.hidden = true; const stdOut = getAttribute(this, "exec-id") || ""; this.outputElement.id = this.id + '-' + stdOut; // add the output div id if there's not output pre-defined mainDiv.appendChild(this.outputElement); } const stdErr = getAttribute(this, "std-err"); if( stdErr ){ const el = document.getElementById(stdErr); if(el){ this.errorElement = el; }else{ this.errorElement = this.outputElement } }else{ this.errorElement = this.outputElement } } this.appendChild(mainDiv); this.editor.focus(); logger.debug(`element ${this.id} successfully connected`); } addToOutput(s: string): void { this.outputElement.innerHTML += '
' + s + '
'; this.outputElement.hidden = false; } preEvaluate(): void { this.setOutputMode("replace"); if(!this.appendOutput) { this.outputElement.innerHTML = ''; } } postEvaluate(): void { this.outputElement.hidden = false; this.outputElement.style.display = 'block'; if (this.hasAttribute('auto-generate')) { const allPyRepls = document.querySelectorAll(`py-repl[root='${this.getAttribute('root')}'][exec-id]`); const lastRepl = allPyRepls[allPyRepls.length - 1]; const lastExecId = lastRepl.getAttribute('exec-id'); const nextExecId = parseInt(lastExecId) + 1; const newPyRepl = document.createElement('py-repl'); newPyRepl.setAttribute('root', this.getAttribute('root')); newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString(); if(this.hasAttribute('auto-generate')) { newPyRepl.setAttribute('auto-generate', ''); this.removeAttribute('auto-generate'); } const outputMode = getAttribute( this, 'output-mode') if(outputMode) { newPyRepl.setAttribute('output-mode', outputMode); } const addReplAttribute = (attribute: string) => { const attr = getAttribute( this, attribute) if(attr) { newPyRepl.setAttribute(attribute, attr); } }; addReplAttribute('output'); addReplAttribute('std-out'); addReplAttribute('std-err'); newPyRepl.setAttribute('exec-id', nextExecId.toString()); if( this.parentElement ){ this.parentElement.appendChild(newPyRepl); } } } getSourceFromElement(): string { return this.editor.state.doc.toString(); } } return PyRepl }