diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 641f426..6e8136c 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -209,13 +209,15 @@ function error(msg) { throw new Error(msg); } function inspect(x) { return JSON.stringify(x); } + + // ========================================================================= // Primitives // ========================================================================= var PRIMITIVES = {}; - // Arithmetic + // core.arithmetic PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; }; PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; }; PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; }; @@ -226,49 +228,31 @@ PRIMITIVES["abs"] = Math.abs; PRIMITIVES["floor"] = Math.floor; PRIMITIVES["ceil"] = Math.ceil; - PRIMITIVES["round"] = Math.round; + PRIMITIVES["round"] = function(x, n) { + if (n === undefined || n === 0) return Math.round(x); + var f = Math.pow(10, n); return Math.round(x * f) / f; + }; PRIMITIVES["min"] = Math.min; PRIMITIVES["max"] = Math.max; PRIMITIVES["sqrt"] = Math.sqrt; PRIMITIVES["pow"] = Math.pow; PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; - // Comparison - PRIMITIVES["="] = function(a, b) { return a == b; }; - PRIMITIVES["!="] = function(a, b) { return a != b; }; + + // core.comparison + PRIMITIVES["="] = function(a, b) { return a === b; }; + PRIMITIVES["!="] = function(a, b) { return a !== b; }; PRIMITIVES["<"] = function(a, b) { return a < b; }; PRIMITIVES[">"] = function(a, b) { return a > b; }; PRIMITIVES["<="] = function(a, b) { return a <= b; }; PRIMITIVES[">="] = function(a, b) { return a >= b; }; - // Logic + + // core.logic PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); }; - // String - PRIMITIVES["str"] = function() { - var p = []; - for (var i = 0; i < arguments.length; i++) { - var v = arguments[i]; if (isNil(v)) continue; p.push(String(v)); - } - return p.join(""); - }; - PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); }; - PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); }; - PRIMITIVES["trim"] = function(s) { return String(s).trim(); }; - PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); }; - PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); }; - PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); }; - PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; - PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; - PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; - PRIMITIVES["concat"] = function() { - var out = []; - for (var i = 0; i < arguments.length; i++) if (arguments[i]) out = out.concat(arguments[i]); - return out; - }; - PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); }; - // Predicates + // core.predicates PRIMITIVES["nil?"] = isNil; PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; @@ -284,7 +268,33 @@ PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; PRIMITIVES["zero?"] = function(n) { return n === 0; }; - // Collections + + // core.strings + PRIMITIVES["str"] = function() { + var p = []; + for (var i = 0; i < arguments.length; i++) { + var v = arguments[i]; if (isNil(v)) continue; p.push(String(v)); + } + return p.join(""); + }; + PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); }; + PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); }; + PRIMITIVES["trim"] = function(s) { return String(s).trim(); }; + PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); }; + PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); }; + PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); }; + PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); }; + PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; }; + PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; }; + PRIMITIVES["slice"] = function(c, a, b) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; + PRIMITIVES["concat"] = function() { + var out = []; + for (var i = 0; i < arguments.length; i++) if (!isNil(arguments[i])) out = out.concat(arguments[i]); + return out; + }; + + + // core.collections PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); }; PRIMITIVES["dict"] = function() { var d = {}; @@ -304,6 +314,15 @@ PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; + PRIMITIVES["chunk-every"] = function(c, n) { + var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; + }; + PRIMITIVES["zip-pairs"] = function(c) { + var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r; + }; + + + // core.dict PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); }; PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; }; PRIMITIVES["merge"] = function() { @@ -321,28 +340,16 @@ for (var i = 1; i < arguments.length; i++) delete out[arguments[i]]; return out; }; - PRIMITIVES["chunk-every"] = function(c, n) { - var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; - }; - PRIMITIVES["zip-pairs"] = function(c) { - var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r; - }; PRIMITIVES["into"] = function(target, coll) { if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll); var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } return r; }; - // Format + + // stdlib.format PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; }; - PRIMITIVES["pluralize"] = function(n, s, p) { - if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s"); - return n == 1 ? "" : "s"; - }; - PRIMITIVES["escape"] = function(s) { - return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); - }; PRIMITIVES["format-date"] = function(s, fmt) { if (!s) return ""; try { @@ -357,12 +364,21 @@ } catch (e) { return String(s); } }; PRIMITIVES["parse-datetime"] = function(s) { return s ? String(s) : NIL; }; - PRIMITIVES["split-ids"] = function(s) { - if (!s) return []; - return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; }); + + + // stdlib.text + PRIMITIVES["pluralize"] = function(n, s, p) { + if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s"); + return n == 1 ? "" : "s"; }; + PRIMITIVES["escape"] = function(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"); + }; + PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); }; + + + // stdlib.style PRIMITIVES["css"] = function() { - // Stub — CSSX requires style dictionary which is browser-only var atoms = []; for (var i = 0; i < arguments.length; i++) { var a = arguments[i]; @@ -383,6 +399,14 @@ return new StyleValue("sx-merged", allDecls, [], [], []); }; + + // stdlib.debug + PRIMITIVES["assert"] = function(cond, msg) { + if (!isSxTruthy(cond)) throw new Error("Assertion error: " + (msg || "Assertion failed")); + return true; + }; + + function isPrimitive(name) { return name in PRIMITIVES; } function getPrimitive(name) { return PRIMITIVES[name]; } @@ -508,6 +532,92 @@ return NIL; } + // ========================================================================= + // Platform: deps module — component dependency analysis + // ========================================================================= + + function componentDeps(c) { + return c.deps ? c.deps.slice() : []; + } + + function componentSetDeps(c, deps) { + c.deps = deps; + } + + function componentCssClasses(c) { + return c.cssClasses ? c.cssClasses.slice() : []; + } + + function envComponents(env) { + var names = []; + for (var k in env) { + var v = env[k]; + if (v && (v._component || v._macro)) names.push(k); + } + return names; + } + + function regexFindAll(pattern, source) { + var re = new RegExp(pattern, "g"); + var results = []; + var m; + while ((m = re.exec(source)) !== null) { + if (m[1] !== undefined) results.push(m[1]); + else results.push(m[0]); + } + return results; + } + + function scanCssClasses(source) { + var classes = {}; + var result = []; + var m; + var re1 = /:class\s+"([^"]*)"/g; + while ((m = re1.exec(source)) !== null) { + var parts = m[1].split(/\s+/); + for (var i = 0; i < parts.length; i++) { + if (parts[i] && !classes[parts[i]]) { + classes[parts[i]] = true; + result.push(parts[i]); + } + } + } + var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g; + while ((m = re2.exec(source)) !== null) { + var re3 = /"([^"]*)"/g; + var m2; + while ((m2 = re3.exec(m[1])) !== null) { + var parts2 = m2[1].split(/\s+/); + for (var j = 0; j < parts2.length; j++) { + if (parts2[j] && !classes[parts2[j]]) { + classes[parts2[j]] = true; + result.push(parts2[j]); + } + } + } + } + var re4 = /;;\s*@css\s+(.+)/g; + while ((m = re4.exec(source)) !== null) { + var parts3 = m[1].split(/\s+/); + for (var k = 0; k < parts3.length; k++) { + if (parts3[k] && !classes[parts3[k]]) { + classes[parts3[k]] = true; + result.push(parts3[k]); + } + } + } + return result; + } + + function componentIoRefs(c) { + return c.ioRefs ? c.ioRefs.slice() : []; + } + + function componentSetIoRefs(c, refs) { + c.ioRefs = refs; + } + + // ========================================================================= // Platform interface — Parser // ========================================================================= @@ -547,10 +657,10 @@ var args = rest(expr); return (isSxTruthy(!sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list"))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() { var name = symbolName(head); - return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() { + return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "letrec")) ? sfLetrec(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "defstyle")) ? sfDefstyle(args, env) : (isSxTruthy((name == "defkeyframes")) ? sfDefkeyframes(args, env) : (isSxTruthy((name == "defhandler")) ? sfDefhandler(args, env) : (isSxTruthy((name == "defpage")) ? sfDefpage(args, env) : (isSxTruthy((name == "defquery")) ? sfDefquery(args, env) : (isSxTruthy((name == "defaction")) ? sfDefaction(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "reset")) ? sfReset(args, env) : (isSxTruthy((name == "shift")) ? sfShift(args, env) : (isSxTruthy((name == "dynamic-wind")) ? sfDynamicWind(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() { var mac = envGet(env, name); return makeThunk(expandMacro(mac, args, env), env); -})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))); +})() : (isSxTruthy(isRenderExpr(expr)) ? renderExpr(expr, env) : evalCall(head, args, env))))))))))))))))))))))))))))))))))))))); })() : evalCall(head, args, env))); })(); }; @@ -651,7 +761,7 @@ })()); }; // sf-let - var sfLet = function(args, env) { return (function() { + var sfLet = function(args, env) { return (isSxTruthy((typeOf(first(args)) == "symbol")) ? sfNamedLet(args, env) : (function() { var bindings = first(args); var body = rest(args); var local = envExtend(env); @@ -668,6 +778,27 @@ })()); { var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } } return makeThunk(last(body), local); +})()); }; + + // sf-named-let + var sfNamedLet = function(args, env) { return (function() { + var loopName = symbolName(first(args)); + var bindings = nth(args, 1); + var body = slice(args, 2); + var params = []; + var inits = []; + (isSxTruthy((isSxTruthy((typeOf(first(bindings)) == "list")) && (len(first(bindings)) == 2))) ? forEach(function(binding) { params.push((isSxTruthy((typeOf(first(binding)) == "symbol")) ? symbolName(first(binding)) : first(binding))); +return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pairIdx) { return (append_b(params, (isSxTruthy((typeOf(nth(bindings, (pairIdx * 2))) == "symbol")) ? symbolName(nth(bindings, (pairIdx * 2))) : nth(bindings, (pairIdx * 2)))), append_b(inits, nth(bindings, ((pairIdx * 2) + 1)))); }, NIL, range(0, (len(bindings) / 2)))); + return (function() { + var loopBody = (isSxTruthy((len(body) == 1)) ? first(body) : cons(makeSymbol("begin"), body)); + var loopFn = makeLambda(params, loopBody, env); + loopFn.name = loopName; + lambdaClosure(loopFn)[loopName] = loopFn; + return (function() { + var initVals = map(function(e) { return trampoline(evalExpr(e, env)); }, inits); + return callLambda(loopFn, initVals, env); +})(); +})(); })(); }; // sf-lambda @@ -797,6 +928,49 @@ return value; })(); }; + // sf-letrec + var sfLetrec = function(args, env) { return (function() { + var bindings = first(args); + var body = rest(args); + var local = envExtend(env); + var names = []; + var valExprs = []; + (isSxTruthy((isSxTruthy((typeOf(first(bindings)) == "list")) && (len(first(bindings)) == 2))) ? forEach(function(binding) { return (function() { + var vname = (isSxTruthy((typeOf(first(binding)) == "symbol")) ? symbolName(first(binding)) : first(binding)); + names.push(vname); + valExprs.push(nth(binding, 1)); + return envSet(local, vname, NIL); +})(); }, bindings) : reduce(function(acc, pairIdx) { return (function() { + var vname = (isSxTruthy((typeOf(nth(bindings, (pairIdx * 2))) == "symbol")) ? symbolName(nth(bindings, (pairIdx * 2))) : nth(bindings, (pairIdx * 2))); + var valExpr = nth(bindings, ((pairIdx * 2) + 1)); + names.push(vname); + valExprs.push(valExpr); + return envSet(local, vname, NIL); +})(); }, NIL, range(0, (len(bindings) / 2)))); + (function() { + var values = map(function(e) { return trampoline(evalExpr(e, local)); }, valExprs); + { var _c = zip(names, values); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = nth(pair, 1); } } + return forEach(function(val) { return (isSxTruthy(isLambda(val)) ? forEach(function(n) { return envSet(lambdaClosure(val), n, envGet(local, n)); }, names) : NIL); }, values); +})(); + { var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } } + return makeThunk(last(body), local); +})(); }; + + // sf-dynamic-wind + var sfDynamicWind = function(args, env) { return (function() { + var before = trampoline(evalExpr(first(args), env)); + var body = trampoline(evalExpr(nth(args, 1), env)); + var after = trampoline(evalExpr(nth(args, 2), env)); + callThunk(before, env); + pushWind(before, after); + return (function() { + var result = callThunk(body, env); + popWind(); + callThunk(after, env); + return result; +})(); +})(); }; + // expand-macro var expandMacro = function(mac, rawArgs, env) { return (function() { var local = envMerge(macroClosure(mac), env); @@ -989,7 +1163,7 @@ return (isSxTruthy((pos >= lenSrc)) ? error("Unexpected end of input") : (functi return (isSxTruthy((ch == "(")) ? ((pos = (pos + 1)), readList(")")) : (isSxTruthy((ch == "[")) ? ((pos = (pos + 1)), readList("]")) : (isSxTruthy((ch == "{")) ? ((pos = (pos + 1)), readMap()) : (isSxTruthy((ch == "\"")) ? readString() : (isSxTruthy((ch == ":")) ? readKeyword() : (isSxTruthy((ch == "`")) ? ((pos = (pos + 1)), [makeSymbol("quasiquote"), readExpr()]) : (isSxTruthy((ch == ",")) ? ((pos = (pos + 1)), (isSxTruthy((isSxTruthy((pos < lenSrc)) && (nth(source, pos) == "@"))) ? ((pos = (pos + 1)), [makeSymbol("splice-unquote"), readExpr()]) : [makeSymbol("unquote"), readExpr()])) : (isSxTruthy(sxOr((isSxTruthy((ch >= "0")) && (ch <= "9")), (isSxTruthy((ch == "-")) && isSxTruthy(((pos + 1) < lenSrc)) && (function() { var nextCh = nth(source, (pos + 1)); return (isSxTruthy((nextCh >= "0")) && (nextCh <= "9")); -})()))) ? readNumber() : (isSxTruthy(isIdentStart(ch)) ? readSymbol() : error((String("Unexpected character: ") + String(ch)))))))))))); +})()))) ? readNumber() : (isSxTruthy((isSxTruthy((ch == ".")) && isSxTruthy(((pos + 2) < lenSrc)) && isSxTruthy((nth(source, (pos + 1)) == ".")) && (nth(source, (pos + 2)) == "."))) ? ((pos = (pos + 3)), makeSymbol("...")) : (isSxTruthy(isIdentStart(ch)) ? readSymbol() : error((String("Unexpected character: ") + String(ch))))))))))))); })()); }; return (function() { var exprs = []; @@ -1008,6 +1182,154 @@ continue; } else { return NIL; } } }; var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); }; + // === Transpiled from adapter-html === + + // render-to-html + var renderToHtml = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(expr); if (_m == "number") return (String(expr)); if (_m == "boolean") return (isSxTruthy(expr) ? "true" : "false"); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? "" : renderListToHtml(expr, env)); if (_m == "symbol") return renderValueToHtml(trampoline(evalExpr(expr, env)), env); if (_m == "keyword") return escapeHtml(keywordName(expr)); if (_m == "raw-html") return rawHtmlContent(expr); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); }; + + // render-value-to-html + var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); if (_m == "style-value") return styleValueClass(val); return escapeHtml((String(val))); })(); }; + + // RENDER_HTML_FORMS + var RENDER_HTML_FORMS = ["if", "when", "cond", "case", "let", "let*", "begin", "do", "define", "defcomp", "defmacro", "defstyle", "defkeyframes", "defhandler", "map", "map-indexed", "filter", "for-each"]; + + // render-html-form? + var isRenderHtmlForm = function(name) { return contains(RENDER_HTML_FORMS, name); }; + + // render-list-to-html + var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() { + var head = first(expr); + return (isSxTruthy(!(typeOf(head) == "symbol")) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() { + var name = symbolName(head); + var args = rest(expr); + return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() { + var val = envGet(env, name); + return (isSxTruthy(isComponent(val)) ? renderHtmlComponent(val, args, env) : (isSxTruthy(isMacro(val)) ? renderToHtml(expandMacro(val, args, env), env) : error((String("Unknown component: ") + String(name))))); +})() : (isSxTruthy(isRenderHtmlForm(name)) ? dispatchHtmlForm(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(expandMacro(envGet(env, name), args, env), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env))))))); +})()); +})()); }; + + // dispatch-html-form + var dispatchHtmlForm = function(name, expr, env) { return (isSxTruthy((name == "if")) ? (function() { + var condVal = trampoline(evalExpr(nth(expr, 1), env)); + return (isSxTruthy(condVal) ? renderToHtml(nth(expr, 2), env) : (isSxTruthy((len(expr) > 3)) ? renderToHtml(nth(expr, 3), env) : "")); +})() : (isSxTruthy((name == "when")) ? (isSxTruthy(!trampoline(evalExpr(nth(expr, 1), env))) ? "" : join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(2, len(expr))))) : (isSxTruthy((name == "cond")) ? (function() { + var branch = evalCond(rest(expr), env); + return (isSxTruthy(branch) ? renderToHtml(branch, env) : ""); +})() : (isSxTruthy((name == "case")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy(sxOr((name == "let"), (name == "let*"))) ? (function() { + var local = processBindings(nth(expr, 1), env); + return join("", map(function(i) { return renderToHtml(nth(expr, i), local); }, range(2, len(expr)))); +})() : (isSxTruthy(sxOr((name == "begin"), (name == "do"))) ? join("", map(function(i) { return renderToHtml(nth(expr, i), env); }, range(1, len(expr)))) : (isSxTruthy(isDefinitionForm(name)) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((name == "map")) ? (function() { + var f = trampoline(evalExpr(nth(expr, 1), env)); + var coll = trampoline(evalExpr(nth(expr, 2), env)); + return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); +})() : (isSxTruthy((name == "map-indexed")) ? (function() { + var f = trampoline(evalExpr(nth(expr, 1), env)); + var coll = trampoline(evalExpr(nth(expr, 2), env)); + return join("", mapIndexed(function(i, item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [i, item], env) : renderToHtml(apply(f, [i, item]), env)); }, coll)); +})() : (isSxTruthy((name == "filter")) ? renderToHtml(trampoline(evalExpr(expr, env)), env) : (isSxTruthy((name == "for-each")) ? (function() { + var f = trampoline(evalExpr(nth(expr, 1), env)); + var coll = trampoline(evalExpr(nth(expr, 2), env)); + return join("", map(function(item) { return (isSxTruthy(isLambda(f)) ? renderLambdaHtml(f, [item], env) : renderToHtml(apply(f, [item]), env)); }, coll)); +})() : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))))))))); }; + + // render-lambda-html + var renderLambdaHtml = function(f, args, env) { return (function() { + var local = envMerge(lambdaClosure(f), env); + forEachIndexed(function(i, p) { return envSet(local, p, nth(args, i)); }, lambdaParams(f)); + return renderToHtml(lambdaBody(f), local); +})(); }; + + // render-html-component + var renderHtmlComponent = function(comp, args, env) { return (function() { + var kwargs = {}; + var children = []; + reduce(function(state, arg) { return (function() { + var skip = get(state, "skip"); + return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { + var val = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); + kwargs[keywordName(arg)] = val; + return assoc(state, "skip", true, "i", (get(state, "i") + 1)); +})() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1))))); +})(); }, {["i"]: 0, ["skip"]: false}, args); + return (function() { + var local = envMerge(componentClosure(comp), env); + { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = (isSxTruthy(dictHas(kwargs, p)) ? dictGet(kwargs, p) : NIL); } } + if (isSxTruthy(componentHasChildren(comp))) { + local["children"] = makeRawHtml(join("", map(function(c) { return renderToHtml(c, env); }, children))); +} + return renderToHtml(componentBody(comp), local); +})(); +})(); }; + + // render-html-element + var renderHtmlElement = function(tag, args, env) { return (function() { + var parsed = parseElementArgs(args, env); + var attrs = first(parsed); + var children = nth(parsed, 1); + var isVoid = contains(VOID_ELEMENTS, tag); + return (String("<") + String(tag) + String(renderAttrs(attrs)) + String((isSxTruthy(isVoid) ? " />" : (String(">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String("") + String(tag) + String(">"))))); +})(); }; + + + // === Transpiled from adapter-sx === + + // render-to-sx + var renderToSx = function(expr, env) { return (function() { + var result = aser(expr, env); + return (isSxTruthy((typeOf(result) == "string")) ? result : serialize(result)); +})(); }; + + // aser + var aser = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { + var name = symbolName(expr); + return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); +})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); return expr; })(); }; + + // aser-list + var aserList = function(expr, env) { return (function() { + var head = first(expr); + var args = rest(expr); + return (isSxTruthy(!(typeOf(head) == "symbol")) ? map(function(x) { return aser(x, env); }, expr) : (function() { + var name = symbolName(head); + return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy(startsWith(name, "~")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() { + var f = trampoline(evalExpr(head, env)); + var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args); + return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isLambda(f)) && !isComponent(f))) ? apply(f, evaledArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, evaledArgs, env)) : (isSxTruthy(isComponent(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : error((String("Not callable: ") + String(inspect(f))))))); +})()))))); +})()); +})(); }; + + // aser-fragment + var aserFragment = function(children, env) { return (function() { + var parts = filter(function(x) { return !isNil(x); }, map(function(c) { return aser(c, env); }, children)); + return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", map(serialize, parts))) + String(")"))); +})(); }; + + // aser-call + var aserCall = function(name, args, env) { return (function() { + var parts = [name]; + reduce(function(state, arg) { return (function() { + var skip = get(state, "skip"); + return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (get(state, "i") + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { + var val = aser(nth(args, (get(state, "i") + 1)), env); + if (isSxTruthy(!isNil(val))) { + parts.push((String(":") + String(keywordName(arg)))); + parts.push(serialize(val)); +} + return assoc(state, "skip", true, "i", (get(state, "i") + 1)); +})() : (function() { + var val = aser(arg, env); + if (isSxTruthy(!isNil(val))) { + parts.push(serialize(val)); +} + return assoc(state, "i", (get(state, "i") + 1)); +})())); +})(); }, {["i"]: 0, ["skip"]: false}, args); + return (String("(") + String(join(" ", parts)) + String(")")); +})(); }; + + // === Transpiled from adapter-dom === // SVG_NS @@ -1663,7 +1985,7 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\" if (isSxTruthy(!domHasAttr(link, "sx-push-url"))) { domSetAttr(link, "sx-push-url", "true"); } - bindBoostLink(link, domGetAttr(link, "href")); + bindClientRouteLink(link, domGetAttr(link, "href")); } } } return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form, "boost")) && shouldBoostForm(form))) ? (markProcessed(form, "boost"), (function() { var method = upper(sxOr(domGetAttr(form, "method"), "GET")); @@ -1677,6 +1999,27 @@ return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isProcessed(form return bindBoostForm(form, method, action); })()) : NIL); }, domQueryAll(container, "form")); }; + // try-client-route + var tryClientRoute = function(pathname) { return (function() { + var match = findMatchingRoute(pathname, _pageRoutes); + return (isSxTruthy(isNil(match)) ? false : (isSxTruthy(get(match, "has-data")) ? false : (function() { + var contentSrc = get(match, "content"); + var closure = sxOr(get(match, "closure"), {}); + var params = get(match, "params"); + return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? false : (function() { + var env = merge(closure, params); + var rendered = tryEvalContent(contentSrc, env); + return (isSxTruthy(isNil(rendered)) ? false : (function() { + var target = domQueryById("main-panel"); + return (isSxTruthy(isNil(target)) ? false : (domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), logInfo((String("sx:route client ") + String(pathname))), true)); +})()); +})()); +})())); +})(); }; + + // bind-client-route-link + var bindClientRouteLink = function(link, href) { return bindClientRouteClick(link, href, function() { return bindBoostLink(link, href); }); }; + // process-sse var processSse = function(root) { return forEach(function(el) { return (isSxTruthy(!isProcessed(el, "sse")) ? (markProcessed(el, "sse"), bindSse(el)) : NIL); }, domQueryAll(sxOr(root, domBody()), "[sx-sse]")); }; @@ -1758,8 +2101,11 @@ return bindInlineHandlers(root); }; var main = domQueryById("main-panel"); var url = browserLocationHref(); return (isSxTruthy(main) ? (function() { + var pathname = urlPathname(url); + return (isSxTruthy(tryClientRoute(pathname)) ? browserScrollTo(0, scrollY) : (function() { var headers = buildRequestHeaders(main, loadedComponentNames(), _cssHash); return fetchAndRestore(main, url, headers, scrollY); +})()); })() : NIL); })(); }; @@ -2061,10 +2407,208 @@ callExpr.push(dictGet(kwargs, k)); } } return setSxStylesCookie(hash); })()); })()) : NIL); }, scripts); +})(); }; + + // _page-routes + var _pageRoutes = []; + + // process-page-scripts + var processPageScripts = function() { return (function() { + var scripts = queryPageScripts(); + return forEach(function(s) { return (isSxTruthy(!isProcessed(s, "pages")) ? (markProcessed(s, "pages"), (function() { + var text = domTextContent(s); + return (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? (function() { + var pages = parse(text); + return forEach(function(page) { return append_b(_pageRoutes, merge(page, {'parsed': [Symbol('parse-route-pattern'), [Symbol('get'), Symbol('page'), 'path']]})); }, pages); +})() : NIL); +})()) : NIL); }, scripts); })(); }; // boot-init - var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); }; + var bootInit = function() { return (initCssTracking(), initStyleDict(), processSxScripts(NIL), processPageScripts(), sxHydrateElements(NIL), processElements(NIL)); }; + + + // === Transpiled from deps (component dependency analysis) === + + // scan-refs + var scanRefs = function(node) { return (function() { + var refs = []; + scanRefsWalk(node, refs); + return refs; +})(); }; + + // scan-refs-walk + var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { + var name = symbolName(node); + return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!contains(refs, name)) ? append_b(refs, name) : NIL) : NIL); +})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); }; + + // transitive-deps-walk + var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!contains(seen, n)) ? (append_b(seen, n), (function() { + var val = envGet(env, n); + return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL)); +})()) : NIL); }; + + // transitive-deps + var transitiveDeps = function(name, env) { return (function() { + var seen = []; + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + transitiveDepsWalk(key, seen, env); + return filter(function(x) { return !(x == key); }, seen); +})(); }; + + // compute-all-deps + var computeAllDeps = function(env) { return forEach(function(name) { return (function() { + var val = envGet(env, name); + return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL); +})(); }, envComponents(env)); }; + + // scan-components-from-source + var scanComponentsFromSource = function(source) { return (function() { + var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source); + return map(function(m) { return (String("~") + String(m)); }, matches); +})(); }; + + // components-needed + var componentsNeeded = function(pageSource, env) { return (function() { + var direct = scanComponentsFromSource(pageSource); + var allNeeded = []; + { var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!contains(allNeeded, name))) { + allNeeded.push(name); +} +(function() { + var val = envGet(env, name); + return (function() { + var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isEmpty(componentDeps(val)))) ? componentDeps(val) : transitiveDeps(name, env)); + return forEach(function(dep) { return (isSxTruthy(!contains(allNeeded, dep)) ? append_b(allNeeded, dep) : NIL); }, deps); +})(); +})(); } } + return allNeeded; +})(); }; + + // page-component-bundle + var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); }; + + // page-css-classes + var pageCssClasses = function(pageSource, env) { return (function() { + var needed = componentsNeeded(pageSource, env); + var classes = []; + { var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() { + var val = envGet(env, name); + return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!contains(classes, cls)) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL); +})(); } } + { var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!contains(classes, cls))) { + classes.push(cls); +} } } + return classes; +})(); }; + + // scan-io-refs-walk + var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() { + var name = symbolName(node); + return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!contains(refs, name)) ? append_b(refs, name) : NIL) : NIL); +})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); }; + + // scan-io-refs + var scanIoRefs = function(node, ioNames) { return (function() { + var refs = []; + scanIoRefsWalk(node, ioNames, refs); + return refs; +})(); }; + + // transitive-io-refs-walk + var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!contains(seen, n)) ? (append_b(seen, n), (function() { + var val = envGet(env, n); + return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!contains(allRefs, ref)) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!contains(allRefs, ref)) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL)); +})()) : NIL); }; + + // transitive-io-refs + var transitiveIoRefs = function(name, env, ioNames) { return (function() { + var allRefs = []; + var seen = []; + var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name))); + transitiveIoRefsWalk(key, seen, allRefs, env, ioNames); + return allRefs; +})(); }; + + // compute-all-io-refs + var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() { + var val = envGet(env, name); + return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL); +})(); }, envComponents(env)); }; + + // component-pure? + var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); }; + + + // === Transpiled from router (client-side route matching) === + + // split-path-segments + var splitPathSegments = function(path) { return (function() { + var trimmed = (isSxTruthy(startsWith(path, "/")) ? slice(path, 1) : path); + return (function() { + var trimmed2 = (isSxTruthy((isSxTruthy(!isEmpty(trimmed)) && endsWith(trimmed, "/"))) ? slice(trimmed, 0, (length(trimmed) - 1)) : trimmed); + return (isSxTruthy(isEmpty(trimmed2)) ? [] : split(trimmed2, "/")); +})(); +})(); }; + + // make-route-segment + var makeRouteSegment = function(seg) { return (isSxTruthy((isSxTruthy(startsWith(seg, "<")) && endsWith(seg, ">"))) ? (function() { + var paramName = slice(seg, 1, (length(seg) - 1)); + return (function() { + var d = {}; + d["type"] = "param"; + d["value"] = paramName; + return d; +})(); +})() : (function() { + var d = {}; + d["type"] = "literal"; + d["value"] = seg; + return d; +})()); }; + + // parse-route-pattern + var parseRoutePattern = function(pattern) { return (function() { + var segments = splitPathSegments(pattern); + return map(makeRouteSegment, segments); +})(); }; + + // match-route-segments + var matchRouteSegments = function(pathSegs, parsedSegs) { return (isSxTruthy(!(length(pathSegs) == length(parsedSegs))) ? NIL : (function() { + var params = {}; + var matched = true; + forEachIndexed(function(i, parsedSeg) { return (isSxTruthy(matched) ? (function() { + var pathSeg = nth(pathSegs, i); + var segType = get(parsedSeg, "type"); + return (isSxTruthy((segType == "literal")) ? (isSxTruthy(!(pathSeg == get(parsedSeg, "value"))) ? (matched = false) : NIL) : (isSxTruthy((segType == "param")) ? dictSet(params, get(parsedSeg, "value"), pathSeg) : (matched = false))); +})() : NIL); }, parsedSegs); + return (isSxTruthy(matched) ? params : NIL); +})()); }; + + // match-route + var matchRoute = function(path, pattern) { return (function() { + var pathSegs = splitPathSegments(path); + var parsedSegs = parseRoutePattern(pattern); + return matchRouteSegments(pathSegs, parsedSegs); +})(); }; + + // find-matching-route + var findMatchingRoute = function(path, routes) { return (function() { + var pathSegs = splitPathSegments(path); + var result = NIL; + { var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var route = _c[_i]; if (isSxTruthy(isNil(result))) { + (function() { + var params = matchRouteSegments(pathSegs, get(route, "parsed")); + return (isSxTruthy(!isNil(params)) ? (function() { + var matched = merge(route, {}); + matched["params"] = params; + return (result = matched); +})() : NIL); +})(); +} } } + return result; +})(); }; // ========================================================================= @@ -2711,6 +3255,50 @@ callExpr.push(dictGet(kwargs, k)); } } }); } + // --- Client-side route bindings --- + + function bindClientRouteClick(link, href, fallbackFn) { + link.addEventListener("click", function(e) { + e.preventDefault(); + var pathname = urlPathname(href); + if (tryClientRoute(pathname)) { + try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + if (typeof window !== "undefined") window.scrollTo(0, 0); + } else { + logInfo("sx:route server " + pathname); + executeRequest(link, { method: "GET", url: href }).then(function() { + try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {} + }); + } + }); + } + + function tryEvalContent(source, env) { + try { + var merged = merge(componentEnv); + if (env && !isNil(env)) { + var ks = Object.keys(env); + for (var i = 0; i < ks.length; i++) merged[ks[i]] = env[ks[i]]; + } + return sxRenderWithEnv(source, merged); + } catch (e) { + return NIL; + } + } + + function urlPathname(href) { + try { + return new URL(href, location.href).pathname; + } catch (e) { + // Fallback: strip query/hash + var idx = href.indexOf("?"); + if (idx >= 0) href = href.substring(0, idx); + idx = href.indexOf("#"); + if (idx >= 0) href = href.substring(0, idx); + return href; + } + } + // --- Inline handlers --- function bindInlineHandler(el, eventName, body) { @@ -2981,6 +3569,12 @@ callExpr.push(dictGet(kwargs, k)); } } document.querySelectorAll('script[type="text/sx-styles"]')); } + function queryPageScripts() { + if (!_hasDom) return []; + return Array.prototype.slice.call( + document.querySelectorAll('script[type="text/sx-pages"]')); + } + // --- localStorage --- function localStorageGet(key) { @@ -3076,6 +3670,9 @@ callExpr.push(dictGet(kwargs, k)); } } }; // Expose render functions as primitives so SX code can call them + if (typeof renderToHtml === "function") PRIMITIVES["render-to-html"] = renderToHtml; + if (typeof renderToSx === "function") PRIMITIVES["render-to-sx"] = renderToSx; + if (typeof aser === "function") PRIMITIVES["aser"] = aser; if (typeof renderToDom === "function") PRIMITIVES["render-to-dom"] = renderToDom; // Parser — compiled from parser.sx (see PLATFORM_PARSER_JS for ident char classes) @@ -3095,12 +3692,25 @@ callExpr.push(dictGet(kwargs, k)); } } } function render(source) { + if (!_hasDom) { + var exprs = parse(source); + var parts = []; + for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); + return parts.join(""); + } var exprs = parse(source); var frag = document.createDocumentFragment(); for (var i = 0; i < exprs.length; i++) frag.appendChild(renderToDom(exprs[i], merge(componentEnv), null)); return frag; } + function renderToString(source) { + var exprs = parse(source); + var parts = []; + for (var i = 0; i < exprs.length; i++) parts.push(renderToHtml(exprs[i], merge(componentEnv))); + return parts.join(""); + } + var Sx = { VERSION: "ref-2.0", parse: parse, @@ -3108,7 +3718,7 @@ callExpr.push(dictGet(kwargs, k)); } } eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); }, loadComponents: loadComponents, render: render, - + renderToString: renderToString, serialize: serialize, NIL: NIL, Symbol: Symbol, @@ -3116,6 +3726,8 @@ callExpr.push(dictGet(kwargs, k)); } } isTruthy: isSxTruthy, isNil: isNil, componentEnv: componentEnv, + renderToHtml: function(expr, env) { return renderToHtml(expr, env || merge(componentEnv)); }, + renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); }, renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null, parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null, morphNode: typeof morphNode === "function" ? morphNode : null, @@ -3131,7 +3743,21 @@ callExpr.push(dictGet(kwargs, k)); } } renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null, getEnv: function() { return componentEnv; }, init: typeof bootInit === "function" ? bootInit : null, - _version: "ref-2.0 (boot+cssx+dom+engine+orchestration+parser, bootstrap-compiled)" + scanRefs: scanRefs, + transitiveDeps: transitiveDeps, + computeAllDeps: computeAllDeps, + componentsNeeded: componentsNeeded, + pageComponentBundle: pageComponentBundle, + pageCssClasses: pageCssClasses, + scanIoRefs: scanIoRefs, + transitiveIoRefs: transitiveIoRefs, + computeAllIoRefs: computeAllIoRefs, + componentPure_p: componentPure_p, + splitPathSegments: splitPathSegments, + parseRoutePattern: parseRoutePattern, + matchRoute: matchRoute, + findMatchingRoute: findMatchingRoute, + _version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; @@ -3154,4 +3780,4 @@ callExpr.push(dictGet(kwargs, k)); } } if (typeof module !== "undefined" && module.exports) module.exports = Sx; else global.Sx = Sx; -})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this); \ No newline at end of file +})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this); diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 88654e4..98a1726 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -631,6 +631,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
+ @@ -638,6 +639,66 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details. """ +def _build_pages_sx(service: str) -> str: + """Build SX page registry for client-side routing. + + Returns SX dict literals (one per page) parseable by the client's + ``parse`` function. Each dict has keys: name, path, auth, has-data, + content, closure. + """ + from .pages import get_all_pages + from .parser import serialize as sx_serialize + + pages = get_all_pages(service) + if not pages: + return "" + + entries = [] + for page_def in pages.values(): + content_src = "" + if page_def.content_expr is not None: + try: + content_src = sx_serialize(page_def.content_expr) + except Exception: + pass + + auth = page_def.auth if isinstance(page_def.auth, str) else "custom" + has_data = "true" if page_def.data_expr is not None else "false" + + # Build closure as SX dict + closure_parts: list[str] = [] + for k, v in page_def.closure.items(): + if isinstance(v, (str, int, float, bool)): + closure_parts.append(f":{k} {_sx_literal(v)}") + closure_sx = "{" + " ".join(closure_parts) + "}" + + entry = ( + "{:name " + _sx_literal(page_def.name) + + " :path " + _sx_literal(page_def.path) + + " :auth " + _sx_literal(auth) + + " :has-data " + has_data + + " :content " + _sx_literal(content_src) + + " :closure " + closure_sx + "}" + ) + entries.append(entry) + + return "\n".join(entries) + + +def _sx_literal(v: object) -> str: + """Serialize a Python value as an SX literal.""" + if v is None: + return "nil" + if isinstance(v, bool): + return "true" if v else "false" + if isinstance(v, (int, float)): + return str(v) + if isinstance(v, str): + escaped = v.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + return f'"{escaped}"' + return "nil" + + def sx_page(ctx: dict, page_sx: str, *, meta_html: str = "") -> str: """Return a minimal HTML shell that boots the page from sx source. @@ -692,6 +753,14 @@ def sx_page(ctx: dict, page_sx: str, *, else: styles_json = _build_style_dict_json() + # Page registry for client-side routing + pages_sx = "" + try: + from quart import current_app + pages_sx = _build_pages_sx(current_app.name) + except Exception: + pass + return _SX_PAGE_TEMPLATE.format( title=_html_escape(title), asset_url=asset_url, @@ -701,6 +770,7 @@ def sx_page(ctx: dict, page_sx: str, *, component_defs=component_defs, styles_hash=styles_hash, styles_json=styles_json, + pages_sx=pages_sx, page_sx=page_sx, sx_css=sx_css, sx_css_classes=sx_css_classes, diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx index 94dc7dc..41e7fec 100644 --- a/shared/sx/ref/boot.sx +++ b/shared/sx/ref/boot.sx @@ -295,6 +295,33 @@ scripts)))) +;; -------------------------------------------------------------------------- +;; Page registry for client-side routing +;; -------------------------------------------------------------------------- + +(define _page-routes (list)) + +(define process-page-scripts + (fn () + ;; Process