import { basicSetup, EditorState, EditorView } from '@codemirror/basic-setup'; import { python } from '@codemirror/lang-python'; import { Compartment, StateCommand } from '@codemirror/state'; import { keymap } from '@codemirror/view'; import { defaultKeymap } from '@codemirror/commands'; import { oneDarkTheme } from '@codemirror/theme-one-dark'; import { componentDetailsNavOpen, loadedEnvironments, mode, pyodideLoaded } from '../stores'; import { addClasses, htmlDecode } from '../utils'; import { BaseEvalElement } from './base'; // Premise used to connect to the first available pyodide interpreter let pyodideReadyPromise; let environments; let currentMode; pyodideLoaded.subscribe(value => { pyodideReadyPromise = value; }); loadedEnvironments.subscribe(value => { environments = value; }); let propertiesNavOpen; componentDetailsNavOpen.subscribe(value => { propertiesNavOpen = value; }); mode.subscribe(value => { currentMode = value; }); 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(); return true; }; } let initialTheme: string; function getEditorTheme(el: BaseEvalElement): string { return initialTheme || (initialTheme = el.getAttribute('theme')); } export 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 = [ 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({ state: EditorState.create({ 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(); }); if (!this.id) { console.log( "WARNING: define with an id. should always have an id. More than one on a page won't work otherwise!", ); } if (!this.hasAttribute('exec-id')) { this.setAttribute('exec-id', '1'); } if (!this.hasAttribute('root')) { this.setAttribute('root', this.id); } if (this.hasAttribute('output')) { this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output')); } else { if (this.hasAttribute('std-out')) { this.outputElement = document.getElementById(this.getAttribute('std-out')); } 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; this.outputElement.id = this.id + '-' + this.getAttribute('exec-id'); // add the output div id if there's not output pre-defined mainDiv.appendChild(this.outputElement); } this.errorElement = this.hasAttribute('std-err') ? document.getElementById(this.getAttribute('std-err')) : this.outputElement; } this.appendChild(mainDiv); this.editor.focus(); console.log('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'); } if(this.hasAttribute('output-mode')) { newPyRepl.setAttribute('output-mode', this.getAttribute('output-mode')); } const addReplAttribute = (attribute: string) => { if (this.hasAttribute(attribute)) { newPyRepl.setAttribute(attribute, this.getAttribute(attribute)); } }; addReplAttribute('output'); addReplAttribute('std-out'); addReplAttribute('std-err'); newPyRepl.setAttribute('exec-id', nextExecId.toString()); this.parentElement.appendChild(newPyRepl); } } getSourceFromElement(): string { const sourceStrings = [ `output_manager.change(out="${this.outputElement.id}", append=True)`, ...this.editor.state.doc.toString().split('\n'), ]; return sourceStrings.join('\n'); } render() { console.log('rendered'); } }