Refactor JavaScript code

This commit is contained in:
Konrad Borowski 2019-09-22 00:01:42 +02:00
parent 9bde4e5039
commit fce86e502d
10 changed files with 293 additions and 177 deletions

15
js/cache.js Normal file
View File

@ -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
}
}

42
js/codemirror-editor.js Normal file
View File

@ -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)
}

97
js/editor.js Normal file
View File

@ -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)
}

10
js/get-language.js Normal file
View File

@ -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)
}

View File

@ -1,174 +1,6 @@
import CodeMirror from 'codemirror'
import createEditor from './editor'
// Essentially <noscript>, but also working for ancient web browsers
// not supporting ES6 used by this script.
for (const element of document.querySelectorAll('[data-autohide]')) {
element.style.display = 'none'
}
for (const element of document.querySelectorAll('[data-autodisable]')) {
element.disabled = true
}
const editor = CodeMirror.fromTextArea(document.getElementById('code'), {
lineNumbers: true,
matchBrackets: true,
lineWrapping: true,
viewportMargin: Infinity,
minLines: 40,
})
const language = document.getElementById('language')
const futures = new Map()
function fetchLanguage(id) {
if (futures.has(id)) {
return futures.get(id)
}
const future = fetch(`/api/v0/language/${id}`).then(x => x.json())
futures.set(id, future)
return future
}
const wrapperButtons = document.getElementById('wrapper-buttons')
const selector = document.createElement('span')
const compilerOptions = document.createElement('input')
compilerOptions.placeholder = 'Compiler options'
compilerOptions.style.display = 'none'
const buttons = document.createElement('span')
wrapperButtons.append(selector, compilerOptions, buttons)
const filterAsm = document.createElement('label')
const filterAsmCheckbox = document.createElement('input')
filterAsmCheckbox.type = 'checkbox'
filterAsmCheckbox.checked = true
filterAsm.append(' ', filterAsmCheckbox, ' Filter assembler directives')
const filterRegex = /(?:\t\.(?:text|file|section|globl|p2align|type|cfi_.*|size|section)\b|.Lfunc_end).*\n?/g
let abortEval = new AbortController
async function updateLanguage() {
const initialValue = language.selectedOptions[0].value
const { mime, mode, implementations } = await fetchLanguage(initialValue)
const isCorrectLanguage = () => initialValue === language.selectedOptions[0].value
if (isCorrectLanguage()) {
if (mode) {
import(`codemirror/mode/${mode}/${mode}.js`).then(() => {
if (isCorrectLanguage()) {
editor.setOption('mode', mime)
}
})
}
selector.textContent = ''
buttons.textContent = ''
if (implementations.length > 1) {
const select = document.createElement('select')
for (const { label, identifier, wrappers } of implementations) {
const option = document.createElement('option')
option.textContent = label
option.showButtons = () => addButtons(wrappers, buttons, `${initialValue}/${identifier}`)
select.append(option)
}
function updateButtons() {
select.selectedOptions[0].showButtons()
}
select.addEventListener('change', updateButtons)
updateButtons()
selector.append(select)
} else if (implementations.length === 1) {
addButtons(implementations[0].wrappers, buttons, `${initialValue}/${implementations[0].identifier}`)
}
compilerOptions.style.display = implementations.length ? 'inline' : 'none'
}
}
function addButtons(wrappers, buttons, prefix) {
buttons.textContent = ''
for (const { identifier, label, isAsm, isFormatter } of wrappers) {
const button = document.createElement('button')
button.textContent = label
button.addEventListener('click', async e => {
e.preventDefault()
const body = new URLSearchParams
body.append('compilerOptions', compilerOptions.value)
body.append('code', editor.getValue())
abortEval.abort()
abortEval = new AbortController
const parameters = {
method: 'POST',
body,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
signal: abortEval.signal,
}
const output = document.getElementById('output')
output.textContent = ''
let response
try {
response = await (await fetch(`/api/v0/run/${prefix}/${identifier}`, parameters)).json()
} catch (e) {
if (e.name != 'AbortError')
output.textContent = 'An error occured while running the code. Try again.'
throw e
}
const { status, stdout, stderr } = response
function updateStdout() {
stdoutElement.textContent = ''
if (stdout) {
if (isAsm && filterAsmCheckbox.checked) {
stdoutElement.textContent = stdout.replace(filterRegex, "")
} else {
stdoutElement.textContent = stdout
}
} else {
const italic = document.createElement('i')
italic.textContent = '(no output)'
stdoutElement.append(italic)
}
}
let stdoutElement
if (stderr) {
const stderrHeader = document.createElement('h2')
stderrHeader.textContent = 'Standard error'
const stderrElement = document.createElement('pre')
stderrElement.textContent = stderr
output.append(stderrHeader, stderrElement)
}
if (isFormatter) {
editor.setValue(stdout)
} else {
const stdoutHeader = document.createElement('div')
stdoutHeader.className = 'stdout-header'
const stdoutHeaderH2 = document.createElement('h2')
stdoutHeaderH2.textContent = 'Standard output'
if (status) {
stdoutHeaderH2.textContent += ` (exit code ${status})`
}
stdoutHeader.append(stdoutHeaderH2)
if (isAsm) {
stdoutHeader.append(filterAsm)
filterAsmCheckbox.onchange = updateStdout
}
stdoutElement = document.createElement('pre')
updateStdout()
output.append(stdoutHeader, stdoutElement)
}
})
buttons.appendChild(button)
}
}
language.addEventListener('change', updateLanguage)
updateLanguage()
// Restore to the main page state on any changes
language.addEventListener('change', restoreOriginalState)
editor.on('change', restoreOriginalState)
function restoreOriginalState() {
for (const element of document.querySelectorAll('[data-autohide]')) {
element.style.display = ''
}
for (const element of document.querySelectorAll('[data-autodisable]')) {
element.disabled = false
}
for (const element of document.querySelectorAll('[data-delete-on-modify]')) {
element.remove()
}
const editor = document.getElementById('editor')
if (editor !== null) {
createEditor(editor)
}

65
js/output.js Normal file
View File

@ -0,0 +1,65 @@
const filterRegex = /(?:\t\.(?:text|file|section|globl|p2align|type|cfi_.*|size|section)\b|.Lfunc_end).*\n?/g
export default class Output {
constructor(output) {
this.output = output
this.filterAsm = document.createElement('label')
this.filterAsmCheckbox = document.createElement('input')
this.filterAsmCheckbox.type = 'checkbox'
this.filterAsmCheckbox.checked = true
this.filterAsmCheckbox.addEventListener('change', () => this.update())
this.filterAsm.append(' ', this.filterAsmCheckbox, ' Filter assembler directives')
}
clear() {
this.output.textContent = ''
}
error() {
this.output.textContent = 'An error occured while running the code. Try again.'
}
display(wrapper, json) {
this.wrapper = wrapper
this.json = json
this.update()
}
update() {
const { stdout, stderr, status } = this.json
this.output.textContent = ''
if (stderr) {
const stderrHeader = document.createElement('h2')
stderrHeader.textContent = 'Standard error'
const stderrElement = document.createElement('pre')
stderrElement.textContent = stderr
this.output.append(stderrHeader, stderrElement)
}
if (!this.wrapper.isFormatter) {
const stdoutHeader = document.createElement('div')
stdoutHeader.className = 'stdout-header'
const stdoutHeaderH2 = document.createElement('h2')
stdoutHeaderH2.textContent = 'Standard output'
if (status) {
stdoutHeaderH2.textContent += ` (exit code ${status})`
}
stdoutHeader.append(stdoutHeaderH2)
if (this.wrapper.isAsm) {
stdoutHeader.append(this.filterAsm)
}
const stdoutElement = document.createElement('pre')
if (stdout) {
if (this.wrapper.isAsm && this.filterAsmCheckbox.checked) {
stdoutElement.textContent = stdout.replace(filterRegex, "")
} else {
stdoutElement.textContent = stdout
}
} else {
const italic = document.createElement('i')
italic.textContent = '(no output)'
stdoutElement.append(italic)
}
output.append(stdoutHeader, stdoutElement)
}
}
}

55
js/wrapper-buttons.js Normal file
View File

@ -0,0 +1,55 @@
export default class WrapperButtons {
constructor(buttonsContainer, run) {
this.buttonsContainer = buttonsContainer
this.compilerOptions = document.createElement('input')
this.compilerOptions.placeholder = 'Compiler options'
this.buttons = document.createElement('span')
this.run = run
this.abortController = null
}
update(implementations) {
this.clear()
this.select = document.createElement('select')
for (const { label, identifier, wrappers } of implementations) {
const option = document.createElement('option')
option.textContent = label
option.identifier = identifier
option.wrappers = wrappers
this.select.append(option)
}
this.buttonsContainer.textContent = ''
if (implementations.length > 1) {
this.buttonsContainer.append(this.select)
this.select.addEventListener('change', () => this.updateButtons())
}
if (implementations.length !== 0) {
this.buttonsContainer.append(this.compilerOptions, this.buttons)
}
this.updateButtons()
}
clear() {
this.buttonsContainer.textContent = ''
}
updateButtons() {
this.buttons.textContent = ''
let options = this.select.selectedOptions
if (options.length === 0) {
options = this.select.options
}
if (options.length !== 0) {
const option = options[0]
for (const wrapper of option.wrappers) {
const button = document.createElement('button')
button.textContent = wrapper.label
button.addEventListener('click', e => {
e.preventDefault()
this.run(option.identifier, wrapper, this.compilerOptions.value)
})
this.buttons.append(button)
}
}
}
}

View File

@ -1,5 +1,5 @@
@()
<p id="buttons">
<span id="wrapper-buttons"></span>
<input type=submit value=Share data-autodisable>
<input type=submit value=Share>
<div id="output"></div>

View File

@ -6,15 +6,15 @@
@:header()
@Html(paste.markdown)
<form method="post" action="/">
<form method="post" action="/" id="editor">
<p>
@:language_selection(selection)
@if let Some(delete_at) = paste.delete_at {
<span data-delete-on-modify>
<span id="autodelete-text">
This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC.
</span>
}
<label data-autohide>
<label id="automatically-hidden-label">
<input type=checkbox name=autodelete checked> Automatically delete after 24 hours
</label>
</p>

View File

@ -4,7 +4,7 @@
@(selection: Selection)
@:header()
<form method="post" action="/">
<form method="post" action="/" id="editor">
<p>
@:language_selection(selection)
<label>