;; ========================================================================== ;; adapter-dom.sx — DOM rendering adapter ;; ;; Renders SX expressions to live DOM nodes. Browser-only. ;; Mirrors the render-to-html adapter but produces Element/Text/Fragment ;; nodes instead of HTML strings. ;; ;; Depends on: ;; render.sx — HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form? ;; eval.sx — eval-expr, trampoline, call-component, expand-macro ;; ========================================================================== (define SVG_NS "http://www.w3.org/2000/svg") (define MATH_NS "http://www.w3.org/1998/Math/MathML") ;; -------------------------------------------------------------------------- ;; render-to-dom — main entry point ;; -------------------------------------------------------------------------- (define render-to-dom (fn (expr env ns) (case (type-of expr) ;; nil / boolean false / boolean true → empty fragment "nil" (create-fragment) "boolean" (create-fragment) ;; Pre-rendered raw HTML → parse into fragment "raw-html" (dom-parse-html (raw-html-content expr)) ;; String → text node "string" (create-text-node expr) ;; Number → text node "number" (create-text-node (str expr)) ;; Symbol → evaluate then render "symbol" (render-to-dom (trampoline (eval-expr expr env)) env ns) ;; Keyword → text "keyword" (create-text-node (keyword-name expr)) ;; Pre-rendered DOM node → pass through "dom-node" expr ;; Dict → empty "dict" (create-fragment) ;; List → dispatch "list" (if (empty? expr) (create-fragment) (render-dom-list expr env ns)) ;; Style value → text of class name "style-value" (create-text-node (style-value-class expr)) ;; Fallback :else (create-text-node (str expr))))) ;; -------------------------------------------------------------------------- ;; render-dom-list — dispatch on list head ;; -------------------------------------------------------------------------- (define render-dom-list (fn (expr env ns) (let ((head (first expr))) (cond ;; Symbol head — dispatch on name (= (type-of head) "symbol") (let ((name (symbol-name head)) (args (rest expr))) (cond ;; raw! → insert unescaped HTML (= name "raw!") (render-dom-raw args env) ;; <> → fragment (= name "<>") (render-dom-fragment args env ns) ;; html: prefix → force element rendering (starts-with? name "html:") (render-dom-element (slice name 5) args env ns) ;; Render-aware special forms (render-dom-form? name) (if (and (contains? HTML_TAGS name) (or (and (> (len args) 0) (= (type-of (first args)) "keyword")) ns)) ;; Ambiguous: tag name that's also a form — treat as tag ;; when keyword arg or namespace present (render-dom-element name args env ns) (dispatch-render-form name expr env ns)) ;; Macro expansion (and (env-has? env name) (macro? (env-get env name))) (render-to-dom (expand-macro (env-get env name) args env) env ns) ;; HTML tag (contains? HTML_TAGS name) (render-dom-element name args env ns) ;; Component (~name) (starts-with? name "~") (let ((comp (env-get env name))) (if (component? comp) (render-dom-component comp args env ns) (render-dom-unknown-component name))) ;; Custom element (hyphenated with keyword attrs) (and (> (index-of name "-") 0) (> (len args) 0) (= (type-of (first args)) "keyword")) (render-dom-element name args env ns) ;; Inside SVG/MathML namespace — treat as element ns (render-dom-element name args env ns) ;; Fallback — evaluate then render :else (render-to-dom (trampoline (eval-expr expr env)) env ns))) ;; Lambda or list head → evaluate (or (lambda? head) (= (type-of head) "list")) (render-to-dom (trampoline (eval-expr expr env)) env ns) ;; Data list :else (let ((frag (create-fragment))) (for-each (fn (x) (dom-append frag (render-to-dom x env ns))) expr) frag))))) ;; -------------------------------------------------------------------------- ;; render-dom-element — create a DOM element with attrs and children ;; -------------------------------------------------------------------------- (define render-dom-element (fn (tag args env ns) ;; Detect namespace from tag (let ((new-ns (cond (= tag "svg") SVG_NS (= tag "math") MATH_NS :else ns)) (el (dom-create-element tag new-ns)) (extra-class nil)) ;; Process args: keywords → attrs, others → 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))) ;; Keyword arg → attribute (let ((attr-name (keyword-name arg)) (attr-val (trampoline (eval-expr (nth args (inc (get state "i"))) env)))) (cond ;; nil or false → skip (or (nil? attr-val) (= attr-val false)) nil ;; :style StyleValue → convert to class (and (= attr-name "style") (style-value? attr-val)) (set! extra-class (style-value-class attr-val)) ;; Boolean attr (contains? BOOLEAN_ATTRS attr-name) (when attr-val (dom-set-attr el attr-name "")) ;; true → empty attr (= attr-val true) (dom-set-attr el attr-name "") ;; Normal attr :else (dom-set-attr el attr-name (str attr-val))) (assoc state "skip" true "i" (inc (get state "i")))) ;; Positional arg → child (do (when (not (contains? VOID_ELEMENTS tag)) (dom-append el (render-to-dom arg env new-ns))) (assoc state "i" (inc (get state "i")))))))) (dict "i" 0 "skip" false) args) ;; Merge StyleValue class (when extra-class (let ((existing (dom-get-attr el "class"))) (dom-set-attr el "class" (if existing (str existing " " extra-class) extra-class)))) el))) ;; -------------------------------------------------------------------------- ;; render-dom-component — expand and render a component ;; -------------------------------------------------------------------------- (define render-dom-component (fn (comp args env ns) ;; Parse kwargs and children, bind into component env, render body. (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))) ;; Keyword arg — evaluate in caller's env (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 a fragment (when (component-has-children? comp) (let ((child-frag (create-fragment))) (for-each (fn (c) (dom-append child-frag (render-to-dom c env ns))) children) (env-set! local "children" child-frag))) (render-to-dom (component-body comp) local ns))))) ;; -------------------------------------------------------------------------- ;; render-dom-fragment — render children into a DocumentFragment ;; -------------------------------------------------------------------------- (define render-dom-fragment (fn (args env ns) (let ((frag (create-fragment))) (for-each (fn (x) (dom-append frag (render-to-dom x env ns))) args) frag))) ;; -------------------------------------------------------------------------- ;; render-dom-raw — insert unescaped content ;; -------------------------------------------------------------------------- (define render-dom-raw (fn (args env) (let ((frag (create-fragment))) (for-each (fn (arg) (let ((val (trampoline (eval-expr arg env)))) (cond (= (type-of val) "string") (dom-append frag (dom-parse-html val)) (= (type-of val) "dom-node") (dom-append frag (dom-clone val)) (not (nil? val)) (dom-append frag (create-text-node (str val)))))) args) frag))) ;; -------------------------------------------------------------------------- ;; render-dom-unknown-component — visible warning element ;; -------------------------------------------------------------------------- (define render-dom-unknown-component (fn (name) (let ((el (dom-create-element "div" nil))) (dom-set-attr el "style" "background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;padding:4px 8px;margin:2px;border-radius:4px;font-size:12px;font-family:monospace") (dom-append el (create-text-node (str "Unknown component: " name))) el))) ;; -------------------------------------------------------------------------- ;; Render-aware special forms for DOM output ;; -------------------------------------------------------------------------- ;; These forms need special handling in DOM rendering because they ;; produce DOM nodes rather than evaluated values. (define RENDER_DOM_FORMS (list "if" "when" "cond" "case" "let" "let*" "begin" "do" "define" "defcomp" "defmacro" "defstyle" "defkeyframes" "defhandler" "map" "map-indexed" "filter" "for-each")) (define render-dom-form? (fn (name) (contains? RENDER_DOM_FORMS name))) (define dispatch-render-form (fn (name expr env ns) (cond ;; if (= name "if") (let ((cond-val (trampoline (eval-expr (nth expr 1) env)))) (if cond-val (render-to-dom (nth expr 2) env ns) (if (> (len expr) 3) (render-to-dom (nth expr 3) env ns) (create-fragment)))) ;; when (= name "when") (if (not (trampoline (eval-expr (nth expr 1) env))) (create-fragment) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 2 (len expr))) frag)) ;; cond (= name "cond") (let ((branch (eval-cond (rest expr) env))) (if branch (render-to-dom branch env ns) (create-fragment))) ;; case (= name "case") (render-to-dom (trampoline (eval-expr expr env)) env ns) ;; let / let* (or (= name "let") (= name "let*")) (let ((local (process-bindings (nth expr 1) env)) (frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) local ns))) (range 2 (len expr))) frag) ;; begin / do (or (= name "begin") (= name "do")) (let ((frag (create-fragment))) (for-each (fn (i) (dom-append frag (render-to-dom (nth expr i) env ns))) (range 1 (len expr))) frag) ;; Definition forms — eval for side effects (definition-form? name) (do (trampoline (eval-expr expr env)) (create-fragment)) ;; map (= name "map") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag) ;; map-indexed (= name "map-indexed") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each-indexed (fn (i item) (let ((val (if (lambda? f) (render-lambda-dom f (list i item) env ns) (render-to-dom (apply f (list i item)) env ns)))) (dom-append frag val))) coll) frag) ;; filter — evaluate fully then render (= name "filter") (render-to-dom (trampoline (eval-expr expr env)) env ns) ;; for-each (render variant) (= name "for-each") (let ((f (trampoline (eval-expr (nth expr 1) env))) (coll (trampoline (eval-expr (nth expr 2) env))) (frag (create-fragment))) (for-each (fn (item) (let ((val (if (lambda? f) (render-lambda-dom f (list item) env ns) (render-to-dom (apply f (list item)) env ns)))) (dom-append frag val))) coll) frag) ;; Fallback :else (render-to-dom (trampoline (eval-expr expr env)) env ns)))) ;; -------------------------------------------------------------------------- ;; render-lambda-dom — render a lambda body in DOM context ;; -------------------------------------------------------------------------- (define render-lambda-dom (fn (f args env ns) ;; Bind lambda params and render body as DOM (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-dom (lambda-body f) local ns)))) ;; -------------------------------------------------------------------------- ;; Platform interface — DOM adapter ;; -------------------------------------------------------------------------- ;; ;; Element creation: ;; (dom-create-element tag ns) → Element (ns=nil for HTML, string for SVG/MathML) ;; (create-text-node s) → Text node ;; (create-fragment) → DocumentFragment ;; ;; Tree mutation: ;; (dom-append parent child) → void (appendChild) ;; (dom-set-attr el name val) → void (setAttribute) ;; (dom-get-attr el name) → string or nil (getAttribute) ;; ;; Content parsing: ;; (dom-parse-html s) → DocumentFragment from HTML string ;; (dom-clone node) → deep clone of a DOM node ;; ;; Type checking: ;; DOM nodes have type-of → "dom-node" ;; ;; From render.sx: ;; HTML_TAGS, VOID_ELEMENTS, BOOLEAN_ATTRS, definition-form? ;; 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 ;; ;; Iteration: ;; (for-each-indexed fn coll) → call fn(index, item) for each element ;; --------------------------------------------------------------------------