Step 18 (part 8): Playground page + compile handler + url_decode fix

sx/sx/hyperscript.sx — _hyperscript playground page at
  /sx/(applications.(hyperscript))
  - compile-result defcomp (SSR compilation display)
  - pipeline documentation (tokenize → parse → compile)
  - example showcases with pre-compiled output
  - sx-post form → handler for interactive compilation

sx/sx/handlers/hyperscript-api.sx — POST handler:
  /sx/(applications.(hyperscript.(api.compile)))
  Accepts source param, returns compiled SX + parse tree HTML
  NOTE: hs-parse returns (do) in server context — JIT/CEK runtime
  issue where parser closures don't evaluate correctly. Works in
  test runner (3127/3127). Investigating separately.

sx_server.ml — url_decode fix: decode + as space in form data
  Standard application/x-www-form-urlencoded uses + for spaces.

Nav: _hyperscript added to Applications section.
Config: handler:hs- prefix added for handler dispatch.

3127/3127 tests, zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 10:10:19 +00:00
parent 770c7fd821
commit cf088a33b4
6 changed files with 211 additions and 1 deletions

View File

@@ -1749,6 +1749,9 @@ let url_decode s =
Buffer.add_char buf (Char.chr (int_of_string ("0x" ^ hex)))
with _ -> Buffer.add_char buf s.[!i]);
i := !i + 3
end else if s.[!i] = '+' then begin
Buffer.add_char buf ' ';
i := !i + 1
end else begin
Buffer.add_char buf s.[!i];
i := !i + 1

View File

@@ -1 +1,6 @@
(define __app-config {:handler-prefixes (list "handler:ex-" "handler:reactive-" "handler:") :shell "~shared:shell/sx-page-shell" :inner-layout "~layouts/doc" :outer-layout "~shared:layout/app-body" :css-files (list "basics.css" "tw.css") :warmup-paths (list "/sx/" "/sx/(geography)" "/sx/(language)" "/sx/(applications)" "/sx/(geography.(reactive.(examples)))" "/sx/(applications.(sxtp))" "/sx/(geography.(cek))" "/sx/(geography.(reactive))" "/sx/(geography.(hypermedia))") :batchable-helpers (list "highlight" "component-source") :title "SX" :init-script :default :home-path "/sx/" :path-prefix "/sx/" :client-libs (list "tw-layout.sx" "tw-type.sx" "tw.sx")})
(dict-set!
__app-config
"handler-prefixes"
(append (get __app-config "handler-prefixes") (list "handler:hs-")))

View File

@@ -0,0 +1,37 @@
;; _hyperscript playground API handler
;; Compiles hyperscript source and returns the AST + SX output as HTML
(defhandler
hs-compile
:path "/sx/(applications.(hyperscript.(api.compile)))"
:method :post
:csrf false
:returns "element"
(&key source)
(if
(or (nil? source) (empty? source))
(p
(~tw :tokens "text-sm text-gray-400 italic")
"Enter some hyperscript and click Compile.")
(let
((ast (hs-compile source)) (sx (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 whitespace-pre-wrap font-mono")
(sx-serialize sx)))
(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 whitespace-pre-wrap font-mono")
(sx-serialize ast)))))))

148
sx/sx/hyperscript.sx Normal file
View File

@@ -0,0 +1,148 @@
;; _hyperscript Playground
;; Lives under Applications: /sx/(applications.(hyperscript))
;; ── Compile result component (server-side rendered) ─────────────
sx-serialize
;; ── 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))
(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 "\"")))
(~hyperscript/compile-result :source source)))
;; ── Pipeline example component ──────────────────────────────────
(defcomp
~hyperscript/playground
()
(div
(~tw :tokens "space-y-4")
(form
:sx-post "/sx/(applications.(hyperscript.(api.compile)))"
:sx-target "#hs-playground-result"
:sx-swap "innerHTML"
(div
(label
(~tw :tokens "block text-sm font-medium text-gray-700 mb-1")
"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"
"on click add .active to me"))
(div
(~tw :tokens "mt-2")
(button
:type "submit"
(~tw
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 text-sm font-medium")
"Compile")))
(div
:id "hs-playground-result"
(~hyperscript/compile-result :source "on click add .active to me"))))
;; ── Playground island ───────────────────────────────────────────
(defcomp
~hyperscript/live-demo
()
(div
:class "space-y-4"
(p
:class "text-sm text-gray-600"
"These buttons have actual hyperscript compiled to SX. Click them to see the behavior.")
(div
:class "flex gap-3 items-center"
(button
:class "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"
:_ "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"))
(div
:class "mt-2"
(button
:class "px-4 py-2 border border-gray-300 rounded-lg text-sm"
:_ "on click increment @data-count on me then set #click-counter's innerHTML to my @data-count"
:data-count "0"
"Count Clicks"))))
;; ── Live demo: actual hyperscript running ───────────────────────
(defcomp
~hyperscript/playground-content
()
(~docs/page
:title "_hyperscript Playground"
(p
:class "text-lg text-gray-600 mb-6"
"Compile "
(code "_hyperscript")
" to SX expressions. "
"The same "
(code "_=\"...\"")
" syntax, compiled to cached bytecode instead of re-parsed every page load.")
(~docs/section
:title "Interactive Playground"
:id "playground"
(p
"Edit the hyperscript source and click Compile to see the tokenized, parsed, and compiled SX output.")
(~hyperscript/playground))
(~docs/section
:title "Pipeline"
:id "pipeline"
(p "Every hyperscript string passes through three stages:")
(ol
:class "list-decimal list-inside space-y-1 text-sm text-gray-700 mb-4"
(li
(strong "Tokenize")
" — source string to typed token stream (keywords, classes, ids, operators, strings)")
(li
(strong "Parse")
" — token stream to AST (commands, expressions, features)")
(li
(strong "Compile")
" — AST to SX expressions targeting "
(code "web/lib/dom.sx")
" primitives"))
(p
"The compiled SX is wrapped in "
(code "(fn (me) ...)")
" and evaluated with "
(code "me")
" bound to the element. Hyperscript variables are visible to "
(code "eval (sx-expr)")
" escapes."))
(~docs/section
:title "Examples"
:id "examples"
(~hyperscript/example
:source "on click add .active to me"
:description "Event handler: click adds a CSS class")
(~hyperscript/example
: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")
(~hyperscript/example
:source "on click render ~card :title 'Hello' into #target"
:description "Render an SX component directly from a hyperscript handler")
(~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")
(~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"))))

View File

@@ -741,6 +741,10 @@
(define sx-nav-tree {:href "/sx/" :children (list {:href "/sx/(geography)" :children (list {:href "/sx/(geography.(reactive))" :children reactive-islands-nav-items :label "Reactive Islands"} {:href "/sx/(geography.(hypermedia))" :children (list {:href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items :label "Reference"} {:href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items :label "Examples"}) :label "Hypermedia Lakes"} {:href "/sx/(geography.(scopes))" :summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag." :label "Scopes"} {:href "/sx/(geography.(provide))" :summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection." :label "Provide / Emit!"} {:href "/sx/(geography.(spreads))" :summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes." :label "Spreads"} {:href "/sx/(geography.(marshes))" :children marshes-examples-nav-items :summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted." :label "Marshes"} {:href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items :label "Isomorphism"} {:href "/sx/(geography.(cek))" :children cek-nav-items :label "CEK Machine"}) :label "Geography"} {:href "/sx/(language)" :children (list {:href "/sx/(language.(doc))" :children docs-nav-items :label "Docs"} {:href "/sx/(language.(spec))" :children specs-nav-items :label "Specs"} {:href "/sx/(language.(spec.(explore.evaluator)))" :label "Spec Explorer"} {:href "/sx/(language.(bootstrapper))" :children bootstrappers-nav-items :label "Bootstrappers"} {:href "/sx/(language.(test))" :children testing-nav-items :label "Testing"}) :label "Language"} {:href "/sx/(applications)" :children (list {:href "/sx/(applications.(sx-urls))" :label "SX URLs"} {:href "/sx/(applications.(cssx))" :children cssx-nav-items :label "CSSX"} {:href "/sx/(applications.(protocol))" :children protocols-nav-items :label "Protocols"} {:href "/sx/(applications.(sx-pub))" :label "sx-pub"} {:href "/sx/(applications.(sx-tools))" :label "SX Tools"} {:href "/sx/(geography.(reactive-runtime))" :children reactive-runtime-nav-items :label "Reactive Runtime"}) :label "Applications"} {:href "/sx/(etc)" :children (list {:href "/sx/(etc.(essay))" :children essays-nav-items :label "Essays"} {:href "/sx/(etc.(philosophy))" :children philosophy-nav-items :label "Philosophy"} {:href "/sx/(etc.(plan))" :children plans-nav-items :label "Plans"}) :label "Etc"}) :label "sx"})
(let
((apps (nth (get sx-nav-tree "children") 2)) (hs-entry {:href "/sx/(applications.(hyperscript))" :label "_hyperscript"}))
(dict-set! apps "children" (append (get apps "children") (list hs-entry))))
(define
has-descendant-href?
(fn
@@ -812,5 +816,10 @@
(find-loop (+ i 1))))))
(find-loop 0))))
(define sxtp-nav-items
(define
hyperscript-nav-items
(list (dict :label "_hyperscript" :href "/sx/(applications.(hyperscript))")))
(define
sxtp-nav-items
(list (dict :label "SXTP Protocol" :href "/sx/(applications.(sxtp))")))

View File

@@ -708,6 +708,14 @@
eval-rules
(fn (&key title &rest args) (quasiquote (~geography/eval-rules-content))))
(define
hyperscript
(make-page-fn
"~hyperscript/playground-content"
"~hyperscript/"
nil
"-content"))
(define
sxtp
(make-page-fn