diff --git a/js/cache.js b/js/cache.js new file mode 100644 index 0000000..41a6cfd --- /dev/null +++ b/js/cache.js @@ -0,0 +1,15 @@ +export default class Cache { + constructor(initializer) { + this.elems = new Map + this.initializer = initializer + } + + get(key) { + if (this.elems.has(key)) { + return this.elems.get(key) + } + const value = this.initializer(key) + this.elems.set(key, value) + return value + } +} diff --git a/js/codemirror-editor.js b/js/codemirror-editor.js new file mode 100644 index 0000000..db96235 --- /dev/null +++ b/js/codemirror-editor.js @@ -0,0 +1,42 @@ +import CodeMirror from 'codemirror' + +class CodeMirrorEditor { + constructor(editor) { + this.editor = editor + } + + async setLanguage({ mode, mime }) { + this.currentMime = mime + this.editor.setOption('mode', mime) + if (mode) { + await import(`codemirror/mode/${mode}/${mode}.js`) + if (this.currentMime === mime) { + this.editor.setOption('mode', mime) + } + } + } + + getValue() { + return this.editor.getValue() + } + + setValue(value) { + this.editor.setValue(value) + } + + onChange(callback) { + this.editor.on('change', callback) + } +} + +export default function createEditor(textarea, onChange) { + const editor = CodeMirror.fromTextArea(textarea, { + lineNumbers: true, + matchBrackets: true, + lineWrapping: true, + viewportMargin: Infinity, + minLines: 40, + }) + editor.on('change', onChange) + return new CodeMirrorEditor(editor) +} diff --git a/js/editor.js b/js/editor.js new file mode 100644 index 0000000..336ad32 --- /dev/null +++ b/js/editor.js @@ -0,0 +1,97 @@ +import getLanguage from './get-language' +import Output from './output' +import WrapperButtons from './wrapper-buttons' + +class Editor { + async initialize(form) { + this.languageSelector = form.querySelector('#language') + this.wrapperButtons = new WrapperButtons(form.querySelector('#wrapper-buttons'), this.run.bind(this)) + this.editor = (async () => { + const module = await import('./codemirror-editor') + return module.default(form.querySelector('#code'), () => this.changeToLookLikeNewPaste()) + })() + this.output = new Output(output) + this.autodeleteText = form.querySelector('#autodelete-text') + this.autodeleteCheckbox = form.querySelector('#automatically-hidden-label') + this.submit = form.querySelector('[type=submit]') + this.submit.disabled = true + if (this.autodeleteText) { + this.autodeleteCheckbox.style.display = 'none' + } + this.assignEvents() + this.updateLanguage() + } + + changeToLookLikeNewPaste() { + if (this.autodeleteText) { + this.autodeleteText.style.display = 'none' + this.autodeleteCheckbox.style.display = '' + } + this.submit.disabled = false + } + + assignEvents() { + this.languageSelector.addEventListener('change', () => { + this.updateLanguage() + this.changeToLookLikeNewPaste() + }) + } + + async updateLanguage() { + this.wrapperButtons.clear() + const identifier = this.getLanguageIdentifier() + const language = await getLanguage(identifier) + // This deals with user changing the language after asynchronous event + if (identifier === this.getLanguageIdentifier()) { + this.wrapperButtons.update(language.implementations) + const editor = await this.editor + if (identifier === this.getLanguageIdentifier()) { + editor.setLanguage(language) + } + } + } + + getLanguageIdentifier() { + return this.languageSelector.selectedOptions[0].value + } + + async run(implementationIdentifier, wrapper, compilerOptions) { + this.output.clear() + if (this.abortEval) { + this.abortEval.abort() + } + this.abortEval = new AbortController + const body = new URLSearchParams + body.append('compilerOptions', compilerOptions) + const editor = await this.editor + body.append('code', editor.getValue()) + const parameters = { + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + signal: this.abortEval.signal, + } + const languageIdentifier = this.getLanguageIdentifier() + const path = `/api/v0/run/${languageIdentifier}/${implementationIdentifier}/${wrapper.identifier}` + let response + try { + response = await (await fetch(path, parameters)).json() + } catch (e) { + if (e.name === 'AbortError') { + return + } + this.output.error() + throw e + } + if (wrapper.isFormatter) { + editor.setValue(response.stdout) + } + this.output.display(wrapper, response) + } +} + +export default function createEditor(form) { + return new Editor().initialize(form) +} diff --git a/js/get-language.js b/js/get-language.js new file mode 100644 index 0000000..17e626e --- /dev/null +++ b/js/get-language.js @@ -0,0 +1,10 @@ +import Cache from './cache' + +const languageCache = new Cache(async identifier => { + const response = await fetch(`/api/v0/language/${identifier}`) + return await response.json() +}) + +export default function getLanguage(identifier) { + return languageCache.get(identifier) +} diff --git a/js/index.js b/js/index.js index a7d6ad1..ff7a635 100644 --- a/js/index.js +++ b/js/index.js @@ -1,174 +1,6 @@ -import CodeMirror from 'codemirror' +import createEditor from './editor' -// Essentially