Files
mono/shared/sx/ref/adapter-html.sx
giles daeecab310 Restructure SX ref spec into core + selectable adapters
Split monolithic render.sx into core (tag registries, shared utils) plus
four adapter .sx files: adapter-html (server HTML strings), adapter-sx
(SX wire format), adapter-dom (browser DOM nodes), and engine (SxEngine
triggers, morphing, swaps). All adapters written in s-expressions with
platform interface declarations for JS bridge functions.

Bootstrap compiler now accepts --adapters flag to emit targeted builds:
  -a html        → server-only (1108 lines)
  -a dom,engine  → browser-only (1634 lines)
  -a html,sx     → server with SX wire (1169 lines)
  (default)      → all adapters (1800 lines)

Fixes: keyword arg i-counter desync in reduce across all adapters,
render-aware special forms (let/if/when/cond/map) in HTML adapter,
component children double-escaping, ~prefixed macro dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:49:44 +00:00

313 lines
11 KiB
Plaintext

;; ==========================================================================
;; adapter-html.sx — HTML string rendering adapter
;;
;; Renders evaluated SX expressions to HTML strings. Used server-side.
;;
;; Depends on:
;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS,
;; parse-element-args, render-attrs, definition-form?
;; eval.sx — eval-expr, trampoline, expand-macro, process-bindings,
;; eval-cond, env-has?, env-get, env-set!, env-merge,
;; lambda?, component?, macro?,
;; lambda-closure, lambda-params, lambda-body
;; ==========================================================================
(define render-to-html
(fn (expr env)
(case (type-of expr)
;; Literals — render directly
"nil" ""
"string" (escape-html expr)
"number" (str expr)
"boolean" (if expr "true" "false")
;; List — dispatch to render-list which handles HTML tags, special forms, etc.
"list" (if (empty? expr) "" (render-list-to-html expr env))
;; Symbol — evaluate then render
"symbol" (render-value-to-html (trampoline (eval-expr expr env)) env)
;; Keyword — render as text
"keyword" (escape-html (keyword-name expr))
;; Raw HTML passthrough
"raw-html" (raw-html-content expr)
;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
(define render-value-to-html
(fn (val env)
(case (type-of val)
"nil" ""
"string" (escape-html val)
"number" (str val)
"boolean" (if val "true" "false")
"list" (render-list-to-html val env)
"raw-html" (raw-html-content val)
"style-value" (style-value-class val)
:else (escape-html (str val)))))
;; --------------------------------------------------------------------------
;; Render-aware form classification
;; --------------------------------------------------------------------------
(define RENDER_HTML_FORMS
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
"define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler"
"map" "map-indexed" "filter" "for-each"))
(define render-html-form?
(fn (name)
(contains? RENDER_HTML_FORMS name)))
;; --------------------------------------------------------------------------
;; render-list-to-html — dispatch on list head
;; --------------------------------------------------------------------------
(define render-list-to-html
(fn (expr env)
(if (empty? expr)
""
(let ((head (first expr)))
(if (not (= (type-of head) "symbol"))
;; Data list — render each item
(join "" (map (fn (x) (render-value-to-html x env)) expr))
(let ((name (symbol-name head))
(args (rest expr)))
(cond
;; Fragment
(= name "<>")
(join "" (map (fn (x) (render-to-html x env)) args))
;; Raw HTML passthrough
(= name "raw!")
(join "" (map (fn (x) (str (trampoline (eval-expr x env)))) args))
;; HTML tag
(contains? HTML_TAGS name)
(render-html-element name args env)
;; Component or macro call (~name)
(starts-with? name "~")
(let ((val (env-get env name)))
(cond
(component? val)
(render-html-component val args env)
(macro? val)
(render-to-html
(expand-macro val args env)
env)
:else
(error (str "Unknown component: " name))))
;; Render-aware special forms
(render-html-form? name)
(dispatch-html-form name expr env)
;; Macro expansion
(and (env-has? env name) (macro? (env-get env name)))
(render-to-html
(expand-macro (env-get env name) args env)
env)
;; Fallback — evaluate then render result
:else
(render-value-to-html
(trampoline (eval-expr expr env))
env))))))))
;; --------------------------------------------------------------------------
;; dispatch-html-form — render-aware special form handling for HTML output
;; --------------------------------------------------------------------------
(define dispatch-html-form
(fn (name expr env)
(cond
;; if
(= name "if")
(let ((cond-val (trampoline (eval-expr (nth expr 1) env))))
(if cond-val
(render-to-html (nth expr 2) env)
(if (> (len expr) 3)
(render-to-html (nth expr 3) env)
"")))
;; when
(= name "when")
(if (not (trampoline (eval-expr (nth expr 1) env)))
""
(join ""
(map
(fn (i) (render-to-html (nth expr i) env))
(range 2 (len expr)))))
;; cond
(= name "cond")
(let ((branch (eval-cond (rest expr) env)))
(if branch
(render-to-html branch env)
""))
;; case
(= name "case")
(render-to-html (trampoline (eval-expr expr env)) env)
;; let / let*
(or (= name "let") (= name "let*"))
(let ((local (process-bindings (nth expr 1) env)))
(join ""
(map
(fn (i) (render-to-html (nth expr i) local))
(range 2 (len expr)))))
;; begin / do
(or (= name "begin") (= name "do"))
(join ""
(map
(fn (i) (render-to-html (nth expr i) env))
(range 1 (len expr))))
;; Definition forms — eval for side effects
(definition-form? name)
(do (trampoline (eval-expr expr env)) "")
;; map
(= name "map")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env))))
(join ""
(map
(fn (item)
(if (lambda? f)
(render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env)))
coll)))
;; map-indexed
(= name "map-indexed")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env))))
(join ""
(map-indexed
(fn (i item)
(if (lambda? f)
(render-lambda-html f (list i item) env)
(render-to-html (apply f (list i item)) env)))
coll)))
;; filter — evaluate fully then render
(= name "filter")
(render-to-html (trampoline (eval-expr expr env)) env)
;; for-each (render variant)
(= name "for-each")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env))))
(join ""
(map
(fn (item)
(if (lambda? f)
(render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env)))
coll)))
;; Fallback
:else
(render-value-to-html (trampoline (eval-expr expr env)) env))))
;; --------------------------------------------------------------------------
;; render-lambda-html — render a lambda body in HTML context
;; --------------------------------------------------------------------------
(define render-lambda-html
(fn (f args env)
(let ((local (env-merge (lambda-closure f) env)))
(for-each-indexed
(fn (i p)
(env-set! local p (nth args i)))
(lambda-params f))
(render-to-html (lambda-body f) local))))
;; --------------------------------------------------------------------------
;; render-html-component — expand and render a component
;; --------------------------------------------------------------------------
(define render-html-component
(fn (comp args env)
;; Expand component and render body through HTML adapter.
;; Component body contains rendering forms (HTML tags) that only the
;; adapter understands, so expansion must happen here, not in eval-expr.
(let ((kwargs (dict))
(children (list)))
;; Separate keyword args from positional children
(reduce
(fn (state arg)
(let ((skip (get state "skip")))
(if skip
(assoc state "skip" false "i" (inc (get state "i")))
(if (and (= (type-of arg) "keyword")
(< (inc (get state "i")) (len args)))
(let ((val (trampoline
(eval-expr (nth args (inc (get state "i"))) env))))
(dict-set! kwargs (keyword-name arg) val)
(assoc state "skip" true "i" (inc (get state "i"))))
(do
(append! children arg)
(assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false)
args)
;; Build component env: closure + caller env + params
(let ((local (env-merge (component-closure comp) env)))
;; Bind params from kwargs
(for-each
(fn (p)
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp))
;; If component accepts children, pre-render them to raw HTML
(when (component-has-children? comp)
(env-set! local "children"
(make-raw-html
(join "" (map (fn (c) (render-to-html c env)) children)))))
(render-to-html (component-body comp) local)))))
(define render-html-element
(fn (tag args env)
(let ((parsed (parse-element-args args env))
(attrs (first parsed))
(children (nth parsed 1))
(is-void (contains? VOID_ELEMENTS tag)))
(str "<" tag
(render-attrs attrs)
(if is-void
" />"
(str ">"
(join "" (map (fn (c) (render-to-html c env)) children))
"</" tag ">"))))))
;; --------------------------------------------------------------------------
;; Platform interface — HTML adapter
;; --------------------------------------------------------------------------
;;
;; Inherited from render.sx:
;; escape-html, escape-attr, raw-html-content, style-value?, style-value-class
;;
;; From eval.sx:
;; eval-expr, trampoline, expand-macro, process-bindings, eval-cond
;; env-has?, env-get, env-set!, env-merge
;; lambda?, component?, macro?
;; lambda-closure, lambda-params, lambda-body
;; component-params, component-body, component-closure,
;; component-has-children?, component-name
;;
;; Raw HTML construction:
;; (make-raw-html s) → wrap string as raw HTML (not double-escaped)
;;
;; Iteration:
;; (for-each-indexed fn coll) → call fn(index, item) for each element
;; (map-indexed fn coll) → map fn(index, item) over each element
;; --------------------------------------------------------------------------