Refactor JavaScript code
This commit is contained in:
parent
9bde4e5039
commit
fce86e502d
15
js/cache.js
Normal file
15
js/cache.js
Normal 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
42
js/codemirror-editor.js
Normal 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
97
js/editor.js
Normal 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
10
js/get-language.js
Normal 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)
|
||||
}
|
176
js/index.js
176
js/index.js
|
@ -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
65
js/output.js
Normal 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
55
js/wrapper-buttons.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
@(selection: Selection)
|
||||
|
||||
@:header()
|
||||
<form method="post" action="/">
|
||||
<form method="post" action="/" id="editor">
|
||||
<p>
|
||||
@:language_selection(selection)
|
||||
<label>
|
||||
|
|
Loading…
Reference in New Issue
Block a user