// ── Utility functions & dropdown refresh ───────────────────────────────────── // // Pure helper functions shared across blocks, persistence, and app modules. // Depends on: dom.js ($, $id) // // Exports: // splitValueAndUnit() → parse "5 km" → {numVal, unitSuffix} // rebuildSelect() → replace , preserving the current selection if it still exists. * @param {HTMLSelectElement} sel - the select element to rebuild * @param {string} placeholder - text for the first (empty-value) option * @param {string[]} values - option value/label pairs to add * @param {string} current - currently selected value to attempt to preserve */ function rebuildSelect(sel, placeholder, values, current) { sel.innerHTML = ``; const frag = document.createDocumentFragment(); for (const v of values) { const opt = document.createElement("option"); opt.value = v; opt.textContent = v; frag.appendChild(opt); } sel.appendChild(frag); if (current && [...sel.options].some(o => o.value === current)) { sel.value = current; } } /** * Refreshes every quantity-name dropdown (.var-type, .unit-type) by scanning * all wc-quantity blocks in the data area. */ function refreshQuantityOptions() { const dataArea = $id("data-area"); if (!dataArea) return; const qNames = Array.from( dataArea.querySelectorAll("wc-quantity") ).map(q => { const el = q.querySelector(".quantity-name"); return el ? (el.value || "unnamed") : "unnamed"; }); document.querySelectorAll( "#variable-area .var-type, #data-area .unit-type" ).forEach(sel => { const placeholder = sel.classList.contains("var-type") ? "(no type)" : "(select quantity)"; rebuildSelect(sel, placeholder, qNames, sel.value); }); } /** * Refreshes the unit-suffix dropdown on every datum block to show only units * that match the datum's currently selected quantity. */ function refreshUnitOptions() { const varArea = $id("variable-area"); const dataArea = $id("data-area"); if (!varArea || !dataArea) return; varArea.querySelectorAll("wc-scalar").forEach(d => { const typeEl = d.querySelector(".var-type"); const sel = d.querySelector(".var-unit"); if (!typeEl || !sel) return; const quantityName = typeEl.value; const suffixes = Array.from( dataArea.querySelectorAll("wc-unit") ) .filter( u => { const t = u.querySelector(".unit-type"); const s = u.querySelector(".unit-suffix"); return t && s && t.value === quantityName && s.value; } ) .map(u => { const s = u.querySelector(".unit-suffix"); return s ? s.value : ""; }); rebuildSelect(sel, "(unit)", suffixes, sel.value); }); } /** * Looks up the default unit suffix for a given quantity name. * @param {string} quantityName * @returns {string} default unit suffix, or "" if not found */ function getDefaultUnitSuffix(quantityName) { const dataArea = $id("data-area"); if (!dataArea) return ""; for (const q of dataArea.querySelectorAll("wc-quantity")) { const nameEl = q.querySelector(".quantity-name"); if (nameEl && (nameEl.value || "unnamed") === quantityName) { const unitEl = q.querySelector(".quantity-default-unit"); return unitEl ? (unitEl.value || "") : ""; } } return ""; } /** * Refreshes the default-unit dropdown on every quantity block with suffixes * of matching unit blocks. */ function refreshDefaultUnitOptions() { const dataArea = $id("data-area"); if (!dataArea) return; dataArea.querySelectorAll("wc-quantity").forEach(q => { const nameEl = q.querySelector(".quantity-name"); const sel = q.querySelector(".quantity-default-unit"); if (!nameEl || !sel) return; const quantityName = nameEl.value || "unnamed"; const suffixes = Array.from( dataArea.querySelectorAll("wc-unit") ) .filter( u => { const t = u.querySelector(".unit-type"); const s = u.querySelector(".unit-suffix"); return t && s && t.value === quantityName && s.value; } ) .map(u => { const s = u.querySelector(".unit-suffix"); return s ? s.value : ""; }); rebuildSelect(sel, "(none)", suffixes, sel.value); }); } /** * Refreshes the variable-select dropdown on every wc-varref block in the * algorithm pane. Scans all boolean, scalar, string, and array blocks. */ function refreshVarRefOptions() { const selects = document.querySelectorAll(".varref-select, .assign-target"); if (!selects.length) return; const varArea = $id("variable-area"); const vars = []; if (varArea) { for (const b of varArea.querySelectorAll("wc-boolean, wc-scalar, wc-string, wc-array, wc-vector")) { const nameEl = b.querySelector(".var-name"); const name = nameEl ? (nameEl.value || "unnamed") : "unnamed"; const tag = b.tagName.toLowerCase().replace("wc-", ""); vars.push({ id: b.id, label: tag + ": " + name }); } } for (const sel of selects) { const current = sel.dataset.restoredValue || sel.value; delete sel.dataset.restoredValue; sel.innerHTML = ''; for (const v of vars) { const opt = document.createElement("option"); opt.value = v.id; opt.textContent = v.label; sel.appendChild(opt); } if (current && [...sel.options].some(o => o.value === current)) { sel.value = current; } } } /** * Activates the clicked library sub-tab and shows its corresponding pane. * @param {string} name - library name (matches data-lib-name attribute) */ function switchLibTab(name) { for (const tab of document.querySelectorAll(".lib-tab")) { tab.classList.toggle("active", tab.dataset.libName === name); } for (const pane of document.querySelectorAll(".lib-pane")) { pane.classList.toggle("active", pane.dataset.libName === name); } } /** @type {WeakMap} Cache mapping data hooks to their source blocks. */ const _dataSourceCache = new WeakMap(); /** * Finds the block connected to a given data hook via any .flow-data path. * Results are cached in _dataSourceCache. * @param {HTMLElement} hook - the data hook button element * @returns {HTMLElement|null} the source block, or null if nothing is connected */ function findDataSource(hook) { if (_dataSourceCache.has(hook)) return _dataSourceCache.get(hook); for (const path of document.querySelectorAll("path.flow-data")) { if (path.toHook === hook) { _dataSourceCache.set(hook, path.fromElement); return path.fromElement; } } return null; } /** Copies the generated C code text to the clipboard. */ /** * Escapes special characters for embedding in a C string literal. * Handles backslash, double-quote, newline, carriage return, and tab. * @param {string} str - raw string * @returns {string} escaped string safe for C "..." literals */ function escapeString(str) { return str .replace(/\\/g, "\\\\").replace(/"/g, '\\"') .replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t") .replace(/\f/g, "\\f").replace(/\v/g, "\\v").replace(/\0/g, "\\0"); } /** @type {Set} C reserved keywords — toCName appends "_" to avoid clashes. */ const C_KEYWORDS = new Set([ "auto","break","case","char","const","continue","default","do","double", "else","enum","extern","float","for","goto","if","int","long","register", "return","short","signed","sizeof","static","struct","switch","typedef", "union","unsigned","void","volatile","while" ]); /** * Converts a natural-language name into a valid C identifier. * Lowercases, replaces non-alphanumeric chars with underscores, * collapses runs of underscores, strips leading/trailing underscores, * and prepends an underscore if the result starts with a digit. * @param {string} str - natural name like "My Variable 2" * @returns {string} valid C identifier like "my_variable_2" */ function toCName(str) { let s = (str || "").trim().toLowerCase(); s = s.replace(/[^a-z0-9_]/g, "_"); s = s.replace(/_+/g, "_"); s = s.replace(/^_|_$/g, ""); if (/^\d/.test(s)) s = "_" + s; if (!s || C_KEYWORDS.has(s)) s = (s || "var") + "_"; return s; } /** * Derives a human-readable natural name from a C function/tech name. * Converts camelCase and snake_case to words, strips common prefixes. * @param {string} techName - e.g. "SDL_CreateWindow", "glfwInit", "puts" * @returns {string} e.g. "Create Window", "Init GLFW", "Puts" */ function deriveNaturalName(techName) { if (!techName) return ""; let s = techName; s = s.replace(/^(SDL_|glfw|gl_?)/i, ''); const known = { puts: "Print String", printf: "Print Formatted", getchar: "Get Char", putchar: "Put Char", perror: "Print Error", malloc: "Allocate Memory", atoi: "Parse Int", atof: "Parse Float", strlen: "String Length", strcmp: "Compare Strings", fabs: "Abs Float", fmod: "Float Modulo", }; if (known[s]) return known[s]; s = s.replace(/([a-z])([A-Z])/g, '$1 $2'); s = s.replace(/_/g, ' '); s = s.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); s = s.replace(/\s+/g, ' ').trim(); return s.charAt(0).toUpperCase() + s.slice(1); } /** * Builds a C comment prefix from a variable's natural name and an optional comment. * @param {string} naturalName - the human-readable variable name * @param {string} comment - the optional comment text * @returns {string} e.g. "// My Flag: indicates status\n" or "" when no comment */ function commentPrefix(naturalName, comment) { naturalName = (naturalName || "").trim(); comment = (comment || "").trim(); return comment && naturalName ? `// ${naturalName}: ${comment}\n` : comment ? `// ${comment}\n` : ""; } /** * Resolves the ultimate C variable name from a block by checking for * .var-name, .expr-name, and following .varref-select chains. * Returns the empty string when nothing is found. * @param {HTMLElement} block - any block that may hold a variable reference * @param {Set} [visited] - cycle detection set (internal use) * @returns {string} the C name, or "" */ function resolveSourceName(block, visited) { visited = visited || new Set(); if (visited.has(block)) return ""; visited.add(block); const ni = block.querySelector(".var-name"); if (ni) return toCName(ni.value) || "x"; const ei = block.querySelector(".expr-name"); if (ei) return ei.value || "res"; const vr = block.querySelector(".varref-select"); if (vr) { const target = vr.value ? $id(vr.value) : null; if (target) return resolveSourceName(target, visited); return vr.value || "var"; } return ""; } /** * Opens a modal dialog that shows the block's editable fields in a compact, * full-width layout for easier editing. * @param {HTMLElement} block */ function showBlockEditor(block) { let overlay = $id("block-editor-overlay"); if (!overlay) { overlay = document.createElement("div"); overlay.id = "block-editor-overlay"; overlay.className = "block-editor-overlay"; overlay.addEventListener("click", ev => { if (ev.target === overlay) overlay.classList.remove("active"); }); document.body.appendChild(overlay); const editorDiv = document.createElement("div"); editorDiv.className = "block-editor-modal"; editorDiv.innerHTML = '
' + ' Edit Block' + ' ' + '
' + '
' + ''; overlay.appendChild(editorDiv); editorDiv.querySelector(".block-editor-close").addEventListener("click", () => overlay.classList.remove("active")); editorDiv.querySelector(".block-editor-cancel").addEventListener("click", () => overlay.classList.remove("active")); } const body = overlay.querySelector(".block-editor-body"); body.innerHTML = ""; /* Colour picker row; error blocks default to red, others to blue */ const defaultColor = block.classList.contains("error-path") || block.dataset.exitError ? "#ef5350" : "#4a90d9"; const colorRow = document.createElement("div"); colorRow.className = "block-editor-color-row"; colorRow.innerHTML = '' + '' + ''; body.appendChild(colorRow); const colorPicker = colorRow.querySelector(".block-editor-color-picker"); const origColor = block.dataset.blockColor || ""; colorRow.querySelector(".block-editor-color-reset").addEventListener("click", () => { colorPicker.value = defaultColor; }); const clone = block.cloneNode(true); clone.style.position = "static"; clone.style.width = ""; clone.style.height = ""; // Remove interactive buttons from clone (edit, delete, resize) for (const btn of clone.querySelectorAll(".edit-btn, .delete-btn, .resize-handle, .hook")) btn.remove(); // Restore hidden inputs and remove their labels so the modal shows editable fields for (const label of clone.querySelectorAll(".field-display")) { const input = label.nextElementSibling; if (input && (input.tagName === "INPUT" || input.tagName === "SELECT" || input.tagName === "TEXTAREA")) { input.style.display = ""; } label.remove(); } body.appendChild(clone); overlay.querySelector(".block-editor-title").textContent = "Edit " + (block.tagName || "Block"); overlay.classList.add("active"); overlay.querySelector(".block-editor-apply").onclick = () => { // Sync inputs/selects/textareas from clone back to original const srcFields = block.querySelectorAll("input, select, textarea"); const cloneFields = clone.querySelectorAll("input, select, textarea"); for (let i = 0; i < srcFields.length && i < cloneFields.length; i++) { if (srcFields[i].type !== "hidden") srcFields[i].value = cloneFields[i].value; } // Trigger change and input events so block internals sync for (const el of srcFields) { el.dispatchEvent(new Event("change", { bubbles: true })); el.dispatchEvent(new Event("input", { bubbles: true })); } // For END blocks: parse and select the errno from the synced value if (block.tagName === "WC-END" && typeof block._selectByCode === "function") { const val = block._el?.input?.value || ""; const code = parseInt(val); if (!isNaN(code)) block._selectByCode(code); } /* Apply colour */ const newColor = colorPicker.value; if (newColor !== defaultColor) { block.dataset.blockColor = newColor; block.style.borderColor = newColor; block.style.borderWidth = "2px"; } else { delete block.dataset.blockColor; block.style.borderColor = ""; block.style.borderWidth = ""; } overlay.classList.remove("active"); }; } /** Check every algorithm block and flag those without an incoming logic connection. */ function refreshBlockWarnings() { for (const block of document.querySelectorAll("#main-program > [data-block]")) { if (typeof block._checkLogicInput === "function") block._checkLogicInput(); } /* Warn when a wc-proccall has no connection from its error (!) output hook. */ for (const block of document.querySelectorAll("wc-proccall")) { const errHook = block.querySelector(".proccall-error-hook"); if (!errHook) continue; const hasConnection = block.wcNextFlow.some(p => p.fromHook === errHook); block.classList.toggle("warn-error-unlinked", !hasConnection); } /* Warn when an error-path END block returns 0 (success in an error branch). */ for (const block of document.querySelectorAll("wc-end.error-path")) { block.classList.toggle("warn-zero-in-error", block._selectedCode === 0); } } /** * Marks algorithm blocks that are reachable ONLY via error-flow paths. * Adds/removes the `.error-path` class so they render with a red highlight. */ function refreshErrorPaths() { for (const el of document.querySelectorAll("[data-block].error-path")) { el.classList.remove("error-path"); } for (const p of document.querySelectorAll("path.flow.flow-error")) { p.classList.remove("flow-error"); } const errorHooks = document.querySelectorAll("path.flow.flow-error-source"); if (errorHooks.length === 0) return; const errorRoots = new Set(); for (const path of errorHooks) { if (path.toElement) errorRoots.add(path.toElement); path.classList.add("flow-error"); } const normalReachable = new Set(); const start = $id("main-start"); if (start) { const q = [start]; while (q.length) { const el = q.pop(); if (normalReachable.has(el)) continue; normalReachable.add(el); for (const p of el.wcNextFlow) { if (p.classList.contains("flow-error-source")) continue; if (p.toElement) q.push(p.toElement); } } } const eq = [...errorRoots]; while (eq.length) { const el = eq.pop(); if (normalReachable.has(el)) continue; if (el.classList.contains("error-path")) continue; el.classList.add("error-path"); for (const p of el.wcNextFlow) { if (p.toElement) { p.classList.add("flow-error"); eq.push(p.toElement); } } } } /** Runs all refresh passes in dependency order. */ function refreshAll() { refreshQuantityOptions(); refreshUnitOptions(); refreshDefaultUnitOptions(); refreshVarRefOptions(); refreshBlockWarnings(); refreshErrorPaths(); }