diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 805918f9..ccc40be1 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -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 diff --git a/sx/sx/app-config.sx b/sx/sx/app-config.sx index 86579a23..95aba90a 100644 --- a/sx/sx/app-config.sx +++ b/sx/sx/app-config.sx @@ -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-"))) diff --git a/sx/sx/handlers/hyperscript-api.sx b/sx/sx/handlers/hyperscript-api.sx new file mode 100644 index 00000000..5f9acffa --- /dev/null +++ b/sx/sx/handlers/hyperscript-api.sx @@ -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))))))) \ No newline at end of file diff --git a/sx/sx/hyperscript.sx b/sx/sx/hyperscript.sx new file mode 100644 index 00000000..6b6d702b --- /dev/null +++ b/sx/sx/hyperscript.sx @@ -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")))) \ No newline at end of file diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 53a1f9e3..a948736c 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -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))"))) diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index 46a96e60..24f4c751 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -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