292 lines
10 KiB
JavaScript
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();
|
|
}
|