Update reference SX spec to match sx.js macros branch (CSSX, dict literals, new primitives)

- eval.sx: Add defstyle, defkeyframes, defhandler special forms; add ho-for-each
- parser.sx: Add dict {...} literal parsing and quasiquote/unquote sugar
- primitives.sx: Add parse-datetime, split-ids, css, merge-styles primitives
- render.sx: Add StyleValue handling, SVG filter elements, definition forms in render, fix render-to-html to handle HTML tags directly
- bootstrap_js.py: Add StyleValue type, buildKeyframes, isEvery platform helper, new primitives (format-date, parse-datetime, split-ids, css, merge-styles), dict/quasiquote parser, expose render functions as primitives
- sx-ref.js: Regenerated — 132/132 tests passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 10:17:28 +00:00
parent 4a3a510a23
commit a9526c4fa1
6 changed files with 406 additions and 29 deletions

View File

@@ -58,6 +58,15 @@
function RawHTML(html) { this.html = html; } function RawHTML(html) { this.html = html; }
RawHTML.prototype._raw = true; RawHTML.prototype._raw = true;
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
this.className = className;
this.declarations = declarations || "";
this.mediaRules = mediaRules || [];
this.pseudoRules = pseudoRules || [];
this.keyframes = keyframes || [];
}
StyleValue.prototype._styleValue = true;
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; } function isKw(x) { return x != null && x._kw === true; }
@@ -93,6 +102,7 @@
if (x._component) return "component"; if (x._component) return "component";
if (x._macro) return "macro"; if (x._macro) return "macro";
if (x._raw) return "raw-html"; if (x._raw) return "raw-html";
if (x._styleValue) return "style-value";
if (Array.isArray(x)) return "list"; if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict"; if (typeof x === "object") return "dict";
return "unknown"; return "unknown";
@@ -138,6 +148,27 @@
function isComponent(x) { return x != null && x._component === true; } function isComponent(x) { return x != null && x._component === true; }
function isMacro(x) { return x != null && x._macro === true; } function isMacro(x) { return x != null && x._macro === true; }
function isStyleValue(x) { return x != null && x._styleValue === true; }
function styleValueClass(x) { return x.className; }
function styleValue_p(x) { return x != null && x._styleValue === true; }
function buildKeyframes(kfName, steps, env) {
// Platform implementation of defkeyframes
var parts = [];
for (var i = 0; i < steps.length; i++) {
var step = steps[i];
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
var body = trampoline(evalExpr(step[1], env));
var decls = isStyleValue(body) ? body.declarations : String(body);
parts.push(selector + "{" + decls + "}");
}
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
var cn = "sx-ref-kf-" + kfName;
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
env[kfName] = sv;
return sv;
}
function envHas(env, name) { return name in env; } function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; } function envGet(env, name) { return env[name]; }
function envSet(env, name, val) { env[name] = val; } function envSet(env, name, val) { env[name] = val; }
@@ -288,6 +319,45 @@
PRIMITIVES["escape"] = function(s) { PRIMITIVES["escape"] = function(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}; };
PRIMITIVES["format-date"] = function(s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
} 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; });
};
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];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
function isPrimitive(name) { return name in PRIMITIVES; } function isPrimitive(name) { return name in PRIMITIVES; }
function getPrimitive(name) { return PRIMITIVES[name]; } function getPrimitive(name) { return PRIMITIVES[name]; }
@@ -306,6 +376,10 @@
return NIL; return NIL;
} }
function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; } function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; }
function isEvery(fn, coll) {
for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; }
return true;
}
function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; }
// List primitives used directly by transpiled code // List primitives used directly by transpiled code
@@ -352,7 +426,8 @@
function isSpecialForm(n) { return n in { function isSpecialForm(n) { return n in {
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"begin":1,"do":1, "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
"quote":1,"quasiquote":1,"->":1,"set!":1 "quote":1,"quasiquote":1,"->":1,"set!":1
}; } }; }
function isHoForm(n) { return n in { function isHoForm(n) { return n in {
@@ -371,7 +446,7 @@
var evalExpr = 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 evalExpr = 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); 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)))))))); 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 == "dict") return mapDict(function(k, v) { return [k, trampoline(evalExpr(v, env))]; }, expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : evalList(expr, env)); return expr; })(); }; })(); if (_m == "keyword") return keywordName(expr); if (_m == "dict") return mapDict(function(k, v) { return trampoline(evalExpr(v, env)); }, expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : evalList(expr, env)); return expr; })(); };
// eval-list // eval-list
var evalList = function(expr, env) { return (function() { var evalList = function(expr, env) { return (function() {
@@ -379,10 +454,10 @@
var args = rest(expr); 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() { 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); 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 == "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 == "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")) ? sfDefine(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() {
var mac = envGet(env, name); var mac = envGet(env, name);
return makeThunk(expandMacro(mac, args, env), env); return makeThunk(expandMacro(mac, args, env), env);
})() : evalCall(head, args, env)))))))))))))))))))))))))))); })() : evalCall(head, args, env)))))))))))))))))))))))))))))));
})() : evalCall(head, args, env))); })() : evalCall(head, args, env)));
})(); }; })(); };
@@ -574,6 +649,21 @@
return [params, restParam]; return [params, restParam];
})(); }; })(); };
// sf-defstyle
var sfDefstyle = function(args, env) { return (function() {
var nameSym = first(args);
var value = trampoline(evalExpr(nth(args, 1), env));
env[symbolName(nameSym)] = value;
return value;
})(); };
// sf-defkeyframes
var sfDefkeyframes = function(args, env) { return (function() {
var kfName = symbolName(first(args));
var steps = rest(args);
return buildKeyframes(kfName, steps, env);
})(); };
// sf-begin // sf-begin
var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); }; var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); };
@@ -667,26 +757,30 @@
return isEvery(function(item) { return trampoline(callLambda(f, [item], env)); }, coll); return isEvery(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
})(); }; })(); };
// ho-for-each
var hoForEach = function(args, env) { return (function() {
var f = trampoline(evalExpr(first(args), env));
var coll = trampoline(evalExpr(nth(args, 1), env));
return forEach(function(item) { return trampoline(callLambda(f, [item], env)); }, coll);
})(); };
// === Transpiled from render.sx === // === Transpiled from render.sx ===
// HTML_TAGS // HTML_TAGS
var HTML_TAGS = ["html", "head", "body", "title", "meta", "link", "script", "style", "noscript", "header", "nav", "main", "section", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "div", "p", "blockquote", "pre", "figure", "figcaption", "address", "details", "summary", "a", "span", "em", "strong", "small", "b", "i", "u", "s", "mark", "sub", "sup", "abbr", "cite", "code", "time", "br", "wbr", "hr", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "form", "input", "textarea", "select", "option", "optgroup", "button", "label", "fieldset", "legend", "output", "datalist", "img", "video", "audio", "source", "picture", "canvas", "iframe", "svg", "path", "circle", "rect", "line", "polyline", "polygon", "text", "g", "defs", "use", "clipPath", "mask", "pattern", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feBlend", "feColorMatrix", "feComposite", "feMerge", "feMergeNode", "animate", "animateTransform", "foreignObject", "template", "slot", "dialog", "menu"]; var HTML_TAGS = ["html", "head", "body", "title", "meta", "link", "script", "style", "noscript", "header", "nav", "main", "section", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "div", "p", "blockquote", "pre", "figure", "figcaption", "address", "details", "summary", "a", "span", "em", "strong", "small", "b", "i", "u", "s", "mark", "sub", "sup", "abbr", "cite", "code", "time", "br", "wbr", "hr", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "form", "input", "textarea", "select", "option", "optgroup", "button", "label", "fieldset", "legend", "output", "datalist", "img", "video", "audio", "source", "picture", "canvas", "iframe", "svg", "math", "path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text", "tspan", "g", "defs", "use", "clipPath", "mask", "pattern", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feBlend", "feColorMatrix", "feComposite", "feMerge", "feMergeNode", "feTurbulence", "feComponentTransfer", "feFuncR", "feFuncG", "feFuncB", "feFuncA", "feDisplacementMap", "feFlood", "feImage", "feMorphology", "feSpecularLighting", "feDiffuseLighting", "fePointLight", "feSpotLight", "feDistantLight", "animate", "animateTransform", "foreignObject", "template", "slot", "dialog", "menu"];
// VOID_ELEMENTS // VOID_ELEMENTS
var VOID_ELEMENTS = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]; var VOID_ELEMENTS = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
// BOOLEAN_ATTRS // BOOLEAN_ATTRS
var BOOLEAN_ATTRS = ["disabled", "checked", "selected", "readonly", "required", "hidden", "autofocus", "autoplay", "controls", "loop", "muted", "defer", "async", "novalidate", "formnovalidate", "multiple", "open", "allowfullscreen"]; var BOOLEAN_ATTRS = ["async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "formnovalidate", "hidden", "inert", "ismap", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected"];
// render-to-html // render-to-html
var renderToHtml = function(expr, env) { return (function() { 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)); return renderValueToHtml(trampoline(evalExpr(expr, env)), env); })(); };
var result = trampoline(evalExpr(expr, env));
return renderValueToHtml(result, env);
})(); };
// render-value-to-html // 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); return escapeHtml((String(val))); })(); }; 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-list-to-html // render-list-to-html
var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() { var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() {
@@ -697,7 +791,7 @@
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() { 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 comp = envGet(env, name); var comp = envGet(env, name);
return (isSxTruthy(isComponent(comp)) ? renderToHtml(trampoline(callComponent(comp, args, env)), env) : error((String("Unknown component: ") + String(name)))); return (isSxTruthy(isComponent(comp)) ? renderToHtml(trampoline(callComponent(comp, args, env)), env) : error((String("Unknown component: ") + String(name))));
})() : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(trampoline(evalExpr(expandMacro(envGet(env, name), args, env), env)), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))); })() : (isSxTruthy(sxOr((name == "define"), (name == "defcomp"), (name == "defmacro"), (name == "defstyle"), (name == "defkeyframes"), (name == "defhandler"))) ? (trampoline(evalExpr(expr, env)), "") : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(trampoline(evalExpr(expandMacro(envGet(env, name), args, env), env)), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))));
})()); })());
})()); }; })()); };
@@ -728,7 +822,7 @@
// render-attrs // render-attrs
var renderAttrs = function(attrs) { return join("", map(function(key) { return (function() { var renderAttrs = function(attrs) { return join("", map(function(key) { return (function() {
var val = dictGet(attrs, key); var val = dictGet(attrs, key);
return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\""))))); return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (isSxTruthy((isSxTruthy((key == "style")) && isStyleValue(val))) ? (String(" class=\"") + String(styleValueClass(val)) + String("\"")) : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\""))))));
})(); }, keys(attrs))); }; })(); }, keys(attrs))); };
// render-to-sx // render-to-sx
@@ -798,6 +892,11 @@
return _rawCallLambda(f, args, callerEnv); return _rawCallLambda(f, args, callerEnv);
}; };
// 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;
// ========================================================================= // =========================================================================
// Parser (reused from reference — hand-written for bootstrap simplicity) // Parser (reused from reference — hand-written for bootstrap simplicity)
// ========================================================================= // =========================================================================
@@ -824,8 +923,15 @@
var ch = text[pos]; var ch = text[pos];
if (ch === "(") { pos++; return readList(")"); } if (ch === "(") { pos++; return readList(")"); }
if (ch === "[") { pos++; return readList("]"); } if (ch === "[") { pos++; return readList("]"); }
if (ch === "{") { pos++; return readMap(); }
if (ch === '"') return readString(); if (ch === '"') return readString();
if (ch === ":") return readKeyword(); if (ch === ":") return readKeyword();
if (ch === "`") { pos++; return [new Symbol("quasiquote"), readExpr()]; }
if (ch === ",") {
pos++;
if (pos < text.length && text[pos] === "@") { pos++; return [new Symbol("splice-unquote"), readExpr()]; }
return [new Symbol("unquote"), readExpr()];
}
if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber(); if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber();
if (ch >= "0" && ch <= "9") return readNumber(); if (ch >= "0" && ch <= "9") return readNumber();
return readSymbol(); return readSymbol();
@@ -839,6 +945,17 @@
items.push(readExpr()); items.push(readExpr());
} }
} }
function readMap() {
var result = {};
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated map");
if (text[pos] === "}") { pos++; return result; }
var key = readExpr();
var keyStr = (key && key._kw) ? key.name : String(key);
result[keyStr] = readExpr();
}
}
function readString() { function readString() {
pos++; // skip " pos++; // skip "
var s = ""; var s = "";

View File

@@ -178,6 +178,13 @@ class JSEmitter:
"ho-reduce": "hoReduce", "ho-reduce": "hoReduce",
"ho-some": "hoSome", "ho-some": "hoSome",
"ho-every": "hoEvery", "ho-every": "hoEvery",
"ho-for-each": "hoForEach",
"sf-defstyle": "sfDefstyle",
"sf-defkeyframes": "sfDefkeyframes",
"build-keyframes": "buildKeyframes",
"style-value?": "isStyleValue",
"style-value-class": "styleValueClass",
"kf-name": "kfName",
"special-form?": "isSpecialForm", "special-form?": "isSpecialForm",
"ho-form?": "isHoForm", "ho-form?": "isHoForm",
"strip-prefix": "stripPrefix", "strip-prefix": "stripPrefix",
@@ -595,6 +602,15 @@ PREAMBLE = '''\
function RawHTML(html) { this.html = html; } function RawHTML(html) { this.html = html; }
RawHTML.prototype._raw = true; RawHTML.prototype._raw = true;
function StyleValue(className, declarations, mediaRules, pseudoRules, keyframes) {
this.className = className;
this.declarations = declarations || "";
this.mediaRules = mediaRules || [];
this.pseudoRules = pseudoRules || [];
this.keyframes = keyframes || [];
}
StyleValue.prototype._styleValue = true;
function isSym(x) { return x != null && x._sym === true; } function isSym(x) { return x != null && x._sym === true; }
function isKw(x) { return x != null && x._kw === true; } function isKw(x) { return x != null && x._kw === true; }
@@ -631,6 +647,7 @@ PLATFORM_JS = '''
if (x._component) return "component"; if (x._component) return "component";
if (x._macro) return "macro"; if (x._macro) return "macro";
if (x._raw) return "raw-html"; if (x._raw) return "raw-html";
if (x._styleValue) return "style-value";
if (Array.isArray(x)) return "list"; if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict"; if (typeof x === "object") return "dict";
return "unknown"; return "unknown";
@@ -676,6 +693,27 @@ PLATFORM_JS = '''
function isComponent(x) { return x != null && x._component === true; } function isComponent(x) { return x != null && x._component === true; }
function isMacro(x) { return x != null && x._macro === true; } function isMacro(x) { return x != null && x._macro === true; }
function isStyleValue(x) { return x != null && x._styleValue === true; }
function styleValueClass(x) { return x.className; }
function styleValue_p(x) { return x != null && x._styleValue === true; }
function buildKeyframes(kfName, steps, env) {
// Platform implementation of defkeyframes
var parts = [];
for (var i = 0; i < steps.length; i++) {
var step = steps[i];
var selector = isSym(step[0]) ? step[0].name : String(step[0]);
var body = trampoline(evalExpr(step[1], env));
var decls = isStyleValue(body) ? body.declarations : String(body);
parts.push(selector + "{" + decls + "}");
}
var kfRule = "@keyframes " + kfName + "{" + parts.join("") + "}";
var cn = "sx-ref-kf-" + kfName;
var sv = new StyleValue(cn, "animation-name:" + kfName, [], [], [[kfName, kfRule]]);
env[kfName] = sv;
return sv;
}
function envHas(env, name) { return name in env; } function envHas(env, name) { return name in env; }
function envGet(env, name) { return env[name]; } function envGet(env, name) { return env[name]; }
function envSet(env, name, val) { env[name] = val; } function envSet(env, name, val) { env[name] = val; }
@@ -826,6 +864,45 @@ PLATFORM_JS = '''
PRIMITIVES["escape"] = function(s) { PRIMITIVES["escape"] = function(s) {
return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}; };
PRIMITIVES["format-date"] = function(s, fmt) {
if (!s) return "";
try {
var d = new Date(s);
if (isNaN(d.getTime())) return String(s);
var months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2))
.replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()])
.replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2))
.replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2));
} 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; });
};
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];
if (isNil(a) || a === false) continue;
atoms.push(isKw(a) ? a.name : String(a));
}
if (!atoms.length) return NIL;
return new StyleValue("sx-" + atoms.join("-"), atoms.join(";"), [], [], []);
};
PRIMITIVES["merge-styles"] = function() {
var valid = [];
for (var i = 0; i < arguments.length; i++) {
if (isStyleValue(arguments[i])) valid.push(arguments[i]);
}
if (!valid.length) return NIL;
if (valid.length === 1) return valid[0];
var allDecls = valid.map(function(v) { return v.declarations; }).join(";");
return new StyleValue("sx-merged", allDecls, [], [], []);
};
function isPrimitive(name) { return name in PRIMITIVES; } function isPrimitive(name) { return name in PRIMITIVES; }
function getPrimitive(name) { return PRIMITIVES[name]; } function getPrimitive(name) { return PRIMITIVES[name]; }
@@ -844,6 +921,10 @@ PLATFORM_JS = '''
return NIL; return NIL;
} }
function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; } function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; }
function isEvery(fn, coll) {
for (var i = 0; i < coll.length; i++) { if (!isSxTruthy(fn(coll[i]))) return false; }
return true;
}
function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; }
// List primitives used directly by transpiled code // List primitives used directly by transpiled code
@@ -890,7 +971,8 @@ PLATFORM_JS = '''
function isSpecialForm(n) { return n in { function isSpecialForm(n) { return n in {
"if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1,
"lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"begin":1,"do":1, "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"defstyle":1,
"defkeyframes":1,"defhandler":1,"begin":1,"do":1,
"quote":1,"quasiquote":1,"->":1,"set!":1 "quote":1,"quasiquote":1,"->":1,"set!":1
}; } }; }
function isHoForm(n) { return n in { function isHoForm(n) { return n in {
@@ -907,7 +989,12 @@ FIXUPS = '''
callLambda = function(f, args, callerEnv) { callLambda = function(f, args, callerEnv) {
if (typeof f === "function") return f.apply(null, args); if (typeof f === "function") return f.apply(null, args);
return _rawCallLambda(f, args, callerEnv); return _rawCallLambda(f, args, callerEnv);
};''' };
// 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;'''
PUBLIC_API = ''' PUBLIC_API = '''
// ========================================================================= // =========================================================================
@@ -936,8 +1023,15 @@ PUBLIC_API = '''
var ch = text[pos]; var ch = text[pos];
if (ch === "(") { pos++; return readList(")"); } if (ch === "(") { pos++; return readList(")"); }
if (ch === "[") { pos++; return readList("]"); } if (ch === "[") { pos++; return readList("]"); }
if (ch === "{") { pos++; return readMap(); }
if (ch === \'"\') return readString(); if (ch === \'"\') return readString();
if (ch === ":") return readKeyword(); if (ch === ":") return readKeyword();
if (ch === "`") { pos++; return [new Symbol("quasiquote"), readExpr()]; }
if (ch === ",") {
pos++;
if (pos < text.length && text[pos] === "@") { pos++; return [new Symbol("splice-unquote"), readExpr()]; }
return [new Symbol("unquote"), readExpr()];
}
if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber(); if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber();
if (ch >= "0" && ch <= "9") return readNumber(); if (ch >= "0" && ch <= "9") return readNumber();
return readSymbol(); return readSymbol();
@@ -951,6 +1045,17 @@ PUBLIC_API = '''
items.push(readExpr()); items.push(readExpr());
} }
} }
function readMap() {
var result = {};
while (true) {
skipWs();
if (pos >= text.length) throw new Error("Unterminated map");
if (text[pos] === "}") { pos++; return result; }
var key = readExpr();
var keyStr = (key && key._kw) ? key.name : String(key);
result[keyStr] = readExpr();
}
}
function readString() { function readString() {
pos++; // skip " pos++; // skip "
var s = ""; var s = "";

View File

@@ -96,7 +96,7 @@
;; --- dict literal --- ;; --- dict literal ---
"dict" "dict"
(map-dict (fn (k v) (list k (trampoline (eval-expr v env)))) expr) (map-dict (fn (k v) (trampoline (eval-expr v env))) expr)
;; --- list = call or special form --- ;; --- list = call or special form ---
"list" "list"
@@ -141,6 +141,9 @@
(= name "define") (sf-define args env) (= name "define") (sf-define args env)
(= name "defcomp") (sf-defcomp args env) (= name "defcomp") (sf-defcomp args env)
(= name "defmacro") (sf-defmacro args env) (= name "defmacro") (sf-defmacro args env)
(= name "defstyle") (sf-defstyle args env)
(= name "defkeyframes") (sf-defkeyframes args env)
(= name "defhandler") (sf-define args env)
(= name "begin") (sf-begin args env) (= name "begin") (sf-begin args env)
(= name "do") (sf-begin args env) (= name "do") (sf-begin args env)
(= name "quote") (sf-quote args env) (= name "quote") (sf-quote args env)
@@ -495,6 +498,25 @@
(list params rest-param)))) (list params rest-param))))
(define sf-defstyle
(fn (args env)
;; (defstyle name expr) — bind name to evaluated expr (typically a StyleValue)
(let ((name-sym (first args))
(value (trampoline (eval-expr (nth args 1) env))))
(env-set! env (symbol-name name-sym) value)
value)))
(define sf-defkeyframes
(fn (args env)
;; (defkeyframes name (selector body) ...) — build @keyframes rule,
;; register in keyframes dict, return StyleValue.
;; Delegates to platform: build-keyframes returns a StyleValue.
(let ((kf-name (symbol-name (first args)))
(steps (rest args)))
(build-keyframes kf-name steps env))))
(define sf-begin (define sf-begin
(fn (args env) (fn (args env)
(if (empty? args) (if (empty? args)
@@ -651,6 +673,15 @@
coll)))) coll))))
(define ho-for-each
(fn (args env)
(let ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env))))
(for-each
(fn (item) (trampoline (call-lambda f (list item) env)))
coll))))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; 8. Primitives — pure functions available in all targets ;; 8. Primitives — pure functions available in all targets
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -728,4 +759,7 @@
;; (strip-prefix s prefix) → string with prefix removed (or s unchanged) ;; (strip-prefix s prefix) → string with prefix removed (or s unchanged)
;; (apply f args) → call f with args list ;; (apply f args) → call f with args list
;; (zip lists...) → list of tuples ;; (zip lists...) → list of tuples
;;
;; CSSX (style system):
;; (build-keyframes name steps env) → StyleValue (platform builds @keyframes)
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -18,11 +18,13 @@
;; ident → [a-zA-Z_~*+\-><=/!?&] [a-zA-Z0-9_~*+\-><=/!?.:&]* ;; ident → [a-zA-Z_~*+\-><=/!?&] [a-zA-Z0-9_~*+\-><=/!?.:&]*
;; comment → ';' to end of line (discarded) ;; comment → ';' to end of line (discarded)
;; ;;
;; Quote sugar (optional — not used in current SX): ;; Dict literal:
;; '(expr) → (quote expr) ;; {key val ...} → dict object (keys are keywords or expressions)
;;
;; Quote sugar:
;; `(expr) → (quasiquote expr) ;; `(expr) → (quasiquote expr)
;; ~(expr) → (unquote expr) ;; ,(expr) → (unquote expr)
;; ~@(expr) → (splice-unquote expr) ;; ,@(expr) → (splice-unquote expr)
;; ========================================================================== ;; ==========================================================================
@@ -81,6 +83,37 @@
(advance-pos!) (advance-pos!)
(scan-next)) (scan-next))
;; Open brace (dict literal)
(= ch "{")
(do (append! tokens (list "lbrace" "{" line col))
(advance-pos!)
(scan-next))
;; Close brace
(= ch "}")
(do (append! tokens (list "rbrace" "}" line col))
(advance-pos!)
(scan-next))
;; Quasiquote sugar
(= ch "`")
(do (advance-pos!)
(let ((inner (scan-next-expr)))
(append! tokens (list "quasiquote" inner line col))
(scan-next)))
;; Unquote / splice-unquote
(= ch ",")
(do (advance-pos!)
(if (and (< pos len-src) (= (nth source pos) "@"))
(do (advance-pos!)
(let ((inner (scan-next-expr)))
(append! tokens (list "splice-unquote" inner line col))
(scan-next)))
(let ((inner (scan-next-expr)))
(append! tokens (list "unquote" inner line col))
(scan-next))))
;; Keyword ;; Keyword
(= ch ":") (= ch ":")
(do (append! tokens (scan-keyword)) (scan-next)) (do (append! tokens (scan-keyword)) (scan-next))
@@ -229,6 +262,10 @@
(do (set! pos (inc pos)) (do (set! pos (inc pos))
(parse-list tokens "rbracket")) (parse-list tokens "rbracket"))
"lbrace"
(do (set! pos (inc pos))
(parse-dict tokens))
"string" (do (set! pos (inc pos)) (nth tok 1)) "string" (do (set! pos (inc pos)) (nth tok 1))
"number" (do (set! pos (inc pos)) (nth tok 1)) "number" (do (set! pos (inc pos)) (nth tok 1))
"boolean" (do (set! pos (inc pos)) (nth tok 1)) "boolean" (do (set! pos (inc pos)) (nth tok 1))
@@ -261,6 +298,28 @@
items))) items)))
(define parse-dict
(fn (tokens)
;; Parse {key val key val ...} until "rbrace" token.
;; Returns a dict (plain object).
(let ((result (dict)))
(define parse-dict-loop
(fn ()
(if (>= pos (len tokens))
(error "Unterminated dict")
(if (= (first (nth tokens pos)) "rbrace")
(do (set! pos (inc pos)) nil) ;; done
(let ((key-expr (parse-expr tokens))
(key-str (if (= (type-of key-expr) "keyword")
(keyword-name key-expr)
(str key-expr)))
(val-expr (parse-expr tokens)))
(dict-set! result key-str val-expr)
(parse-dict-loop))))))
(parse-dict-loop)
result)))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
;; Serializer — AST → SX source text ;; Serializer — AST → SX source text
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------

View File

@@ -426,3 +426,34 @@
:params (s) :params (s)
:returns "string" :returns "string"
:doc "Remove HTML tags from string.") :doc "Remove HTML tags from string.")
;; --------------------------------------------------------------------------
;; Date & parsing helpers
;; --------------------------------------------------------------------------
(define-primitive "parse-datetime"
:params (s)
:returns "string"
:doc "Parse datetime string — identity passthrough (returns string or nil).")
(define-primitive "split-ids"
:params (s)
:returns "list"
:doc "Split comma-separated ID string into list of trimmed non-empty strings.")
;; --------------------------------------------------------------------------
;; CSSX — style system primitives
;; --------------------------------------------------------------------------
(define-primitive "css"
:params (&rest atoms)
:returns "style-value"
:doc "Resolve style atoms to a StyleValue with className and CSS declarations.
Atoms are keywords or strings: (css :flex :gap-4 :hover:bg-sky-200).")
(define-primitive "merge-styles"
:params (&rest styles)
:returns "style-value"
:doc "Merge multiple StyleValues into one combined StyleValue.")

View File

@@ -44,10 +44,15 @@
;; Media ;; Media
"img" "video" "audio" "source" "picture" "canvas" "iframe" "img" "video" "audio" "source" "picture" "canvas" "iframe"
;; SVG ;; SVG
"svg" "path" "circle" "rect" "line" "polyline" "polygon" "text" "svg" "math" "path" "circle" "ellipse" "rect" "line" "polyline" "polygon"
"g" "defs" "use" "clipPath" "mask" "pattern" "linearGradient" "text" "tspan" "g" "defs" "use" "clipPath" "mask" "pattern"
"radialGradient" "stop" "filter" "feGaussianBlur" "feOffset" "linearGradient" "radialGradient" "stop" "filter"
"feBlend" "feColorMatrix" "feComposite" "feMerge" "feMergeNode" "feGaussianBlur" "feOffset" "feBlend" "feColorMatrix" "feComposite"
"feMerge" "feMergeNode" "feTurbulence"
"feComponentTransfer" "feFuncR" "feFuncG" "feFuncB" "feFuncA"
"feDisplacementMap" "feFlood" "feImage" "feMorphology"
"feSpecularLighting" "feDiffuseLighting"
"fePointLight" "feSpotLight" "feDistantLight"
"animate" "animateTransform" "foreignObject" "animate" "animateTransform" "foreignObject"
;; Other ;; Other
"template" "slot" "dialog" "menu")) "template" "slot" "dialog" "menu"))
@@ -57,9 +62,10 @@
"link" "meta" "param" "source" "track" "wbr")) "link" "meta" "param" "source" "track" "wbr"))
(define BOOLEAN_ATTRS (define BOOLEAN_ATTRS
(list "disabled" "checked" "selected" "readonly" "required" "hidden" (list "async" "autofocus" "autoplay" "checked" "controls" "default"
"autofocus" "autoplay" "controls" "loop" "muted" "defer" "async" "defer" "disabled" "formnovalidate" "hidden" "inert" "ismap"
"novalidate" "formnovalidate" "multiple" "open" "allowfullscreen")) "loop" "multiple" "muted" "nomodule" "novalidate" "open"
"playsinline" "readonly" "required" "reversed" "selected"))
;; -------------------------------------------------------------------------- ;; --------------------------------------------------------------------------
@@ -68,8 +74,20 @@
(define render-to-html (define render-to-html
(fn (expr env) (fn (expr env)
(let ((result (trampoline (eval-expr expr env)))) (case (type-of expr)
(render-value-to-html result env)))) ;; Literals — render directly
"nil" ""
"string" (escape-html expr)
"number" (str expr)
"boolean" (if expr "true" "false")
;; List — dispatch to render-list which handles HTML tags, special forms, etc.
"list" (if (empty? expr) "" (render-list-to-html expr env))
;; Symbol — evaluate then render
"symbol" (render-value-to-html (trampoline (eval-expr expr env)) env)
;; Keyword — render as text
"keyword" (escape-html (keyword-name expr))
;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
(define render-value-to-html (define render-value-to-html
(fn (val env) (fn (val env)
@@ -80,6 +98,7 @@
"boolean" (if val "true" "false") "boolean" (if val "true" "false")
"list" (render-list-to-html val env) "list" (render-list-to-html val env)
"raw-html" (raw-html-content val) "raw-html" (raw-html-content val)
"style-value" (style-value-class val)
:else (escape-html (str val))))) :else (escape-html (str val)))))
(define render-list-to-html (define render-list-to-html
@@ -114,6 +133,11 @@
env) env)
(error (str "Unknown component: " name)))) (error (str "Unknown component: " name))))
;; Definitions — evaluate for side effects, render nothing
(or (= name "define") (= name "defcomp") (= name "defmacro")
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))
(do (trampoline (eval-expr expr env)) "")
;; Macro expansion ;; Macro expansion
(and (env-has? env name) (macro? (env-get env name))) (and (env-has? env name) (macro? (env-get env name)))
(render-to-html (render-to-html
@@ -182,6 +206,9 @@
"" ""
;; Nil values — skip ;; Nil values — skip
(nil? val) "" (nil? val) ""
;; StyleValue on :style → emit as class
(and (= key "style") (style-value? val))
(str " class=\"" (style-value-class val) "\"")
;; Normal attr ;; Normal attr
:else (str " " key "=\"" (escape-attr (str val)) "\"")))) :else (str " " key "=\"" (escape-attr (str val)) "\""))))
(keys attrs))))) (keys attrs)))))
@@ -323,6 +350,10 @@
;; (set-attribute el k v) → void ;; (set-attribute el k v) → void
;; (append-child parent c) → void ;; (append-child parent c) → void
;; ;;
;; StyleValue:
;; (style-value? x) → boolean (is x a StyleValue?)
;; (style-value-class sv) → string (CSS class name)
;;
;; Serialization: ;; Serialization:
;; (serialize val) → SX source string representation of val ;; (serialize val) → SX source string representation of val
;; ;;