better drag and drop, escaping for js, load indication
This commit is contained in:
parent
ac29780e6b
commit
7f0b77327e
2 changed files with 138 additions and 56 deletions
|
@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
// 2) Convert tabs → a run of non-breaking spaces
|
||||
const nbspTab = " ".repeat(tabWidth);
|
||||
s = s.replace(/\t/g, nbspTab);
|
||||
|
||||
// 3) Optionally convert every single space →
|
||||
if (spaces) {
|
||||
s = s.replace(/ /g, " ");
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue