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; --primary-light: #6366f1;
--text: #374151; --text: #374151;
--border: #e5e7eb; --border: #e5e7eb;
--highlight: #e0e7ff;
--fade: rgba(0, 0, 0, 0.1);
} }
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; background: var(--bg); color: var(--text); } 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; } 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; } .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; } 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; 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; } 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; 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; border: none; border-radius: 0.375rem; font-weight: 600; cursor: pointer;
transition: background 0.2s ease; 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 { display: flex; gap: 1rem; }
.flex .card { flex: 1; } .flex > div { flex: 1; }
#output, #last_state { background: #1e293b; color: #d1d5db; height: 150px; overflow-y: auto; } #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> </style>
</head> </head>
<body> <body>
@ -42,13 +79,19 @@
<div class="flex"> <div class="flex">
<div> <div>
<label for="code">Code</label> <label for="code">Code</label>
<textarea id="code" placeholder="Enter Brainfuck code, upload .bf, .in, or .txt file..."></textarea> <textarea id="code" placeholder="Enter Brainfuck code..."></textarea>
<input type="file" id="code-file" accept=".bf, .in, .txt"> <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>
<div> <div>
<label for="input">Input</label> <label for="input">Input</label>
<textarea id="input" placeholder="Program input, upload .in or .txt..."></textarea> <textarea id="input" placeholder="Program input..."></textarea>
<input type="file" id="input-file" accept=".txt, .in"> <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>
</div> </div>
<button type="button" id="run-btn" class="btn">Run</button> <button type="button" id="run-btn" class="btn">Run</button>
@ -60,29 +103,50 @@
</div> </div>
<div class="card"> <div class="card">
<label for="last_state">Stav</label> <label for="last_state">State</label>
<div id="last_state"></div> <div id="last_state"></div>
</div> </div>
</div> </div>
<script> <script>
// Load file into textarea // Setup drop zones with filename display
document.getElementById('code-file').addEventListener('change', function(e) { function setupDropZone(dropZoneId, textareaId, textId) {
const file = e.target.files[0]; const dropZone = document.getElementById(dropZoneId);
if (!file) return; const fileInput = dropZone.querySelector('input');
const reader = new FileReader(); const textarea = document.getElementById(textareaId);
reader.onload = () => document.getElementById('code').value = reader.result; const dropText = document.getElementById(textId);
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);
});
// 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) { function runBrainfuck(code, input) {
const tape = Array(30000).fill(0); const tape = Array(30000).fill(0);
let ptr = 0, inputPtr = 0, output = ''; let ptr = 0, inputPtr = 0, output = '';
@ -108,11 +172,8 @@
} }
break; break;
case ']': case ']':
if (tape[ptr] !== 0) { if (tape[ptr] !== 0) { i = loopStack[loopStack.length - 1]; }
i = loopStack[loopStack.length - 1]; else { loopStack.pop(); }
} else {
loopStack.pop();
}
break; break;
default: break; default: break;
} }
@ -120,33 +181,55 @@
return output; 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', () => { document.getElementById('run-btn').addEventListener('click', () => {
const btn = document.getElementById('run-btn');
const code = document.getElementById('code').value; const code = document.getElementById('code').value;
const input = document.getElementById('input').value; const input = document.getElementById('input').value;
// const output = runBrainfuck(code, input); if (btn.disabled) return;
// here compilation... btn.disabled = true;
url = '/brainfuck/process_code?code=' + encodeURI(code) + "&user_input=" + encodeURI(input); const spinner = document.createElement('div'); spinner.className = 'spinner';
console.log(url); btn.insertBefore(spinner, btn.firstChild);
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;
})
const url = '/brainfuck/process_code?code=' + encodeURIComponent(code) + "&user_input=" + encodeURIComponent(input);
// fetch('/brainfuck/', { fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } })
// method: 'POST', .then(response => response.json())
// headers: { 'Content-Type': 'application/json' }, .then(data => {
// body: body // escape HTML entities in output
// }).then(Response => Response.json()) console.log(data);
// .then(data => { document.getElementById('output').textContent = data.output;
// console.log(data); 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> </script>
</body> </body>

View file

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