/** * js_top_worker Feature Demo * * This JavaScript file demonstrates all features of js_top_worker: * - Basic execution * - Multiple isolated environments * - MIME output (HTML, SVG, images) * - Autocomplete * - Type information * - Error reporting * - Directives * - Library loading */ // ============================================================================ // Worker Communication Setup // ============================================================================ let worker = null; let rpcId = 1; const pendingCalls = new Map(); let currentEnv = ""; let envCount = 0; function getWorkerURL(baseUrl) { // Convert relative URL to absolute - importScripts in blob workers needs absolute URLs const absoluteBase = new URL(baseUrl, window.location.href).href; const workerScript = new URL("rpc_worker.bc.js", window.location.href).href; const content = `globalThis.__global_rel_url="${absoluteBase}"\nimportScripts("${workerScript}");`; return URL.createObjectURL(new Blob([content], { type: "text/javascript" })); } function log(message, type = "info") { const entries = document.getElementById("log-entries"); const entry = document.createElement("div"); entry.className = `log-entry ${type}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; entries.appendChild(entry); entries.parentElement.scrollTop = entries.parentElement.scrollHeight; console.log(`[${type}] ${message}`); } function setStatus(status, text) { const indicator = document.getElementById("status-indicator"); const statusText = document.getElementById("status-text"); indicator.className = `status-indicator ${status}`; statusText.textContent = text; } // RPC call wrapper function rpc(method, params) { return new Promise((resolve, reject) => { const id = rpcId++; const message = JSON.stringify({ id, method, params }); pendingCalls.set(id, { resolve, reject }); worker.postMessage(message); log(`RPC: ${method}(${JSON.stringify(params).substring(0, 100)}...)`, "info"); }); } // Handle incoming messages from worker function onWorkerMessage(e) { try { const response = JSON.parse(e.data); if (response.id && pendingCalls.has(response.id)) { const { resolve, reject } = pendingCalls.get(response.id); pendingCalls.delete(response.id); if (response.error) { log(`RPC Error: ${JSON.stringify(response.error)}`, "error"); reject(response.error); } else { resolve(response.result); } } } catch (err) { log(`Parse error: ${err.message}`, "error"); } } // ============================================================================ // Initialization // ============================================================================ async function initWorker() { try { setStatus("", "Loading worker..."); // Create worker from the _opam directory const workerUrl = getWorkerURL("_opam"); worker = new Worker(workerUrl); worker.onmessage = onWorkerMessage; worker.onerror = (e) => { log(`Worker error: ${e.message}`, "error"); setStatus("error", "Worker error"); }; setStatus("", "Initializing toplevel..."); // Initialize the toplevel - named params are in first element of array // Include mime_printer for MIME output support await rpc("init", [ { init_libs: { stdlib_dcs: "lib/ocaml/dynamic_cmis.json", findlib_requires: ["mime_printer"], execute: true } } ]); log("Toplevel initialized", "success"); // Setup default environment setStatus("", "Setting up default environment..."); const setupResult = await rpc("setup", [{ env_id: "" }]); log("Default environment ready", "success"); setStatus("ready", "Ready"); // Show setup blurb if (setupResult && setupResult.caml_ppf) { log(`OCaml toplevel: ${setupResult.caml_ppf.substring(0, 100)}...`, "info"); } } catch (err) { log(`Initialization failed: ${err.message || JSON.stringify(err)}`, "error"); setStatus("error", "Initialization failed"); } } // ============================================================================ // Output Helpers // ============================================================================ function formatOutput(result) { let html = ""; if (result.stdout) { html += `
${escapeHtml(result.stdout)}
`; } if (result.stderr) { html += `
${escapeHtml(result.stderr)}
`; } if (result.sharp_ppf) { html += `
${escapeHtml(result.sharp_ppf)}
`; } if (result.caml_ppf) { html += `
${escapeHtml(result.caml_ppf)}
`; } if (result.highlight) { const h = result.highlight; html += `
Highlight: (${h.line1}:${h.col1}) to (${h.line2}:${h.col2})
`; } return html || '
No output
'; } function escapeHtml(str) { return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function clearOutput(elementId) { document.getElementById(elementId).innerHTML = ""; } // ============================================================================ // Feature: Basic Execution // ============================================================================ async function runExec() { const input = document.getElementById("exec-input").value; const output = document.getElementById("exec-output"); try { output.innerHTML = '
Executing...
'; const result = await rpc("exec", [{ env_id: "" }, input]); output.innerHTML = formatOutput(result); } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Feature: Multiple Environments // ============================================================================ function updateEnvSelector() { const selector = document.getElementById("env-selector"); const buttons = selector.querySelectorAll(".env-btn"); buttons.forEach(btn => { btn.classList.toggle("active", btn.dataset.env === currentEnv); }); } async function createEnv() { const envId = `env${++envCount}`; try { await rpc("create_env", [{ env_id: envId }]); await rpc("setup", [{ env_id: envId }]); const selector = document.getElementById("env-selector"); const btn = document.createElement("button"); btn.className = "env-btn"; btn.dataset.env = envId; btn.textContent = envId; btn.onclick = () => selectEnv(envId); selector.appendChild(btn); log(`Created environment: ${envId}`, "success"); selectEnv(envId); } catch (err) { log(`Failed to create environment: ${err.message || JSON.stringify(err)}`, "error"); } } function selectEnv(envId) { currentEnv = envId; updateEnvSelector(); log(`Selected environment: ${envId || "default"}`, "info"); } async function listEnvs() { try { const envs = await rpc("list_envs", []); const output = document.getElementById("env-output"); output.innerHTML = `
Environments: ${envs.join(", ")}
`; } catch (err) { log(`Failed to list environments: ${err.message || JSON.stringify(err)}`, "error"); } } async function runInEnv() { const input = document.getElementById("env-input").value; const output = document.getElementById("env-output"); try { output.innerHTML = `
Executing in "${currentEnv || "default"}"...
`; const result = await rpc("exec", [{ env_id: currentEnv }, input]); output.innerHTML = formatOutput(result); } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // Make selectEnv available for onclick document.addEventListener("DOMContentLoaded", () => { document.querySelector('.env-btn[data-env=""]').onclick = () => selectEnv(""); }); // ============================================================================ // Feature: MIME Output // ============================================================================ async function runMime() { const input = document.getElementById("mime-input").value; const output = document.getElementById("mime-output"); const rendered = document.getElementById("mime-rendered"); try { output.innerHTML = '
Executing...
'; rendered.classList.add("hidden"); const result = await rpc("exec", [{ env_id: "" }, input]); output.innerHTML = formatOutput(result); // Render MIME values if (result.mime_vals && result.mime_vals.length > 0) { rendered.classList.remove("hidden"); rendered.innerHTML = ""; for (const mime of result.mime_vals) { const div = document.createElement("div"); div.style.marginBottom = "10px"; if (mime.mime_type.startsWith("image/svg")) { div.innerHTML = mime.data; } else if (mime.mime_type.startsWith("image/") && mime.encoding === "Base64") { div.innerHTML = ``; } else if (mime.mime_type === "text/html") { div.innerHTML = mime.data; } else { div.innerHTML = `
${escapeHtml(mime.data)}
`; } const label = document.createElement("div"); label.style.fontSize = "0.75rem"; label.style.color = "#666"; label.textContent = `MIME: ${mime.mime_type}`; div.appendChild(label); rendered.appendChild(div); } } } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } function loadMimeExamples() { const examples = [ { name: "SVG Circle", code: `let svg = {| OCaml |};; Mime_printer.push "image/svg" svg;;` }, { name: "HTML Table", code: `let html = {|
NameValue
x42
y3.14
|};; Mime_printer.push "text/html" html;;` }, { name: "SVG Bar Chart", code: `let bars = [20; 45; 30; 60; 35];; let bar_svg = let bar i h = Printf.sprintf {||} (i * 40 + 10) (100 - h) h in Printf.sprintf {|%s|} (String.concat "" (List.mapi bar bars));; Mime_printer.push "image/svg" bar_svg;;` } ]; const idx = Math.floor(Math.random() * examples.length); document.getElementById("mime-input").value = examples[idx].code; log(`Loaded example: ${examples[idx].name}`, "info"); } // ============================================================================ // Feature: Autocomplete // ============================================================================ async function runComplete() { const input = document.getElementById("complete-input").value; const output = document.getElementById("complete-output"); try { output.innerHTML = '
Loading...
'; // Position at end of input - variants encoded as arrays in rpclib const pos = ["Offset", input.length]; const result = await rpc("complete_prefix", [{ env_id: "", is_toplevel: true }, [], [], input, pos]); if (result.entries && result.entries.length > 0) { output.innerHTML = result.entries.map(entry => `
${escapeHtml(entry.name)} ${escapeHtml(entry.kind)}
`).join(""); } else { output.innerHTML = '
No completions found
'; } } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Feature: Type Information // ============================================================================ async function runTypeEnclosing() { const input = document.getElementById("type-input").value; const pos = parseInt(document.getElementById("type-pos").value) || 0; const output = document.getElementById("type-output"); try { output.textContent = "Loading..."; const position = ["Offset", pos]; const result = await rpc("type_enclosing", [{ env_id: "", is_toplevel: true }, [], [], input, position]); if (result && result.length > 0) { output.innerHTML = result.map(([loc, typeStr, tailPos]) => { const typeText = typeof typeStr === "object" && typeStr.String ? typeStr.String : (typeof typeStr === "object" && typeStr.Index !== undefined ? `(index ${typeStr.Index})` : JSON.stringify(typeStr)); return `
${escapeHtml(typeText)}
`; }).join(""); } else { output.textContent = "No type information at this position"; } } catch (err) { output.textContent = `Error: ${JSON.stringify(err)}`; } } // ============================================================================ // Feature: Error Reporting // ============================================================================ async function runQueryErrors() { const input = document.getElementById("errors-input").value; const output = document.getElementById("errors-output"); try { output.innerHTML = '
Analyzing...
'; // Named params: env_id, is_toplevel. Positional: id, dependencies, source const result = await rpc("query_errors", [{ env_id: "", is_toplevel: true }, [], [], input]); if (result && result.length > 0) { output.innerHTML = result.map(err => { const isWarning = err.kind && (err.kind.Report_warning || err.kind.Report_alert); return `
Line ${err.loc.loc_start.pos_lnum}: ${escapeHtml(err.main)} ${err.sub && err.sub.length > 0 ? `
${err.sub.map(escapeHtml).join("
")}
` : ""}
`; }).join(""); } else { output.innerHTML = '
No errors found!
'; } } catch (err) { output.innerHTML = `
Analysis error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Feature: Directives // ============================================================================ async function runDirective(directive) { const input = directive || document.getElementById("directive-input").value; const output = document.getElementById("directive-output"); if (directive) { document.getElementById("directive-input").value = directive; } try { output.innerHTML = '
Executing...
'; const result = await rpc("exec", [{ env_id: "" }, input]); output.innerHTML = formatOutput(result); } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Feature: Custom Printers // ============================================================================ async function runPrinter() { const input = document.getElementById("printer-input").value; const output = document.getElementById("printer-output"); try { output.innerHTML = '
Executing...
'; // Execute each phrase separately const phrases = input.split(";;").filter(p => p.trim()).map(p => p.trim() + ";;"); let allOutput = ""; for (const phrase of phrases) { const result = await rpc("exec", [{ env_id: "" }, phrase]); allOutput += formatOutput(result); } output.innerHTML = allOutput; } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Feature: Library Loading // ============================================================================ async function runRequire() { const input = document.getElementById("require-input").value; const output = document.getElementById("require-output"); try { output.innerHTML = '
Executing (loading libraries may take a moment)...
'; // Execute each phrase separately const phrases = input.split(";;").filter(p => p.trim()).map(p => p.trim() + ";;"); let allOutput = ""; for (const phrase of phrases) { const result = await rpc("exec", [{ env_id: "" }, phrase]); allOutput += formatOutput(result); } output.innerHTML = allOutput; } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Feature: Toplevel Script // ============================================================================ async function runToplevel() { const input = document.getElementById("toplevel-input").value; const output = document.getElementById("toplevel-output"); try { output.innerHTML = '
Executing script...
'; const result = await rpc("exec_toplevel", [{ env_id: "" }, input]); if (result.script) { output.innerHTML = `
${escapeHtml(result.script)}
`; } else { output.innerHTML = formatOutput(result); } // Handle MIME output from toplevel if (result.mime_vals && result.mime_vals.length > 0) { const mimeDiv = document.createElement("div"); mimeDiv.className = "mime-output"; mimeDiv.style.marginTop = "10px"; for (const mime of result.mime_vals) { if (mime.mime_type.startsWith("image/svg")) { mimeDiv.innerHTML += mime.data; } else if (mime.mime_type === "text/html") { mimeDiv.innerHTML += mime.data; } } if (mimeDiv.innerHTML) { output.appendChild(mimeDiv); } } } catch (err) { output.innerHTML = `
Error: ${escapeHtml(JSON.stringify(err))}
`; } } // ============================================================================ // Initialize on page load // ============================================================================ document.addEventListener("DOMContentLoaded", initWorker);