Merge branch 'worktree-api-urls' into macros

This commit is contained in:
2026-03-13 12:20:27 +00:00
6 changed files with 88 additions and 33 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-13T11:02:33Z";
var SX_VERSION = "2026-03-13T12:16:43Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1510,7 +1510,7 @@ continue; } else { return NIL; } } };
})(); };
// sx-serialize
var sxSerialize = function(val) { return (function() { var _m = typeOf(val); if (_m == "nil") return "nil"; if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "number") return (String(val)); if (_m == "string") return (String("\"") + String(escapeString(val)) + String("\"")); if (_m == "symbol") return symbolName(val); if (_m == "keyword") return (String(":") + String(keywordName(val))); if (_m == "list") return (String("(") + String(join(" ", map(sxSerialize, val))) + String(")")); if (_m == "dict") return sxSerializeDict(val); if (_m == "sx-expr") return sxExprSource(val); return (String(val)); })(); };
var sxSerialize = function(val) { return (function() { var _m = typeOf(val); if (_m == "nil") return "nil"; if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "number") return (String(val)); if (_m == "string") return (String("\"") + String(escapeString(val)) + String("\"")); if (_m == "symbol") return symbolName(val); if (_m == "keyword") return (String(":") + String(keywordName(val))); if (_m == "list") return (String("(") + String(join(" ", map(sxSerialize, val))) + String(")")); if (_m == "dict") return sxSerializeDict(val); if (_m == "sx-expr") return sxExprSource(val); if (_m == "spread") return (String("(make-spread ") + String(sxSerializeDict(spreadAttrs(val))) + String(")")); return (String(val)); })(); };
// sx-serialize-dict
var sxSerializeDict = function(d) { return (String("{") + String(join(" ", reduce(function(acc, key) { return concat(acc, [(String(":") + String(key)), sxSerialize(dictGet(d, key))]); }, [], keys(d)))) + String("}")); };
@@ -1786,7 +1786,11 @@ return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if
})() : (function() {
var val = aser(arg, env);
if (isSxTruthy(!isSxTruthy(isNil(val)))) {
(isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val)));
(isSxTruthy(isSpread(val)) ? forEach(function(k) { return (function() {
var v = dictGet(spreadAttrs(val), k);
parts.push((String(":") + String(k)));
return append_b(parts, serialize(v));
})(); }, keys(spreadAttrs(val))) : (isSxTruthy((typeOf(val) == "list")) ? forEach(function(item) { return (isSxTruthy(!isSxTruthy(isNil(item))) ? append_b(parts, serialize(item)) : NIL); }, val) : append_b(parts, serialize(val))));
}
return (i = (i + 1));
})())); } }

View File

@@ -775,6 +775,7 @@
(define-async async-aser-fragment :effects [render io]
(fn ((children :as list) (env :as dict) ctx)
;; Spreads are filtered — fragments have no parent element to merge into
(let ((parts (list)))
(for-each
(fn (c)
@@ -782,10 +783,10 @@
(if (= (type-of result) "list")
(for-each
(fn (item)
(when (not (nil? item))
(when (and (not (nil? item)) (not (spread? item)))
(append! parts (serialize item))))
result)
(when (not (nil? result))
(when (and (not (nil? result)) (not (spread? result)))
(append! parts (serialize result))))))
children)
(if (empty? parts)
@@ -885,13 +886,21 @@
(set! i (inc i)))
(let ((result (async-aser arg env ctx)))
(when (not (nil? result))
(if (= (type-of result) "list")
(if (spread? result)
;; Spread child — merge attrs as keyword args into parent element
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
result)
(append! parts (serialize result))))
(fn (k)
(let ((v (dict-get (spread-attrs result) k)))
(append! parts (str ":" k))
(append! parts (serialize v))))
(keys (spread-attrs result)))
(if (= (type-of result) "list")
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
result)
(append! parts (serialize result)))))
(set! i (inc i))))))
args)
(when token (svg-context-reset! token))

View File

@@ -110,6 +110,7 @@
(fn ((children :as list) (env :as dict))
;; Serialize (<> child1 child2 ...) to sx source string
;; Must flatten list results (e.g. from map/filter) to avoid nested parens
;; Spreads are filtered — fragments have no parent element to merge into
(let ((parts (list)))
(for-each
(fn (c)
@@ -117,10 +118,10 @@
(if (= (type-of result) "list")
(for-each
(fn (item)
(when (not (nil? item))
(when (and (not (nil? item)) (not (spread? item)))
(append! parts (serialize item))))
result)
(when (not (nil? result))
(when (and (not (nil? result)) (not (spread? result)))
(append! parts (serialize result))))))
children)
(if (empty? parts)
@@ -151,13 +152,21 @@
(set! i (inc i)))
(let ((val (aser arg env)))
(when (not (nil? val))
(if (= (type-of val) "list")
(if (spread? val)
;; Spread child — merge attrs as keyword args into parent element
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
val)
(append! parts (serialize val))))
(fn (k)
(let ((v (dict-get (spread-attrs val) k)))
(append! parts (str ":" k))
(append! parts (serialize v))))
(keys (spread-attrs val)))
(if (= (type-of val) "list")
(for-each
(fn (item)
(when (not (nil? item))
(append! parts (serialize item))))
val)
(append! parts (serialize val)))))
(set! i (inc i))))))
args)
(str "(" (join " " parts) ")"))))

View File

@@ -366,6 +366,7 @@
"list" (str "(" (join " " (map sx-serialize val)) ")")
"dict" (sx-serialize-dict val)
"sx-expr" (sx-expr-source val)
"spread" (str "(make-spread " (sx-serialize-dict (spread-attrs val)) ")")
:else (str val))))

View File

@@ -1,3 +1,5 @@
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift
# WARNING: eval.sx dispatches forms not in special-forms.sx: form?, provide
"""
sx_ref.py -- Generated from reference SX evaluator specification.
@@ -2559,10 +2561,10 @@ def aser_fragment(children, env):
result = aser(c, env)
if sx_truthy((type_of(result) == 'list')):
for item in result:
if sx_truthy((not sx_truthy(is_nil(item)))):
if sx_truthy(((not sx_truthy(is_nil(item))) if not sx_truthy((not sx_truthy(is_nil(item)))) else (not sx_truthy(is_spread(item))))):
parts.append(serialize(item))
else:
if sx_truthy((not sx_truthy(is_nil(result)))):
if sx_truthy(((not sx_truthy(is_nil(result))) if not sx_truthy((not sx_truthy(is_nil(result)))) else (not sx_truthy(is_spread(result))))):
parts.append(serialize(result))
if sx_truthy(empty_p(parts)):
return ''
@@ -2590,12 +2592,18 @@ def aser_call(name, args, env):
else:
val = aser(arg, env)
if sx_truthy((not sx_truthy(is_nil(val)))):
if sx_truthy((type_of(val) == 'list')):
for item in val:
if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item))
if sx_truthy(is_spread(val)):
for k in keys(spread_attrs(val)):
v = dict_get(spread_attrs(val), k)
parts.append(sx_str(':', k))
parts.append(serialize(v))
else:
parts.append(serialize(val))
if sx_truthy((type_of(val) == 'list')):
for item in val:
if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item))
else:
parts.append(serialize(val))
_cells['i'] = (_cells['i'] + 1)
return sx_str('(', join(' ', parts), ')')
@@ -4140,10 +4148,10 @@ async def async_aser_fragment(children, env, ctx):
result = (await async_aser(c, env, ctx))
if sx_truthy((type_of(result) == 'list')):
for item in result:
if sx_truthy((not sx_truthy(is_nil(item)))):
if sx_truthy(((not sx_truthy(is_nil(item))) if not sx_truthy((not sx_truthy(is_nil(item)))) else (not sx_truthy(is_spread(item))))):
parts.append(serialize(item))
else:
if sx_truthy((not sx_truthy(is_nil(result)))):
if sx_truthy(((not sx_truthy(is_nil(result))) if not sx_truthy((not sx_truthy(is_nil(result)))) else (not sx_truthy(is_spread(result))))):
parts.append(serialize(result))
if sx_truthy(empty_p(parts)):
return make_sx_expr('')
@@ -4225,12 +4233,18 @@ async def async_aser_call(name, args, env, ctx):
else:
result = (await async_aser(arg, env, ctx))
if sx_truthy((not sx_truthy(is_nil(result)))):
if sx_truthy((type_of(result) == 'list')):
for item in result:
if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item))
if sx_truthy(is_spread(result)):
for k in keys(spread_attrs(result)):
v = dict_get(spread_attrs(result), k)
parts.append(sx_str(':', k))
parts.append(serialize(v))
else:
parts.append(serialize(result))
if sx_truthy((type_of(result) == 'list')):
for item in result:
if sx_truthy((not sx_truthy(is_nil(item)))):
parts.append(serialize(item))
else:
parts.append(serialize(result))
_cells['i'] = (_cells['i'] + 1)
if sx_truthy(token):
svg_context_reset(token)

View File

@@ -270,3 +270,21 @@
(deftest "or returns last falsy"
(assert-equal "false"
(render-sx "(or false false)"))))
;; --------------------------------------------------------------------------
;; Spread serialization
;; --------------------------------------------------------------------------
(defsuite "aser-spreads"
(deftest "spread in element merges attrs"
(assert-equal "(div :class \"card\" \"hello\")"
(render-sx "(div (make-spread {:class \"card\"}) \"hello\")")))
(deftest "multiple spreads merge into element"
(assert-equal "(div :class \"card\" :style \"color:red\" \"hello\")"
(render-sx "(div (make-spread {:class \"card\"}) (make-spread {:style \"color:red\"}) \"hello\")")))
(deftest "spread in fragment is filtered"
(assert-equal "(<> \"hello\")"
(render-sx "(<> (make-spread {:class \"card\"}) \"hello\")"))))