Files
avcf/helpers.js
T

292 lines
10 KiB
JavaScript

// ── 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 <select> options preserving selection
// refreshQuantityOptions() → sync quantity-name dropdowns + "boolean"
// refreshUnitOptions() → sync unit-suffix dropdowns from unit blocks
// getDefaultUnitSuffix() → lookup default unit for a quantity name
// refreshDefaultUnitOptions() → sync default-unit dropdowns on quantity blocks
// refreshVarRefOptions() → sync variable-select dropdowns
// refreshProcOptions() → sync procedure-select dropdowns
// switchLibTab() → activate library sub-tab
// findDataSource() → find block connected to a data hook
// escapeString() → escape special chars for C string literal
// refreshAll() → run all refresh passes in order
/**
* Parses a string like "5 km" into a numeric value and an optional unit suffix.
* Falls back to parsing the entire string as a bare number when no suffix is present.
* @param {string} str - e.g. "5 km", "42", ""
* @param {number} defaultNum - fallback when parseFloat yields NaN
* @returns {{ numVal: number, unitSuffix: string|null }}
*/
function splitValueAndUnit(str, defaultNum) {
const trimmed = (str || "").trim();
const parts = trimmed.split(/\s+/);
let numVal, unitSuffix;
if (parts.length < 2 || isNaN(parseFloat(parts[0]))) {
numVal = parseFloat(trimmed);
unitSuffix = null;
} else {
numVal = parseFloat(parts[0]);
unitSuffix = parts.slice(1).join(" ");
}
return { numVal: isNaN(numVal) ? defaultNum : numVal, unitSuffix };
}
/**
* Replaces all options in a <select>, 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 = `<option value="">${placeholder}</option>`;
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 = '<option value="">(select variable)</option>';
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 = '<option value="">(select procedure)</option>';
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();
}