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:
@@ -178,6 +178,13 @@ class JSEmitter:
|
||||
"ho-reduce": "hoReduce",
|
||||
"ho-some": "hoSome",
|
||||
"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",
|
||||
"ho-form?": "isHoForm",
|
||||
"strip-prefix": "stripPrefix",
|
||||
@@ -595,6 +602,15 @@ PREAMBLE = '''\
|
||||
function RawHTML(html) { this.html = html; }
|
||||
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 isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -631,6 +647,7 @@ PLATFORM_JS = '''
|
||||
if (x._component) return "component";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
return "unknown";
|
||||
@@ -676,6 +693,27 @@ PLATFORM_JS = '''
|
||||
function isComponent(x) { return x != null && x._component === 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 envGet(env, name) { return env[name]; }
|
||||
function envSet(env, name, val) { env[name] = val; }
|
||||
@@ -826,6 +864,45 @@ PLATFORM_JS = '''
|
||||
PRIMITIVES["escape"] = function(s) {
|
||||
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 getPrimitive(name) { return PRIMITIVES[name]; }
|
||||
@@ -844,6 +921,10 @@ PLATFORM_JS = '''
|
||||
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; }
|
||||
|
||||
// List primitives used directly by transpiled code
|
||||
@@ -890,7 +971,8 @@ PLATFORM_JS = '''
|
||||
|
||||
function isSpecialForm(n) { return n in {
|
||||
"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
|
||||
}; }
|
||||
function isHoForm(n) { return n in {
|
||||
@@ -907,7 +989,12 @@ FIXUPS = '''
|
||||
callLambda = function(f, args, callerEnv) {
|
||||
if (typeof f === "function") return f.apply(null, args);
|
||||
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 = '''
|
||||
// =========================================================================
|
||||
@@ -936,8 +1023,15 @@ PUBLIC_API = '''
|
||||
var ch = text[pos];
|
||||
if (ch === "(") { pos++; return readList(")"); }
|
||||
if (ch === "[") { pos++; return readList("]"); }
|
||||
if (ch === "{") { pos++; return readMap(); }
|
||||
if (ch === \'"\') return readString();
|
||||
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 >= "0" && ch <= "9") return readNumber();
|
||||
return readSymbol();
|
||||
@@ -951,6 +1045,17 @@ PUBLIC_API = '''
|
||||
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() {
|
||||
pos++; // skip "
|
||||
var s = "";
|
||||
|
||||
Reference in New Issue
Block a user