// Safe DOM builder used by all components / views. Never assigns // innerHTML from API data — all text is set via textContent (auto-escaped // by the browser), all structure via createElement. // // Usage: // el('div', { class: 'card', onclick: fn }, 'title', el('span', {}, 'sub')) // Children may be strings (becomes text node), Nodes, arrays of either, // or null/undefined (ignored). export function el(tag, attrs = {}, ...children) { const node = document.createElement(tag); for (const [k, v] of Object.entries(attrs || {})) { if (v === null || v === undefined || v === false) continue; if (k === 'class') node.className = v; else if (k === 'style' && typeof v === 'object') Object.assign(node.style, v); else if (k.startsWith('on') && typeof v === 'function') node.addEventListener(k.slice(2).toLowerCase(), v); else if (k === 'dataset' && typeof v === 'object') Object.assign(node.dataset, v); else if (k === 'html') { // Explicit, scary-named opt-in. ONLY use with strings produced by // marked() or other vetted sanitizers — never with raw API data. node.innerHTML = v; } else if (k in node && typeof node[k] !== 'function') { try { node[k] = v; } catch { node.setAttribute(k, v); } } else { node.setAttribute(k, v); } } appendAll(node, children); return node; } function appendAll(parent, children) { for (const c of children) { if (c === null || c === undefined || c === false) continue; if (Array.isArray(c)) appendAll(parent, c); else if (c instanceof Node) parent.appendChild(c); else parent.appendChild(document.createTextNode(String(c))); } } export function clear(node) { while (node.firstChild) node.removeChild(node.firstChild); } export function mount(node, ...children) { clear(node); appendAll(node, children); } // Validate a URL before using it as href/src. Agent-suggested content // could carry a `javascript:` scheme that would execute in the owner's // browser context when clicked. Only http(s)/mailto/relative pass. export function safeHref(u) { if (!u) return '#'; try { const url = new URL(u, location.origin); if (url.protocol === 'http:' || url.protocol === 'https:' || url.protocol === 'mailto:') return url.href; return '#'; } catch { return '#'; } }