wisp_html.gleam
1 //// Wisp-style server-rendered initial HTML adapter. 2 3 import gleam/int 4 import gleam/string 5 import lightspeed/diff 6 import lightspeed/protocol 7 import lightspeed/transport/contract 8 9 /// Initial HTTP render request context. 10 pub type HtmlRequest { 11 HtmlRequest( 12 session_id: String, 13 route: String, 14 csrf_token: String, 15 origin: String, 16 ) 17 } 18 19 /// Minimal HTTP response shape emitted by the adapter. 20 pub type HtmlResponse { 21 HtmlResponse(status: Int, headers: List(#(String, String)), body: String) 22 } 23 24 /// Render initial HTML with adapter metadata markers. 25 pub fn render_initial( 26 request: HtmlRequest, 27 rendered_html: String, 28 websocket_path: String, 29 auth_hook: contract.AuthHook, 30 ) -> Result(HtmlResponse, contract.AdapterError) { 31 let context = 32 contract.AuthContext( 33 session_id: request.session_id, 34 route: request.route, 35 csrf_token: request.csrf_token, 36 origin: request.origin, 37 ) 38 39 case contract.authenticate(auth_hook, context) { 40 contract.Denied(reason) -> Error(contract.AuthenticationFailed(reason)) 41 42 contract.Authorized(owner) -> 43 Ok(HtmlResponse( 44 status: 200, 45 headers: [ 46 #("content-type", "text/html; charset=utf-8"), 47 ], 48 body: build_document(request, rendered_html, websocket_path, owner), 49 )) 50 } 51 } 52 53 /// Access response status. 54 pub fn status(response: HtmlResponse) -> Int { 55 response.status 56 } 57 58 /// Access response body. 59 pub fn body(response: HtmlResponse) -> String { 60 response.body 61 } 62 63 fn build_document( 64 request: HtmlRequest, 65 rendered_html: String, 66 websocket_path: String, 67 owner: String, 68 ) -> String { 69 "<!doctype html>" 70 <> "<html lang=\"en\">" 71 <> "<head><meta charset=\"utf-8\"><title>Lightspeed</title></head>" 72 <> "<body>" 73 <> "<main id=\"app\"" 74 <> " data-ls-session=\"" 75 <> escape_attr(request.session_id) 76 <> "\"" 77 <> " data-ls-owner=\"" 78 <> escape_attr(owner) 79 <> "\"" 80 <> " data-ls-route=\"" 81 <> escape_attr(request.route) 82 <> "\"" 83 <> " data-ls-ws=\"" 84 <> escape_attr(websocket_path) 85 <> "\"" 86 <> " data-ls-protocol=\"" 87 <> escape_attr(protocol.protocol_name) 88 <> "\"" 89 <> " data-ls-version=\"" 90 <> int.to_string(protocol.protocol_version) 91 <> "\"" 92 <> " data-ls-patch-stream-version=\"" 93 <> int.to_string(diff.patch_stream_version) 94 <> "\"" 95 <> ">" 96 <> rendered_html 97 <> "</main>" 98 <> "</body></html>" 99 } 100 101 fn escape_attr(value: String) -> String { 102 value 103 |> string.replace("&", "&") 104 |> string.replace("\"", """) 105 |> string.replace("<", "<") 106 |> string.replace(">", ">") 107 }