// ── Algorithm-tab block definitions ──────────────────────────────────────── // // Blocks that live on the Algorithm (flowchart) canvas: // WCStart, WCEnd, WCOutput, WCDecision, WCAssign, WCExpression, WCVarRef // // Each extends WCBlock (from wcblock.js) and provides generateOwnCode(). // // Load order dependencies: // wcblock.js → WCBlock base class // helpers.js → findDataSource, resolveSourceName, escapeString, toCName, // commentPrefix, refreshVarRefOptions // dom.js → $id // error.js → showError // ── WCStart — program entry point ────────────────────────────────────────── /** Program entry point. Cannot be deleted. */ class WCStart extends WCBlock { delete() { showError("Cannot delete the start block."); } } // ── WCEnd — program exit ─────────────────────────────────────────────────── /** Program exit block with errno autocomplete. At least one must return 0. */ class WCEnd extends WCBlock { connectedCallback() { super.connectedCallback(); this._errnoItems = _ERRNO_ITEMS; this._selectedCode = 0; this._el.input = this._require(".end-errno-input"); this._el.suggestions = this._require(".end-errno-suggestions"); this._el.desc = this._require(".end-desc-text"); this._el.input.addEventListener("input", () => this._onInput()); this._el.input.addEventListener("focus", () => { if (this._el.suggestions.children.length > 0) this._showSuggestions(); }); this._el.input.addEventListener("keydown", ev => this._onKeyDown(ev)); this._el.suggestions.addEventListener("mousedown", ev => { const sug = ev.target.closest(".end-errno-suggestion"); if (sug) { const code = parseInt(sug.dataset.code); if (!isNaN(code)) this._selectByCode(code); } }); document.addEventListener("click", ev => { if (!this.contains(ev.target)) this._hideSuggestions(); }); this._el.input.value = "0"; this._selectByCode(0); } _onInput() { const q = this._el.input.value.trim().toLowerCase(); if (!q) { this._renderSuggestions(this._errnoItems); this._showSuggestions(); return; } const qCode = parseInt(q); const matches = this._errnoItems.filter(item => (!isNaN(qCode) && item.code === qCode) || item.name.toLowerCase().includes(q) || item.desc.toLowerCase().includes(q) ); this._renderSuggestions(matches); this._showSuggestions(); } _renderSuggestions(items) { this._el.suggestions.innerHTML = ""; for (const item of items) this._addSuggestion(item); } _addSuggestion(item) { const div = document.createElement("div"); div.className = "end-errno-suggestion"; div.dataset.code = item.code; const codeSpan = document.createElement("span"); codeSpan.className = "end-suggestion-code"; codeSpan.textContent = item.code + " " + item.name; div.appendChild(codeSpan); const descSpan = document.createElement("span"); descSpan.className = "end-suggestion-desc"; descSpan.textContent = item.desc; div.appendChild(descSpan); this._el.suggestions.appendChild(div); } _showSuggestions() { this._el.suggestions.style.display = "block"; } _hideSuggestions() { this._el.suggestions.style.display = "none"; } _onKeyDown(ev) { if (ev.key === "ArrowDown" || ev.key === "ArrowUp") { ev.preventDefault(); const items = [...this._el.suggestions.querySelectorAll(".end-errno-suggestion")]; let idx = items.findIndex(el => el.classList.contains("highlighted")); if (ev.key === "ArrowDown") idx = Math.min(idx + 1, items.length - 1); else idx = Math.max(idx - 1, 0); items.forEach(el => el.classList.remove("highlighted")); if (items[idx]) { items[idx].classList.add("highlighted"); items[idx].scrollIntoView({ block: "nearest" }); } } if (ev.key === "Enter") { const hl = this._el.suggestions.querySelector(".highlighted"); if (hl) { ev.preventDefault(); this._selectByCode(parseInt(hl.dataset.code)); } } if (ev.key === "Escape") this._hideSuggestions(); } _selectByCode(code) { this._selectedCode = code; const item = this._errnoItems.find(i => i.code === code); if (item) { const display = code + " " + item.name; this._el.input.value = display; this._el.input.setAttribute("value", display); this._el.desc.textContent = item.desc; this.dataset.exitError = code !== 0 ? "true" : ""; } else { const display = code + " error"; this._el.input.value = display; this._el.input.setAttribute("value", display); this._el.desc.textContent = "unknown error"; this.dataset.exitError = "true"; } this._hideSuggestions(); } _enforceZeroEnd() { if (this._selectedCode === 0) return; const others = document.querySelectorAll("wc-end"); for (const other of others) { if (other !== this && other._selectedCode === 0) return; } this._selectByCode(0); } _hasOtherZero() { const others = document.querySelectorAll("wc-end"); for (const other of others) { if (other !== this && other._selectedCode === 0) return true; } return false; } delete() { if (this.id === "main-end") { showError("Cannot delete the default end block."); return; } if (!this._hasOtherZero()) { showError("At least one end block must return 0."); return; } super.delete(); setTimeout(() => { for (const end of document.querySelectorAll("wc-end")) end._enforceZeroEnd(); }, 0); } _genStringFrees() { const freed = new Set(); let code = ""; for (const el of document.querySelectorAll("#variable-area > [data-block]")) { if (el.dataType === "string") { const name = resolveSourceName(el); if (name && !freed.has(name)) { freed.add(name); code += ` free(${name}.data);\n`; } } } return code; } generateOwnCode() { const code = this._selectedCode; if (code === 0) { const frees = this._genStringFrees(); if (frees) return `\n${frees} return 0;\n`; return "\n return 0;\n"; } return `\n exit(${code});\n`; } saveData() { return { exitCode: this._selectedCode }; } restoreData(d) { this._selectByCode(d.exitCode !== undefined ? d.exitCode : 0); } } function _buildErrnoItems() { const items = [ { code: 0, name: "success", desc: "success" }, { code: 255, name: "error", desc: "generic error" }, ]; for (const e of ERRNO_LIST) { items.push({ code: e.code, name: e.name, desc: e.desc }); } return items; } const _ERRNO_ITEMS = _buildErrnoItems(); // ── WCOutput — printf statement ──────────────────────────────────────────── /** Output description block. Provides inline documentation without generating code. */ class WCOutput extends WCBlock { connectedCallback() { super.connectedCallback(); this._el.textarea = this.querySelector("textarea"); } saveData() { return { textarea: this._el.textarea.value }; } restoreData(d) { this.querySelector("textarea").value = d.textarea || ""; } generateOwnCode() { const msg = escapeString(this._el.textarea.value || ""); return `\nprintf("${msg}");\n`; } } // ── WCDecision — conditional branch ──────────────────────────────────────── /** Conditional branch block; splits flow into yes/no branches and emits if/else C code. */ class WCDecision extends WCBlock { connectedCallback() { super.connectedCallback(); this._el.boolIn = this.querySelector(".decision-data-in"); } _getBoolName(block) { return resolveSourceName(block) || "flag"; } _branchDescendants(roots) { const d = new Set(), q = [...roots]; for (const el of q) { if (d.has(el)) continue; d.add(el); if (el instanceof WCDecision) continue; for (const p of el.wcNextFlow) { if (!d.has(p.toElement)) q.push(p.toElement); } } return d; } _emitBranch(roots, stop, visited) { const v = new Set(visited); let c = ""; const walk = el => { if (v.has(el) || stop.has(el)) return; v.add(el); if (el instanceof WCDecision) { c += el.generateCode(v); } else { c += el.generateOwnCode(); for (const p of el.wcNextFlow) walk(p.toElement); } }; for (const el of roots) walk(el); return c; } generateCode(visited = new Set()) { const yesTargets = [], noTargets = []; for (const path of this.wcNextFlow) { const hook = path.fromHook; if (!hook || hook.id === "hook-out1") { yesTargets.push(path.toElement); } else if (hook.id === "hook-out0") { noTargets.push(path.toElement); } } const yesAll = this._branchDescendants(yesTargets); const noAll = this._branchDescendants(noTargets); const shared = new Set([...yesAll].filter(el => noAll.has(el) && el !== this)); const yesCode = this._emitBranch(yesTargets, shared, visited); const noCode = this._emitBranch(noTargets, shared, visited); const sharedCode = this._emitBranch([...shared], new Set(), visited); for (const el of yesAll) visited.add(el); for (const el of noAll) visited.add(el); const source = findDataSource(this._el.boolIn); const cond = source ? this._getBoolName(source) : "0"; let out = ""; if (yesCode || noCode) { if (yesCode) out += `if (${cond}) {\n${yesCode}}`; if (noCode) { out += yesCode ? ` else {\n${noCode}}` : `if (!(${cond})) {\n${noCode}}`; } } out += sharedCode; return out ? `\n${out}\n` : ""; } } // ── WCAssign — variable assignment block ─────────────────────────────────── /** Variable assignment block; writes to scalar/array/vector/string targets with data hooks. */ class WCAssign extends WCBlock { connectedCallback() { super.connectedCallback(); this._el.target = this.querySelector(".assign-target"); this._el.value = this.querySelector(".assign-value"); this._el.dataIn = this.querySelector(".assign-row .hook"); this._el.indexArea = this.querySelector(".assign-index-area"); this._el.indexInput = this.querySelector(".assign-index"); this._el.indexHook = this.querySelector(".assign-index-area .hook"); this._el.target.addEventListener("change", () => this._syncType()); setTimeout(() => { refreshVarRefOptions(); this._syncType(); this._updateValueVisibility(); }, 0); } _updateValueVisibility() { const hasConn = findDataSource(this._el.dataIn) !== null; this._el.value.classList.toggle("hidden", hasConn); const hasIdxConn = findDataSource(this._el.indexHook) !== null; this._el.indexInput.classList.toggle("hidden", hasIdxConn); } _syncType() { const id = this._el.target.value; const target = id ? $id(id) : null; const type = target ? target.dataType : "scalar"; this._el.dataIn.dataset.type = type; this._el.dataIn.title = "expects " + type; this._el.indexArea.style.display = type === "array" ? "flex" : "none"; } _getSourceVarName(block) { return resolveSourceName(block) || "x"; } _genStringLiteral(name, raw) { const escaped = escapeString(raw); const len = raw.length; return "\n" + [ `free(${name}.data);`, `${name}.length = ${len};`, `${name}.data = malloc(${len} + 1);`, `memcpy(${name}.data, "${escaped}", ${len});`, `${name}.data[${len}] = 0;`, ].join("\n") + "\n"; } _genStringCopy(dst, src) { return "\n" + [ `free(${dst}.data);`, `${dst}.length = ${src}.length;`, `${dst}.data = malloc(${src}.length + 1);`, `memcpy(${dst}.data, ${src}.data, ${src}.length);`, `${dst}.data[${dst}.length] = 0;`, ].join("\n") + "\n"; } saveData() { return { target: this._el.target.value, value: this._el.value.value, index: this._el.indexInput.value }; } restoreData(d) { const tgt = this.querySelector(".assign-target"); tgt.dataset.restoredValue = d.target || ""; tgt.value = d.target || ""; this.querySelector(".assign-value").value = d.value || ""; this.querySelector(".assign-index").value = d.index || "0"; this._syncType(); this._updateValueVisibility(); } generateOwnCode() { const targetId = this._el.target.value; if (!targetId) return ""; const target = $id(targetId); if (!target) return ""; const targetName = resolveSourceName(target) || "x"; const targetType = target.dataType || "scalar"; const source = findDataSource(this._el.dataIn); if (targetType === "array") { const idxSource = findDataSource(this._el.indexHook); const idxExpr = idxSource ? this._getSourceVarName(idxSource) : (this._el.indexInput.value || "0"); if (!source) { const raw = this._el.value.value || "0"; return `\n${targetName}[${idxExpr}] = ${raw};\n`; } const srcName = this._getSourceVarName(source); return `\n${targetName}[${idxExpr}] = ${srcName};\n`; } if (!source) { const raw = this._el.value.value || "0"; if (targetType === "string") return this._genStringLiteral(targetName, raw); return `\n${targetName} = ${raw};\n`; } const srcName = this._getSourceVarName(source); if (targetType === "string" && source.dataType === "string") { return this._genStringCopy(targetName, srcName); } return `\n${targetName} = ${srcName};\n`; } } // ── WCVarRef — read-only variable reference with data output hook ────────── /** Read-only variable reference with data output hook; syncs type from selected variable. */ class WCVarRef extends WCBlock { connectedCallback() { super.connectedCallback(); this._el.select = this.querySelector(".varref-select"); this._el.select.addEventListener("change", () => { this._syncType(); }); } _syncType() { const id = this._el.select.value; const target = id ? $id(id) : null; const hook = this.querySelector(".hook-out"); hook.dataset.type = target ? target.dataType : "scalar"; } saveData() { return { selected: this._el.select.value }; } restoreData(d) { const sel = this.querySelector(".varref-select"); sel.dataset.restoredValue = d.selected || ""; sel.value = d.selected || ""; const id = sel.value; const target = id ? $id(id) : null; const hook = this.querySelector(".hook-out"); hook.dataset.type = target ? target.dataType : "scalar"; } generateOwnCode() { return ""; } } // ── WCExpression — mathematical expression with MathJax rendering & data hooks ── /** Math expression block with LaTeX rendering via MathJax and dynamic variable data hooks. */ class WCExpression extends WCBlock { get dataType() { const el = this._el?.latex || this.querySelector(".expr-latex"); const latex = el?.value || ""; return /[<>]|==|!=|\\(?:ge|le|ne|neq|geq|leq|land|lor|wedge|vee|neg|lnot)\b/.test(latex) ? "boolean" : "scalar"; } connectedCallback() { super.connectedCallback(); this._el.name = this.querySelector(".expr-name"); this._el.latex = this.querySelector(".expr-latex"); this._el.rendered = this.querySelector(".expr-rendered"); this._el.varhooks = this.querySelector(".expr-varhooks"); this._el.latex.addEventListener("input", () => this._renderFormula()); setTimeout(() => this._renderFormula(), 0); } saveData() { return { name: this._el.name.value, latex: this._el.latex.value }; } restoreData(d) { this._el.name.value = d.name || ""; this._el.latex.value = d.latex || ""; this._renderFormula(); } _parseVariables(latex) { const tokens = latex .replace(/\\[a-zA-Z]+/g, "") .replace(/[{}\[\]()]/g, " ") .replace(/[\d+*\/^_=,;:!?<>|~`@#\$%&'-]/g, " ") .split(/\s+/) .filter(t => t.length > 0 && t.length <= 8) .filter(t => ![ "sin","cos","tan","cot","sec","csc","arcsin","arccos","arctan", "log","ln","lg","exp","sqrt","lim","inf","sum","int","prod","det", "gcd","lcm","max","min","arg","mod","Re","Im","deg","Pr","var", "alpha","beta","gamma","delta","epsilon","zeta","eta","theta", "iota","kappa","lambda","mu","nu","xi","omicron","rho","sigma", "tau","upsilon","phi","chi","psi","omega","varepsilon","vartheta", "varpi","varphi","varrho","varsigma","varUpsilon" ].includes(t.toLowerCase())); return [...new Set(tokens)]; } _rebuildVarHooks(vars) { this._el.varhooks.innerHTML = ""; for (const v of vars) { const hook = document.createElement("button"); hook.className = "hook"; hook.id = this.id + "-vh-" + v; hook.title = v; hook.textContent = v; hook.dataset.direction = "in"; hook.dataset.linkType = "data"; hook.dataset.type = "scalar"; hook.dataset.exit = "left"; this._setupHook(hook); this._el.varhooks.appendChild(hook); } } _renderFormula() { const latex = this._el.latex.value; const outHook = this.querySelector(".hook-out"); if (outHook) outHook.dataset.type = this.dataType; this._el.rendered.innerHTML = ""; const vars = latex.trim() ? this._parseVariables(latex) : []; this._rebuildVarHooks(vars); if (!latex.trim()) return; if (window.MathJax && MathJax.typesetPromise) { const wrapper = document.createElement("div"); wrapper.textContent = "$$" + latex + "$$"; const typeset = () => { MathJax.typesetPromise([wrapper]).then(() => { this._el.rendered.innerHTML = wrapper.innerHTML; }).catch(() => { this._el.rendered.textContent = "\u26A0 error"; }); }; if (MathJax.startup && MathJax.startup.promise && MathJax.startup.promise.then) { MathJax.startup.promise.then(typeset); } else { typeset(); } } else { this._el.rendered.textContent = latex; } } generateCode(visited = new Set()) { if (visited.has(this)) return ""; visited.add(this); return this.generateOwnCode(); } generateOwnCode() { const name = this._el.name.value || "res"; const formula = this._el.latex.value || "0"; const type = this.dataType === "boolean" ? "int" : "double"; return `${type} ${name} = ${formula};`; } }