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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user