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