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