better drag and drop, escaping for js, load indication

This commit is contained in:
ticvac 2025-05-07 15:27:29 +02:00
parent ac29780e6b
commit 7f0b77327e
2 changed files with 138 additions and 56 deletions

View file

@ -13,6 +13,8 @@
--primary-light: #6366f1;
--text: #374151;
--border: #e5e7eb;
--highlight: #e0e7ff;
--fade: rgba(0, 0, 0, 0.1);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); }
@ -20,7 +22,7 @@
h1 { text-align: center; margin-bottom: 1.5rem; font-size: 2rem; font-weight: 700; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1.5rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05); margin-bottom: 1.5rem; }
label { display: block; margin-bottom: 0.5rem; font-weight: 600; }
textarea, input[type="text"], input[type="file"] {
textarea, input[type="text"] {
width: 100%; padding: 0.75rem; border: 1px solid var(--border); border-radius: 0.375rem; font-family: monospace; font-size: 0.9rem; margin-bottom: 1rem;
}
textarea { resize: vertical; height: 200px; }
@ -28,11 +30,46 @@
display: inline-block; padding: 0.75rem 1.5rem; background: var(--primary); color: #fff; text-decoration: none;
border: none; border-radius: 0.375rem; font-weight: 600; cursor: pointer;
transition: background 0.2s ease;
margin-top: 1rem;
}
.btn:hover { background: var(--primary-light); }
.btn:hover:not([disabled]) { background: var(--primary-light); }
.btn[disabled] { opacity: 0.6; cursor: not-allowed; }
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #fff;
border-radius: 50%;
width: 16px;
height: 16px;
display: inline-block;
vertical-align: middle;
margin-right: 0.5rem;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.flex { display: flex; gap: 1rem; }
.flex .card { flex: 1; }
#output, #last_state { background: #1e293b; color: #d1d5db; height: 150px; overflow-y: auto; }
.flex > div { flex: 1; }
#output, #last_state { background: #1e293b; color: #d1d5db; height: 150px; overflow-y: auto; font-family: monospace; }
.drop-zone {
position: relative;
padding: 1rem;
border: 2px dashed var(--border);
border-radius: 0.375rem;
transition: background 0.2s ease, border-color 0.2s ease;
text-align: center;
}
.drop-zone.active {
background: var(--highlight);
border-color: var(--primary);
}
.drop-zone input {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%; opacity: 0;
cursor: pointer;
}
@media screen and (max-width: 768px) {
.flex { flex-direction: column; }
.flex > div { width: 100%; }
}
</style>
</head>
<body>
@ -42,13 +79,19 @@
<div class="flex">
<div>
<label for="code">Code</label>
<textarea id="code" placeholder="Enter Brainfuck code, upload .bf, .in, or .txt file..."></textarea>
<input type="file" id="code-file" accept=".bf, .in, .txt">
<textarea id="code" placeholder="Enter Brainfuck code..."></textarea>
<div id="code-drop" class="drop-zone">
<p id="code-drop-text">Drag & drop code file here or click to select</p>
<input type="file" id="code-file" accept=".bf, .in, .txt">
</div>
</div>
<div>
<label for="input">Input</label>
<textarea id="input" placeholder="Program input, upload .in or .txt..."></textarea>
<input type="file" id="input-file" accept=".txt, .in">
<textarea id="input" placeholder="Program input..."></textarea>
<div id="input-drop" class="drop-zone">
<p id="input-drop-text">Drag & drop input file here or click to select</p>
<input type="file" id="input-file" accept=".txt, .in">
</div>
</div>
</div>
<button type="button" id="run-btn" class="btn">Run</button>
@ -60,29 +103,50 @@
</div>
<div class="card">
<label for="last_state">Stav</label>
<label for="last_state">State</label>
<div id="last_state"></div>
</div>
</div>
<script>
// Load file into textarea
document.getElementById('code-file').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => document.getElementById('code').value = reader.result;
reader.readAsText(file);
});
document.getElementById('input-file').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => document.getElementById('input').value = reader.result;
reader.readAsText(file);
});
// Setup drop zones with filename display
function setupDropZone(dropZoneId, textareaId, textId) {
const dropZone = document.getElementById(dropZoneId);
const fileInput = dropZone.querySelector('input');
const textarea = document.getElementById(textareaId);
const dropText = document.getElementById(textId);
// Brainfuck Interpreter
['dragenter', 'dragover'].forEach(evt => {
dropZone.addEventListener(evt, e => {
e.preventDefault(); dropZone.classList.add('active');
});
});
['dragleave', 'drop'].forEach(evt => {
dropZone.addEventListener(evt, e => {
e.preventDefault(); dropZone.classList.remove('active');
});
});
dropZone.addEventListener('drop', e => {
const file = e.dataTransfer.files[0];
if (!file) return;
dropText.textContent = file.name;
const reader = new FileReader();
reader.onload = () => textarea.value = reader.result;
reader.readAsText(file);
});
fileInput.addEventListener('change', e => {
const file = e.target.files[0]; if (!file) return;
dropText.textContent = file.name;
const reader = new FileReader();
reader.onload = () => textarea.value = reader.result;
reader.readAsText(file);
});
}
setupDropZone('code-drop', 'code', 'code-drop-text');
setupDropZone('input-drop', 'input', 'input-drop-text');
// Brainfuck Interpreter unchanged
function runBrainfuck(code, input) {
const tape = Array(30000).fill(0);
let ptr = 0, inputPtr = 0, output = '';
@ -108,11 +172,8 @@
}
break;
case ']':
if (tape[ptr] !== 0) {
i = loopStack[loopStack.length - 1];
} else {
loopStack.pop();
}
if (tape[ptr] !== 0) { i = loopStack[loopStack.length - 1]; }
else { loopStack.pop(); }
break;
default: break;
}
@ -120,33 +181,55 @@
return output;
}
function formatForHTML(text, tabWidth = 4, spaces = true) {
// 1) Escape &, <, >, " and '
let s = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
// 2) Convert tabs → a run of non-breaking spaces
const nbspTab = "&nbsp;".repeat(tabWidth);
s = s.replace(/\t/g, nbspTab);
// 3) Optionally convert every single space → &nbsp;
if (spaces) {
s = s.replace(/ /g, "&nbsp;");
}
// 4) Convert newlines → <br>
s = s.replace(/\r?\n/g, "<br>\n");
return s;
}
// Run button handler with spinner prepend
document.getElementById('run-btn').addEventListener('click', () => {
const btn = document.getElementById('run-btn');
const code = document.getElementById('code').value;
const input = document.getElementById('input').value;
// const output = runBrainfuck(code, input);
// here compilation...
url = '/brainfuck/process_code?code=' + encodeURI(code) + "&user_input=" + encodeURI(input);
console.log(url);
console.log(encodeURI(code));
fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}).then(Response => Response.json())
.then(data => {
console.log(data);
document.getElementById('output').textContent = data.output;
document.getElementById('last_state').textContent = data.end_state;
})
if (btn.disabled) return;
btn.disabled = true;
const spinner = document.createElement('div'); spinner.className = 'spinner';
btn.insertBefore(spinner, btn.firstChild);
// fetch('/brainfuck/', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: body
// }).then(Response => Response.json())
// .then(data => {
// console.log(data);
// })
const url = '/brainfuck/process_code?code=' + encodeURIComponent(code) + "&user_input=" + encodeURIComponent(input);
fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
.then(response => response.json())
.then(data => {
// escape HTML entities in output
console.log(data);
document.getElementById('output').textContent = data.output;
document.getElementById('last_state').textContent = data.end_state;
document.getElementById('output').innerHTML = formatForHTML(data.output);
})
.catch(err => console.error(err))
.finally(() => {
btn.disabled = false;
btn.removeChild(spinner);
});
});
</script>
</body>

View file

@ -4,12 +4,10 @@ from .interpreter import Interpreter
from urllib.parse import parse_qsl, unquote_plus, unquote
from typing import Dict
def index(request):
args = {}
return TemplateResponse(request, 'brainfuck/index.html', args)
def process_code(request):
qs = request.META['QUERY_STRING']
params: Dict[str, str] = {}
@ -31,7 +29,7 @@ def process_code(request):
interpreter = Interpreter(commands_per_second=1_000_000_000_000, debug=True, comma_standart_input=False)
interpreter.user_input = input_data
interpreter.load_code_from_string(code)
# should be try/except?
# TODO kill the process if it takes too long?
try:
interpreter.execute_loaded_code()
out = interpreter.saved_output
@ -40,6 +38,7 @@ def process_code(request):
out = 'Error: Code execution failed'
end_state = interpreter.print_data(interpreter.last_step)
return JSONResponse({
'code': code,