/** * sx-test.js — String renderer for sx.js (Node-only, used by test harness). * * Provides Sx.renderToString() for server-side / test rendering. * Assumes sx.js is loaded first and Sx global is available. */ ;(function (Sx) { "use strict"; // Pull references from Sx internals var NIL = Sx.NIL; var _eval = Sx._eval; var _types = Sx._types; var RawHTML = _types.RawHTML; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSym(x) { return x && x._sym === true; } function isKw(x) { return x && x._kw === true; } function isLambda(x) { return x && x._lambda === true; } function isComponent(x) { return x && x._component === true; } function isMacro(x) { return x && x._macro === true; } function isRaw(x) { return x && x._raw === true; } function isSxTruthy(x) { return x !== false && !isNil(x); } function merge(target) { for (var i = 1; i < arguments.length; i++) { var src = arguments[i]; if (src) for (var k in src) target[k] = src[k]; } return target; } // Use the same tag/attr sets as sx.js var HTML_TAGS = Sx._renderDOM ? null : null; // We'll use a local copy var _HTML_TAGS_STR = "html head body title meta link style script base noscript " + "header footer main nav aside section article address hgroup " + "h1 h2 h3 h4 h5 h6 " + "div p blockquote pre figure figcaption ul ol li dl dt dd hr " + "a span em strong small s cite q abbr code var samp kbd sub sup " + "i b u mark ruby rt rp bdi bdo br wbr time data " + "ins del " + "img picture source iframe embed object param video audio track canvas map area " + "table caption colgroup col thead tbody tfoot tr td th " + "form input textarea button select option optgroup label fieldset legend " + "details summary dialog " + "svg path circle rect line ellipse polyline polygon text g defs use " + "clippath lineargradient radialgradient stop pattern mask " + "tspan textpath foreignobject"; var _VOID_STR = "area base br col embed hr img input link meta param source track wbr"; var _BOOL_STR = "disabled checked readonly required selected autofocus autoplay " + "controls loop muted multiple hidden open novalidate"; function makeSet(str) { var s = {}, parts = str.split(/\s+/); for (var i = 0; i < parts.length; i++) if (parts[i]) s[parts[i]] = true; return s; } HTML_TAGS = makeSet(_HTML_TAGS_STR); var VOID_ELEMENTS = makeSet(_VOID_STR); var BOOLEAN_ATTRS = makeSet(_BOOL_STR); // Access expandMacro via Sx._eval on a defmacro — we need to replicate macro expansion // Actually, we need the internal expandMacro. Let's check if Sx exposes it. // Sx._eval handles macro expansion internally, so we can call sxEval for macro forms. var sxEval = _eval; // _isRenderExpr — check if an expression is a render-only form function _isRenderExpr(v) { if (!Array.isArray(v) || !v.length) return false; var h = v[0]; if (!isSym(h)) return false; var n = h.name; if (n === "<>" || n === "raw!" || n === "if" || n === "when" || n === "cond" || n === "case" || n === "let" || n === "let*" || n === "begin" || n === "do" || n === "map" || n === "map-indexed" || n === "filter" || n === "for-each") return true; if (n.charAt(0) === "~") return true; if (HTML_TAGS[n]) return true; return false; } // --- String Renderer --- function escapeText(s) { return s.replace(/&/g, "&").replace(//g, ">"); } function escapeAttr(s) { return s.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } function renderStr(expr, env) { if (isNil(expr) || expr === false || expr === true) return ""; if (isRaw(expr)) return expr.html; if (typeof expr === "string") return escapeText(expr); if (typeof expr === "number") return escapeText(String(expr)); if (isSym(expr)) return renderStr(sxEval(expr, env), env); if (isKw(expr)) return escapeText(expr.name); if (Array.isArray(expr)) { if (!expr.length) return ""; return renderStrList(expr, env); } if (expr && typeof expr === "object") return ""; return escapeText(String(expr)); } function renderStrList(expr, env) { var head = expr[0]; if (!isSym(head)) { var parts = []; for (var i = 0; i < expr.length; i++) parts.push(renderStr(expr[i], env)); return parts.join(""); } var name = head.name; if (name === "raw!") { var ps = []; for (var ri = 1; ri < expr.length; ri++) { var v = sxEval(expr[ri], env); if (isRaw(v)) ps.push(v.html); else if (typeof v === "string") ps.push(v); else if (!isNil(v)) ps.push(String(v)); } return ps.join(""); } if (name === "<>") { var fs = []; for (var fi = 1; fi < expr.length; fi++) fs.push(renderStr(expr[fi], env)); return fs.join(""); } if (name === "if") { return isSxTruthy(sxEval(expr[1], env)) ? renderStr(expr[2], env) : (expr.length > 3 ? renderStr(expr[3], env) : ""); } if (name === "when") { if (!isSxTruthy(sxEval(expr[1], env))) return ""; var ws = []; for (var wi = 2; wi < expr.length; wi++) ws.push(renderStr(expr[wi], env)); return ws.join(""); } if (name === "let" || name === "let*") { var bindings = expr[1], local = merge({}, env); if (Array.isArray(bindings)) { if (bindings.length && Array.isArray(bindings[0])) { for (var li = 0; li < bindings.length; li++) { local[isSym(bindings[li][0]) ? bindings[li][0].name : bindings[li][0]] = sxEval(bindings[li][1], local); } } else { for (var lj = 0; lj < bindings.length; lj += 2) { local[isSym(bindings[lj]) ? bindings[lj].name : bindings[lj]] = sxEval(bindings[lj + 1], local); } } } var ls = []; for (var lk = 2; lk < expr.length; lk++) ls.push(renderStr(expr[lk], local)); return ls.join(""); } if (name === "begin" || name === "do") { var bs = []; for (var bi = 1; bi < expr.length; bi++) bs.push(renderStr(expr[bi], env)); return bs.join(""); } if (name === "define" || name === "defcomp" || name === "defmacro" || name === "defhandler") { sxEval(expr, env); return ""; } // Macro expansion in string renderer if (name in env && isMacro(env[name])) { var smExp = Sx._expandMacro(env[name], expr.slice(1), env); return renderStr(smExp, env); } // Higher-order forms — render-aware if (name === "map") { var mapFn = sxEval(expr[1], env), mapColl = sxEval(expr[2], env); if (!Array.isArray(mapColl)) return ""; var mapParts = []; for (var mi = 0; mi < mapColl.length; mi++) { if (isLambda(mapFn)) mapParts.push(renderLambdaStr(mapFn, [mapColl[mi]], env)); else mapParts.push(renderStr(mapFn(mapColl[mi]), env)); } return mapParts.join(""); } if (name === "map-indexed") { var mixFn = sxEval(expr[1], env), mixColl = sxEval(expr[2], env); if (!Array.isArray(mixColl)) return ""; var mixParts = []; for (var mxi = 0; mxi < mixColl.length; mxi++) { if (isLambda(mixFn)) mixParts.push(renderLambdaStr(mixFn, [mxi, mixColl[mxi]], env)); else mixParts.push(renderStr(mixFn(mxi, mixColl[mxi]), env)); } return mixParts.join(""); } if (name === "filter") { var filtFn = sxEval(expr[1], env), filtColl = sxEval(expr[2], env); if (!Array.isArray(filtColl)) return ""; var filtParts = []; for (var fli = 0; fli < filtColl.length; fli++) { var keep = isLambda(filtFn) ? Sx._callLambda(filtFn, [filtColl[fli]], env) : filtFn(filtColl[fli]); if (isSxTruthy(keep)) filtParts.push(renderStr(filtColl[fli], env)); } return filtParts.join(""); } if (HTML_TAGS[name]) return renderStrElement(name, expr.slice(1), env); if (name.charAt(0) === "~") { var comp = env[name]; if (isComponent(comp)) return renderStrComponent(comp, expr.slice(1), env); console.warn("sx.js: unknown component " + name); return '