diff options
Diffstat (limited to 'static/jscl-snippets.js')
| -rw-r--r-- | static/jscl-snippets.js | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/static/jscl-snippets.js b/static/jscl-snippets.js new file mode 100644 index 0000000..2240528 --- /dev/null +++ b/static/jscl-snippets.js @@ -0,0 +1,319 @@ +// Common Lisp Snippet Executor and Playground using JSCL +// Loaded dynamically and runs completely client-side. + +let jsclLoading = false; +let jsclLoaded = false; +const jsclCallbacks = []; + +function ensureJSCL(callback) { + if (window.jscl) { + callback(); + return; + } + jsclCallbacks.push(callback); + if (jsclLoading) return; + jsclLoading = true; + + const script = document.createElement('script'); + script.src = "https://cdn.jsdelivr.net/gh/jscl-project/jscl-project.github.io/jscl.js"; + script.onload = () => { + jsclLoaded = true; + while (jsclCallbacks.length > 0) { + const cb = jsclCallbacks.shift(); + cb(); + } + }; + script.onerror = () => { + // Try unpkg as a fallback CDN + const fallbackScript = document.createElement('script'); + fallbackScript.src = "https://unpkg.com/jscl/dist/jscl.js"; + fallbackScript.onload = () => { + jsclLoaded = true; + while (jsclCallbacks.length > 0) { + const cb = jsclCallbacks.shift(); + cb(); + } + }; + fallbackScript.onerror = (err) => { + console.error("Failed to load JSCL from both CDNs:", err); + alert("Failed to load JSCL Common Lisp compiler. Please check your internet connection."); + }; + document.head.appendChild(fallbackScript); + }; + document.head.appendChild(script); +} + +function initInlineSnippets() { + const blocks = document.querySelectorAll('pre.lisp-code-block:not(#playground-editor)'); + blocks.forEach((block) => { + // Wrap the pre element + const container = document.createElement('div'); + container.className = 'lisp-snippet-container'; + container.style.margin = '1em 0'; + + block.parentNode.insertBefore(container, block); + container.appendChild(block); + + // Create controls + const controls = document.createElement('div'); + controls.className = 'lisp-snippet-controls'; + controls.style.margin = '5px 0'; + controls.style.display = 'flex'; + controls.style.gap = '10px'; + + const runBtn = document.createElement('button'); + runBtn.textContent = 'Run'; + runBtn.className = 'lisp-btn lisp-btn-run'; + + const editBtn = document.createElement('button'); + editBtn.textContent = 'Edit'; + editBtn.className = 'lisp-btn lisp-btn-edit'; + + controls.appendChild(runBtn); + controls.appendChild(editBtn); + container.appendChild(controls); + + // Create output panel + const outputPanel = document.createElement('pre'); + outputPanel.className = 'lisp-output-panel'; + outputPanel.style.display = 'none'; + outputPanel.style.padding = '0.5em'; + outputPanel.style.marginTop = '5px'; + outputPanel.style.border = '1px dashed currentColor'; + outputPanel.style.backgroundColor = 'rgba(128, 128, 128, 0.05)'; + outputPanel.style.whiteSpace = 'pre-wrap'; + outputPanel.style.wordBreak = 'break-all'; + container.appendChild(outputPanel); + + let isEditing = false; + + editBtn.addEventListener('click', () => { + if (!isEditing) { + block.contentEditable = 'true'; + block.spellcheck = false; + block.style.outline = '1px solid currentColor'; + block.style.padding = '5px'; + block.focus(); + editBtn.textContent = 'View'; + isEditing = true; + } else { + block.contentEditable = 'false'; + block.style.outline = 'none'; + block.style.padding = ''; + editBtn.textContent = 'Edit'; + isEditing = false; + + // Fetch colorized version from backend API + const code = block.textContent; + fetch('/api/colorize', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'code=' + encodeURIComponent(code) + }) + .then(response => response.text()) + .then(html => { + block.innerHTML = html; + }) + .catch(err => console.error("Failed to colorize snippet:", err)); + } + }); + + block.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.key === 'Enter') { + e.preventDefault(); + runBtn.click(); + } + }); + + runBtn.addEventListener('click', () => { + runBtn.disabled = true; + const originalText = runBtn.textContent; + runBtn.textContent = 'Running...'; + outputPanel.style.display = 'block'; + outputPanel.textContent = 'Initializing JSCL and executing...'; + + ensureJSCL(() => { + try { + const code = block.textContent; + const wrappedCode = ` +(let ((out (make-string-output-stream))) + (let ((*standard-output* out)) + (let ((val (progn +${code} + ))) + (format nil "~A|==SEPARATOR==|~S" (get-output-stream-string out) val)))) +`; + const resRaw = window.jscl.evaluateString(wrappedCode); + let resStr = resRaw; + if (Array.isArray(resRaw)) { + resStr = resRaw.join(''); + } else if (typeof resRaw !== 'string') { + resStr = String(resRaw); + } + + let stdout = ""; + let returnValue = ""; + if (typeof resStr === 'string' && resStr.includes('|==SEPARATOR==|')) { + const parts = resStr.split('|==SEPARATOR==|'); + stdout = parts[0]; + returnValue = parts[1]; + } else { + returnValue = String(resStr); + } + + let displayResult = ""; + if (stdout) { + displayResult += stdout; + if (!stdout.endsWith('\n')) { + displayResult += '\n'; + } + } + displayResult += `=> ${returnValue}`; + + outputPanel.textContent = displayResult; + outputPanel.style.color = ''; + } catch (err) { + outputPanel.textContent = `Error: ${err.message || err}`; + outputPanel.style.color = 'red'; + } finally { + runBtn.disabled = false; + runBtn.textContent = originalText; + } + }); + }); + }); +} + +function initPlayground() { + const editor = document.getElementById('playground-editor'); + const runBtn = document.getElementById('playground-run'); + const clearBtn = document.getElementById('playground-clear'); + const examplesSelect = document.getElementById('playground-examples'); + const outputPanel = document.getElementById('playground-output'); + + if (!editor || !runBtn || !clearBtn || !examplesSelect || !outputPanel) return; + + const examples = { + hello: `(format t "Hello, World!~%")`, + fib: `(defun fib (n) + (if (< n 2) + n + (+ (fib (- n 1)) (fib (- n 2))))) + +(format t "Fibonacci of 10 is: ~a~%" (fib 10))`, + loop: `(loop for x from 1 to 5 + do (format t "Square of ~d is ~d~%" x (* x x)))`, + clos: `(defclass person () + ((name :accessor person-name :initarg :name) + (age :accessor person-age :initarg :age))) + +(defmethod introduce ((p person)) + (format t "Hi, I am ~a and I am ~a years old.~%" + (person-name p) + (person-age p))) + +(let ((p (make-instance 'person :name "Alice" :age 30))) + (introduce p))` + }; + + examplesSelect.addEventListener('change', () => { + const key = examplesSelect.value; + const exampleData = document.getElementById('example-data-' + key); + if (exampleData) { + editor.innerHTML = exampleData.innerHTML; + } else { + editor.innerHTML = ""; + } + }); + + clearBtn.addEventListener('click', () => { + outputPanel.textContent = ''; + outputPanel.style.display = 'none'; + }); + + editor.addEventListener('keydown', (e) => { + if (e.ctrlKey && e.key === 'Enter') { + e.preventDefault(); + runBtn.click(); + } + }); + + runBtn.addEventListener('click', () => { + runBtn.disabled = true; + const originalText = runBtn.textContent; + runBtn.textContent = 'Running...'; + outputPanel.style.display = 'block'; + outputPanel.textContent = 'Initializing JSCL and executing...'; + + ensureJSCL(() => { + try { + const code = editor.textContent; + const wrappedCode = ` +(let ((out (make-string-output-stream))) + (let ((*standard-output* out)) + (let ((val (progn +${code} + ))) + (format nil "~A|==SEPARATOR==|~S" (get-output-stream-string out) val)))) +`; + const resRaw = window.jscl.evaluateString(wrappedCode); + let resStr = resRaw; + if (Array.isArray(resRaw)) { + resStr = resRaw.join(''); + } else if (typeof resRaw !== 'string') { + resStr = String(resRaw); + } + + let stdout = ""; + let returnValue = ""; + if (typeof resStr === 'string' && resStr.includes('|==SEPARATOR==|')) { + const parts = resStr.split('|==SEPARATOR==|'); + stdout = parts[0]; + returnValue = parts[1]; + } else { + returnValue = String(resStr); + } + + let displayResult = ""; + if (stdout) { + displayResult += stdout; + if (!stdout.endsWith('\n')) { + displayResult += '\n'; + } + } + displayResult += `=> ${returnValue}`; + + outputPanel.textContent = displayResult; + outputPanel.style.color = ''; + + // Dynamically update the editor syntax highlighting using backend colorize API + fetch('/api/colorize', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: 'code=' + encodeURIComponent(code) + }) + .then(response => response.text()) + .then(html => { + editor.innerHTML = html; + }) + .catch(err => console.error("Failed to colorize playground code:", err)); + + } catch (err) { + outputPanel.textContent = `Error: ${err.message || err}`; + outputPanel.style.color = 'red'; + } finally { + runBtn.disabled = false; + runBtn.textContent = originalText; + } + }); + }); +} + +document.addEventListener('DOMContentLoaded', () => { + initInlineSnippets(); + initPlayground(); +}); |
