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;
|
--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, "&")
|
||||||
|
.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', () => {
|
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>
|
||||||
|
|
|
@ -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
|
||||||
|
@ -40,6 +38,7 @@ def process_code(request):
|
||||||
out = 'Error: Code execution failed'
|
out = 'Error: Code execution failed'
|
||||||
|
|
||||||
end_state = interpreter.print_data(interpreter.last_step)
|
end_state = interpreter.print_data(interpreter.last_step)
|
||||||
|
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
'code': code,
|
'code': code,
|
||||||
|
|
Loading…
Reference in a new issue