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:
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||||
};
|
};
|
||||||
|
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 = "";
|
||||||
|
|||||||
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
||||||
};
|
};
|
||||||
|
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 = "";
|
||||||
|
|||||||
@@ -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)
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
||||||
;;
|
;;
|
||||||
|
|||||||
Reference in New Issue
Block a user