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>
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
;; ==========================================================================
|
||||
;; render.sx — Reference rendering specification
|
||||
;; render.sx — Core rendering specification
|
||||
;;
|
||||
;; Defines how evaluated SX expressions become output (DOM nodes, HTML
|
||||
;; strings, or SX wire format). Each target provides a renderer adapter
|
||||
;; that implements the platform-specific output operations.
|
||||
;; Shared registries and utilities used by all rendering adapters.
|
||||
;; This file defines WHAT is renderable (tag registries, attribute rules)
|
||||
;; and HOW arguments are parsed — but not the output format.
|
||||
;;
|
||||
;; Three rendering modes (matching the Python/JS implementations):
|
||||
;; Adapters:
|
||||
;; adapter-html.sx — HTML string output (server)
|
||||
;; adapter-sx.sx — SX wire format output (server → client)
|
||||
;; adapter-dom.sx — Live DOM node output (browser)
|
||||
;;
|
||||
;; 1. render-to-dom — produces DOM nodes (browser only)
|
||||
;; 2. render-to-html — produces HTML string (server)
|
||||
;; 3. render-to-sx — produces SX wire format (server → client)
|
||||
;;
|
||||
;; This file specifies the LOGIC of rendering. Platform-specific
|
||||
;; operations are declared as interfaces at the bottom.
|
||||
;; Each adapter imports these shared definitions and provides its own
|
||||
;; render entry point (render-to-html, render-to-sx, render-to-dom).
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
@@ -69,104 +68,13 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-to-html — server-side HTML rendering
|
||||
;; Shared utilities
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(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))
|
||||
;; 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)))))
|
||||
|
||||
(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 call (~name)
|
||||
(starts-with? name "~")
|
||||
(let ((comp (env-get env name)))
|
||||
(if (component? comp)
|
||||
(render-to-html
|
||||
(trampoline (call-component comp args env))
|
||||
env)
|
||||
(error (str "Unknown component: " name))))
|
||||
|
||||
;; Definitions — evaluate for side effects, render nothing
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))
|
||||
(do (trampoline (eval-expr expr env)) "")
|
||||
|
||||
;; Macro expansion
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(render-to-html
|
||||
(trampoline
|
||||
(eval-expr
|
||||
(expand-macro (env-get env name) args env)
|
||||
env))
|
||||
env)
|
||||
|
||||
;; Special form / function call — evaluate then render result
|
||||
:else
|
||||
(render-value-to-html
|
||||
(trampoline (eval-expr expr env))
|
||||
env))))))))
|
||||
|
||||
|
||||
(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 ">"))))))
|
||||
(define definition-form?
|
||||
(fn (name)
|
||||
(or (= name "define") (= name "defcomp") (= name "defmacro")
|
||||
(= name "defstyle") (= name "defkeyframes") (= name "defhandler"))))
|
||||
|
||||
|
||||
(define parse-element-args
|
||||
@@ -178,7 +86,7 @@
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false)
|
||||
(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))))
|
||||
@@ -194,6 +102,8 @@
|
||||
|
||||
(define render-attrs
|
||||
(fn (attrs)
|
||||
;; Render an attrs dict to an HTML attribute string.
|
||||
;; Used by adapter-html.sx and adapter-sx.sx.
|
||||
(join ""
|
||||
(map
|
||||
(fn (key)
|
||||
@@ -215,141 +125,14 @@
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; render-to-sx — server-side SX wire format (for client rendering)
|
||||
;; --------------------------------------------------------------------------
|
||||
;; This mode serializes the expression as SX source text.
|
||||
;; Component calls are NOT expanded — they're sent to the client.
|
||||
;; HTML tags are serialized as-is. Special forms are evaluated.
|
||||
|
||||
(define render-to-sx
|
||||
(fn (expr env)
|
||||
(let ((result (aser expr env)))
|
||||
(serialize result))))
|
||||
|
||||
(define aser
|
||||
(fn (expr env)
|
||||
;; Evaluate for SX wire format — serialize rendering forms,
|
||||
;; evaluate control flow and function calls.
|
||||
(case (type-of expr)
|
||||
"number" expr
|
||||
"string" expr
|
||||
"boolean" expr
|
||||
"nil" nil
|
||||
|
||||
"symbol"
|
||||
(let ((name (symbol-name expr)))
|
||||
(cond
|
||||
(env-has? env name) (env-get env name)
|
||||
(primitive? name) (get-primitive name)
|
||||
(= name "true") true
|
||||
(= name "false") false
|
||||
(= name "nil") nil
|
||||
:else (error (str "Undefined symbol: " name))))
|
||||
|
||||
"keyword" (keyword-name expr)
|
||||
|
||||
"list"
|
||||
(if (empty? expr)
|
||||
(list)
|
||||
(aser-list expr env))
|
||||
|
||||
:else expr)))
|
||||
|
||||
|
||||
(define aser-list
|
||||
(fn (expr env)
|
||||
(let ((head (first expr))
|
||||
(args (rest expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
(map (fn (x) (aser x env)) expr)
|
||||
(let ((name (symbol-name head)))
|
||||
(cond
|
||||
;; Fragment — serialize children
|
||||
(= name "<>")
|
||||
(aser-fragment args env)
|
||||
|
||||
;; Component call — serialize WITHOUT expanding
|
||||
(starts-with? name "~")
|
||||
(aser-call name args env)
|
||||
|
||||
;; HTML tag — serialize
|
||||
(contains? HTML_TAGS name)
|
||||
(aser-call name args env)
|
||||
|
||||
;; Special/HO forms — evaluate (produces data)
|
||||
(or (special-form? name) (ho-form? name))
|
||||
(aser-special name expr env)
|
||||
|
||||
;; Macro — expand then aser
|
||||
(and (env-has? env name) (macro? (env-get env name)))
|
||||
(aser (expand-macro (env-get env name) args env) env)
|
||||
|
||||
;; Function call — evaluate fully
|
||||
:else
|
||||
(let ((f (trampoline (eval-expr head env)))
|
||||
(evaled-args (map (fn (a) (trampoline (eval-expr a env))) args)))
|
||||
(cond
|
||||
(and (callable? f) (not (lambda? f)) (not (component? f)))
|
||||
(apply f evaled-args)
|
||||
(lambda? f)
|
||||
(trampoline (call-lambda f evaled-args env))
|
||||
(component? f)
|
||||
(aser-call (str "~" (component-name f)) args env)
|
||||
:else (error (str "Not callable: " (inspect f)))))))))))
|
||||
|
||||
|
||||
(define aser-fragment
|
||||
(fn (children env)
|
||||
;; Serialize (<> child1 child2 ...) to sx source string
|
||||
(let ((parts (filter
|
||||
(fn (x) (not (nil? x)))
|
||||
(map (fn (c) (aser c env)) children))))
|
||||
(if (empty? parts)
|
||||
""
|
||||
(str "(<> " (join " " (map serialize parts)) ")")))))
|
||||
|
||||
|
||||
(define aser-call
|
||||
(fn (name args env)
|
||||
;; Serialize (name :key val child ...) — evaluate args but keep as sx
|
||||
(let ((parts (list name)))
|
||||
(reduce
|
||||
(fn (state arg)
|
||||
(let ((skip (get state "skip")))
|
||||
(if skip
|
||||
(assoc state "skip" false)
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc (get state "i")) (len args)))
|
||||
(let ((val (aser (nth args (inc (get state "i"))) env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (str ":" (keyword-name arg)))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
(let ((val (aser arg env)))
|
||||
(when (not (nil? val))
|
||||
(append! parts (serialize val)))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
(str "(" (join " " parts) ")"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform rendering interface
|
||||
;; Platform interface (shared across adapters)
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; HTML rendering (server targets):
|
||||
;; HTML/attribute escaping (used by HTML and SX wire adapters):
|
||||
;; (escape-html s) → HTML-escaped string
|
||||
;; (escape-attr s) → attribute-value-escaped string
|
||||
;; (raw-html-content r) → unwrap RawHTML marker to string
|
||||
;;
|
||||
;; DOM rendering (browser target):
|
||||
;; (create-element tag) → DOM Element
|
||||
;; (create-text-node s) → DOM Text
|
||||
;; (create-fragment) → DOM DocumentFragment
|
||||
;; (set-attribute el k v) → void
|
||||
;; (append-child parent c) → void
|
||||
;;
|
||||
;; StyleValue:
|
||||
;; (style-value? x) → boolean (is x a StyleValue?)
|
||||
;; (style-value-class sv) → string (CSS class name)
|
||||
@@ -357,7 +140,7 @@
|
||||
;; Serialization:
|
||||
;; (serialize val) → SX source string representation of val
|
||||
;;
|
||||
;; Form classification:
|
||||
;; Form classification (used by SX wire adapter):
|
||||
;; (special-form? name) → boolean
|
||||
;; (ho-form? name) → boolean
|
||||
;; (aser-special name expr env) → evaluate special/HO form through aser
|
||||
|
||||
Reference in New Issue
Block a user