sx-http: header island working, stepper partial, CSSX source included

Include cssx.sx source in component-defs for client CSSX runtime.
Island placeholders now contain SX call expression for client hydration.
escape_sx_string only escapes </script, not all </ sequences.
serialize_value: no (list) wrapper, matching Python serialize() exactly.

Homepage: header renders (<sx>, tagline, copyright), stepper shows
raw SX (cssx/tw not expanding client-side). Geography fully rendered
including island hydration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 19:23:44 +00:00
parent 31ae9b5110
commit dc5da2f5ed
2 changed files with 46 additions and 27 deletions

View File

@@ -34,8 +34,10 @@ let escape_sx_string s =
| '\n' -> Buffer.add_string buf "\\n"
| '\r' -> Buffer.add_string buf "\\r"
| '\t' -> Buffer.add_string buf "\\t"
| '<' when i + 1 < len && s.[i + 1] = '/' ->
(* Escape </ as <\\/ to prevent HTML parser matching </script> *)
| '<' when i + 7 < len && s.[i + 1] = '/' &&
(s.[i + 2] = 's' || s.[i + 2] = 'S') &&
String.lowercase_ascii (String.sub s (i + 2) 6) = "script" ->
(* Escape </script as <\\/script to prevent HTML parser closing the tag *)
Buffer.add_string buf "<\\\\/"
| c -> Buffer.add_char buf c
done;
@@ -1515,6 +1517,15 @@ let http_render_page env path =
| String s | SxExpr s -> s
| _ -> serialize_value body_result
in
(* Debug: dump aser for homepage *)
if path = "/sx/" then begin
try
let oc = open_out "/tmp/sx-aser-home.txt" in
output_string oc body_str;
close_out oc;
Printf.eprintf "[debug] Wrote aser for %s: %d bytes\n%!" path (String.length body_str)
with _ -> ()
end;
let t2 = Unix.gettimeofday () in
(* Phase 2: SSR — render to HTML using streaming buffer renderer.
Writes directly to buffer, no intermediate string allocations. *)
@@ -1525,7 +1536,7 @@ let http_render_page env path =
| [e] -> e | [] -> Nil | _ -> List (Symbol "<>" :: body_exprs) in
let buf = Buffer.create 65536 in
(try Sx_render.render_to_buffer buf body_expr env
with e -> Printf.eprintf "[http-ssr] partial: %s\n%!" (Printexc.to_string e));
with e -> Printf.eprintf "[http-ssr] partial for %s: %s\n%!" path (Printexc.to_string e));
Buffer.contents buf
with e ->
Printf.eprintf "[http-ssr] failed: %s\n%!" (Printexc.to_string e); ""
@@ -1631,8 +1642,7 @@ let read_css_file path =
let http_inject_shell_statics env static_dir sx_sxc =
(* Component definitions for client *)
let buf = Buffer.create 65536 in
Hashtbl.iter (fun sym v ->
let name = Sx_types.unintern sym in
Hashtbl.iter (fun _sym v ->
match v with
| Component c ->
let ps = String.concat " " (
@@ -1646,24 +1656,24 @@ let http_inject_shell_statics env static_dir sx_sxc =
(if i.i_has_children then ["&rest"; "children"] else [])) in
Buffer.add_string buf (Printf.sprintf "(defisland ~%s (%s) %s)\n"
i.i_name ps (serialize_value i.i_body))
| Lambda l when l.l_name <> None ->
(* Named lambdas — client needs utility functions like cssx-process-token *)
let fn_name = match l.l_name with Some n -> n | None -> name in
let ps = String.concat " " l.l_params in
Buffer.add_string buf (Printf.sprintf "(define %s (fn (%s) %s))\n"
fn_name ps (serialize_value l.l_body))
| Macro m ->
let ps = String.concat " " m.m_params in
let mname = match m.m_name with Some n -> n | None -> name in
Buffer.add_string buf (Printf.sprintf "(defmacro %s (%s) %s)\n"
mname ps (serialize_value m.m_body))
| Dict _ | Number _ | String _ | Bool _ | List _ | ListRef _ ->
(* Named values (dicts, lists, constants) — client needs CSSX config etc. *)
if String.length name > 0 && name.[0] <> '_' && name.[0] <> '*' then
Buffer.add_string buf (Printf.sprintf "(define %s %s)\n"
name (serialize_value v))
| _ -> ()
) env.bindings;
(* Prepend client library source files — these define functions that
island components need at runtime (CSSX, signals, etc.).
Read directly from .sx files, same as Quart's _CLIENT_LIBRARY_SOURCES. *)
let project_dir = try Sys.getenv "SX_PROJECT_DIR" with Not_found ->
Filename.dirname (Filename.dirname static_dir) in
let templates_dir = project_dir ^ "/shared/sx/templates" in
let client_libs = [
templates_dir ^ "/cssx.sx";
] in
List.iter (fun path ->
if Sys.file_exists path then begin
let src = In_channel.with_open_text path In_channel.input_all in
Buffer.add_string buf src;
Buffer.add_char buf '\n'
end
) client_libs;
let raw_defs = Buffer.contents buf in
(* Component-defs are inlined in <script type="text/sx">.
The escape_sx_string function handles </ → <\\/ inside string

View File

@@ -258,8 +258,12 @@ and render_list_to_html head args env =
| Component c when c.c_affinity = "client" -> "" (* skip client-only *)
| Component _ -> render_component v args env
| Island _i ->
(* Islands are client-rendered — emit placeholder for hydration *)
Printf.sprintf "<span data-sx-island=\"%s\"></span>" _i.i_name
let call_sx = "(" ^ String.concat " " (List.map (fun v ->
match v with
| Symbol s -> s | Keyword k -> ":" ^ k | String s -> "\"" ^ s ^ "\""
| _ -> Sx_runtime.value_to_str v
) (Symbol name :: args)) ^ ")" in
Printf.sprintf "<span data-sx-island=\"%s\">%s</span>" _i.i_name call_sx
| Macro m ->
let expanded = expand_macro m args env in
do_render_to_html expanded env
@@ -501,10 +505,15 @@ and render_list_buf buf head args env =
| Component c when c.c_affinity = "client" -> ()
| Component _ -> render_component_buf buf v args env
| Island _i ->
(* Islands are client-rendered — emit placeholder for hydration.
SSR of island bodies is unreliable (client-only symbols like
signals, DOM refs) and can cascade errors. *)
Buffer.add_string buf (Printf.sprintf "<span data-sx-island=\"%s\"></span>" _i.i_name)
(* Islands are client-rendered — emit placeholder with SX call
expression so the client can hydrate from source. *)
let call_sx = "(" ^ String.concat " " (List.map (fun v ->
match v with
| Symbol s -> s | Keyword k -> ":" ^ k | String s -> "\"" ^ s ^ "\""
| _ -> Sx_runtime.value_to_str v
) (Symbol name :: args)) ^ ")" in
Buffer.add_string buf (Printf.sprintf "<span data-sx-island=\"%s\">%s</span>"
_i.i_name call_sx)
| Macro m ->
let expanded = expand_macro m args env in
render_to_buf buf expanded env