sx-http: fix </script escaping in serialize_value, inline component-defs

escape_sx_string now escapes </ as <\\/ inside SX string literals,
matching Python's serialize() behavior. This prevents the HTML parser
from matching </script> inside component-defs strings while keeping
the SX valid for the client parser.

Component-defs back to inline <script data-components> (reverts
external endpoint approach). init-sx triggers client render when
sx-root is empty.

Geography page: fully rendered with header, nav, content, styling.
Header island hydration warning (Undefined symbol: let) is a
pre-existing WASM kernel issue, not related to the HTTP server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 18:14:09 +00:00
parent f1d08bbbe9
commit 303fc5c319

View File

@@ -25,14 +25,20 @@ open Sx_types
(** Escape a string for embedding in an SX string literal. *)
let escape_sx_string s =
let buf = Buffer.create (String.length s + 16) in
String.iter (function
let len = String.length s in
let buf = Buffer.create (len + 16) in
for i = 0 to len - 1 do
match s.[i] with
| '"' -> Buffer.add_string buf "\\\""
| '\\' -> Buffer.add_string buf "\\\\"
| '\n' -> Buffer.add_string buf "\\n"
| '\r' -> Buffer.add_string buf "\\r"
| '\t' -> Buffer.add_string buf "\\t"
| c -> Buffer.add_char buf c) s;
| '<' when i + 1 < len && s.[i + 1] = '/' ->
(* Escape </ as <\\/ to prevent HTML parser matching </script> *)
Buffer.add_string buf "<\\\\/"
| c -> Buffer.add_char buf c
done;
Buffer.contents buf
(** Serialize a value to SX text (for io-request args). *)
@@ -1619,14 +1625,10 @@ let http_inject_shell_statics env static_dir sx_sxc =
| _ -> ()
) env.bindings;
let raw_defs = Buffer.contents buf in
(* Don't inline component-defs — serve from /static/sx-components.sx.
Inlining breaks because SX source can contain literal </script> which
the HTML parser uses to close the tag prematurely. *)
let component_defs = "" in
(* Cache the raw defs for the /static/sx-components.sx endpoint *)
Hashtbl.replace static_cache "/static/sx-components.sx"
(Printf.sprintf "HTTP/1.1 200 OK\r\nContent-Type: text/sx; charset=utf-8\r\nContent-Length: %d\r\nCache-Control: public, max-age=31536000, immutable\r\n\r\n%s"
(String.length raw_defs) raw_defs);
(* Component-defs are inlined in <script type="text/sx">.
The escape_sx_string function handles </ → <\\/ inside string
literals, preventing the HTML parser from matching </script>. *)
let component_defs = raw_defs in
let component_hash = Digest.string component_defs |> Digest.to_hex in
(* Compute file hashes for cache busting *)
let sx_js_hash = file_hash (static_dir ^ "/scripts/sx-browser.js") in