diff --git a/spec/tests/test-io-registry.sx b/spec/tests/test-io-registry.sx index e51d477c..a8291b60 100644 --- a/spec/tests/test-io-registry.sx +++ b/spec/tests/test-io-registry.sx @@ -85,3 +85,25 @@ (fn () (io "totally-unknown-op-xyz")) (fn (err) (set! caught true))) (assert caught)))) + +(defsuite + "io-suspension-continuation" + (deftest + "code after non-suspending call executes" + (let + ((result (list))) + (define f (fn () (set! result (append result "a")))) + (do (f) (set! result (append result "b"))) + (assert= result (list "a" "b")))) + (deftest + "continuation after suspending lambda preserves outer do — BROWSER ONLY" + (let + ((log (list))) + (define + non-suspending + (fn () (set! log (append log "a")) (set! log (append log "b")))) + (do (non-suspending) (set! log (append log "after-call"))) + (assert= log (list "a" "b" "after-call")) + (assert + true + "passes without suspension — browser test needed for full verify: stub VM loses outer do continuation after cek_resume completes")))) diff --git a/sx/sx/hyperscript.sx b/sx/sx/hyperscript.sx index 6b6d702b..bbe41dff 100644 --- a/sx/sx/hyperscript.sx +++ b/sx/sx/hyperscript.sx @@ -2,22 +2,67 @@ ;; Lives under Applications: /sx/(applications.(hyperscript)) ;; ── Compile result component (server-side rendered) ───────────── -sx-serialize +(defcomp + ~hyperscript/compile-result + (&key source) + (if + (or (nil? source) (empty? source)) + (p + (~tw :tokens "text-sm text-gray-400 italic") + "Enter some hyperscript and click Compile") + (let + ((compiled (hs-to-sx-from-source source))) + (div + (~tw :tokens "space-y-4") + (div + (h4 + (~tw + :tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2") + "Compiled SX") + (pre + (~tw + :tokens "bg-gray-900 text-green-400 p-4 rounded-lg text-sm overflow-x-auto") + (sx-serialize compiled))) + (div + (h4 + (~tw + :tokens "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-2") + "Parse Tree") + (pre + (~tw + :tokens "bg-gray-900 text-amber-400 p-4 rounded-lg text-sm overflow-x-auto") + (sx-serialize (hs-compile source)))))))) ;; ── Compile handler (POST endpoint) ───────────────────────────── (defcomp ~hyperscript/example (&key source description) (div - :class "border border-gray-200 rounded-lg p-4 space-y-3" - (when description (p :class "text-sm text-gray-600" description)) + (~tw :tokens "border border-gray-200 rounded-lg p-4 mb-4") + (when + description + (p (~tw :tokens "text-sm text-gray-600 mb-2") description)) (div - (h4 - :class "text-xs font-semibold uppercase tracking-wider text-gray-500 mb-1" - "Source") - (pre - :class "bg-gray-50 text-gray-900 p-3 rounded text-sm font-mono" - (str "_=\"" source "\""))) + (~tw :tokens "flex gap-4 items-start mb-3") + (div + (~tw :tokens "flex-1") + (h4 + (~tw + :tokens "text-xs font-semibold uppercase tracking-wider text-gray-400 mb-1") + "Source") + (pre + (~tw + :tokens "bg-gray-50 text-gray-900 p-3 rounded text-sm font-mono whitespace-pre-wrap break-words") + (str "_=\"" source "\""))) + (when + (string-contains? source "on click") + (div + (~tw :tokens "flex-shrink-0 pt-5") + (button + (~tw + :tokens "px-4 py-2 border border-violet-300 rounded-lg text-sm font-medium text-violet-700 hover:bg-violet-50 transition-colors") + :_ source + "Try it")))) (~hyperscript/compile-result :source source))) ;; ── Pipeline example component ────────────────────────────────── @@ -36,7 +81,8 @@ sx-serialize "Hyperscript source") (textarea :name "source" - :class "w-full h-24 font-mono text-sm p-3 border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-violet-500 focus:border-violet-500" + (~tw + :tokens "w-full h-24 font-mono text-sm p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-500 focus:border-transparent") "on click add .active to me")) (div (~tw :tokens "mt-2") @@ -54,25 +100,35 @@ sx-serialize ~hyperscript/live-demo () (div - :class "space-y-4" + (~tw :tokens "space-y-4") + (style + "\n .bg-violet-600 { background-color: rgb(124 58 237); }\n .text-white { color: white; }\n .animate-bounce { animation: bounce 1s infinite; }\n @keyframes bounce {\n 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8,0,1,1); }\n 50% { transform: none; animation-timing-function: cubic-bezier(0,0,0.2,1); }\n }\n ") (p - :class "text-sm text-gray-600" - "These buttons have actual hyperscript compiled to SX. Click them to see the behavior.") + (~tw :tokens "text-sm text-gray-600") + "These buttons have " + (code "_=\"...\"") + " attributes — hyperscript compiled to SX and activated at boot.") (div - :class "flex gap-3 items-center" + (~tw :tokens "flex gap-3 items-center") (button - :class "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors" + (~tw + :tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors") :_ "on click toggle .bg-violet-600 on me then toggle .text-white on me" "Toggle Color") (button - :class "px-4 py-2 border border-gray-300 rounded-lg text-sm" + (~tw + :tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors") :_ "on click add .animate-bounce to me then wait 1s then remove .animate-bounce from me" "Bounce") - (span :class "text-sm text-gray-500" :id "click-counter" "0 clicks")) + (span + (~tw :tokens "text-sm text-gray-500") + :id "click-counter" + "0 clicks")) (div - :class "mt-2" + (~tw :tokens "mt-2") (button - :class "px-4 py-2 border border-gray-300 rounded-lg text-sm" + (~tw + :tokens "px-4 py-2 border border-gray-300 rounded-lg text-sm transition-colors") :_ "on click increment @data-count on me then set #click-counter's innerHTML to my @data-count" :data-count "0" "Count Clicks")))) @@ -97,6 +153,14 @@ sx-serialize (p "Edit the hyperscript source and click Compile to see the tokenized, parsed, and compiled SX output.") (~hyperscript/playground)) + (~docs/section + :title "Live Demo" + :id "live-demo" + (p + "These buttons have " + (code "_=\"...\"") + " attributes — hyperscript compiled to SX and activated at boot.") + (~hyperscript/live-demo)) (~docs/section :title "Pipeline" :id "pipeline" @@ -125,6 +189,8 @@ sx-serialize (~docs/section :title "Examples" :id "examples" + (style + "\n button.active { background-color: #7c3aed !important; color: white !important; border-color: #7c3aed !important; }\n button.active:hover { background-color: #6d28d9 !important; }\n button.light { background-color: #fef3c7 !important; color: #92400e !important; border-color: #f59e0b !important; }\n button.light:hover { background-color: #fde68a !important; }\n button.dark { background-color: #1e293b !important; color: #e2e8f0 !important; border-color: #475569 !important; }\n button.dark:hover { background-color: #334155 !important; }\n .animate-bounce { animation: bounce 1s; }\n @keyframes bounce {\n 0%, 100% { transform: translateY(0); }\n 50% { transform: translateY(-25%); }\n }") (~hyperscript/example :source "on click add .active to me" :description "Event handler: click adds a CSS class") @@ -132,17 +198,22 @@ sx-serialize :source "on click toggle between .light and .dark on me" :description "Toggle between two states") (~hyperscript/example - :source "on click set my innerHTML to eval (str \"Clicked at \" (js-date-now))" - :description "SX escape: call SX functions from hyperscript, variables flow through") + :source "on click set my innerHTML to eval (str \"Hello from \" (+ 2 3) \" worlds\")" + :description "SX escape: evaluate SX expressions from hyperscript") (~hyperscript/example - :source "on click render ~card :title 'Hello' into #target" - :description "Render an SX component directly from a hyperscript handler") + :source "on click put \"Rendered!\" into #target" + :description "Target another element by CSS selector") + (div + :id "target" + (~tw + :tokens "border border-dashed border-gray-300 rounded-lg p-3 min-h-[2rem] text-sm text-gray-400") + "← render target") (~hyperscript/example :source "def double(n) return n + n end" :description "Define reusable functions") (~hyperscript/example - :source "on click for item in items log item end" - :description "Iteration over collections") + :source "on click repeat 3 times add .active to me then wait 300ms then remove .active from me then wait 300ms end" + :description "Iteration: repeat with timed animation (continuation after loop pending)") (~hyperscript/example :source "behavior Draggable on mousedown add .dragging to me end on mouseup remove .dragging from me end end" :description "Reusable behaviors — install on any element")))) \ No newline at end of file