// ── 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 qNames = Array.from( $id("data-area").querySelectorAll("wc-quantity") ).map(q => (q.querySelector(".quantity-name").value || "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() { $id("variable-area").querySelectorAll("wc-scalar").forEach(d => { const quantityName = d.querySelector(".var-type").value; const sel = d.querySelector(".var-unit"); const suffixes = Array.from( $id("data-area").querySelectorAll("wc-unit") ) .filter( u => u.querySelector(".unit-type").value === quantityName && u.querySelector(".unit-suffix").value ) .map(u => u.querySelector(".unit-suffix").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) { for (const q of $id("data-area").querySelectorAll("wc-quantity")) { if ((q.querySelector(".quantity-name").value || "unnamed") === quantityName) { return q.querySelector(".quantity-default-unit").value || ""; } } return ""; } /** * Refreshes the default-unit dropdown on every quantity block with suffixes * of matching unit blocks. */ function refreshDefaultUnitOptions() { $id("data-area").querySelectorAll("wc-quantity").forEach(q => { const quantityName = q.querySelector(".quantity-name").value || "unnamed"; const sel = q.querySelector(".quantity-default-unit"); const suffixes = Array.from( $id("data-area").querySelectorAll("wc-unit") ) .filter( u => u.querySelector(".unit-type").value === quantityName && u.querySelector(".unit-suffix").value ) .map(u => u.querySelector(".unit-suffix").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 vars = []; for (const b of $id("variable-area").querySelectorAll("wc-boolean, wc-scalar, wc-string, wc-array")) { const name = (b.querySelector(".var-name") || {}).value || "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; } } } /** * Refreshes the procedure-select dropdown on every wc-proccall block. * Scans all wc-procedure blocks and populates options as "library/name". */ function refreshProcOptions() { const proccalls = document.querySelectorAll("wc-proccall"); if (!proccalls.length) return; const options = Array.from(document.querySelectorAll("wc-procedure")).map(p => p.getQualifiedName()); for (const call of proccalls) { const sel = call.querySelector(".proccall-select"); const current = sel.dataset.restoredValue || sel.value; delete sel.dataset.restoredValue; sel.innerHTML = ''; for (const opt of options) { const o = document.createElement("option"); o.value = opt; o.textContent = opt; sel.appendChild(o); } 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); } } /** * Finds the block connected to a given data hook via any .flow-data path. * @param {HTMLElement} hook - the data hook button element * @returns {HTMLElement|null} the source block, or null if nothing is connected */ function findDataSource(hook) { for (const path of document.querySelectorAll("path.flow-data")) { if (path.toHook === hook) return path.fromElement; } return null; } /** * 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"); } /** * 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; return s || "var"; } /** * 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 ""; } /** Runs all refresh passes in dependency order. */ function refreshAll() { refreshQuantityOptions(); refreshUnitOptions(); refreshDefaultUnitOptions(); refreshVarRefOptions(); refreshProcOptions(); }