/ src / lightspeed / transport / wisp_html.gleam
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("&", "&amp;")
104    |> string.replace("\"", "&quot;")
105    |> string.replace("<", "&lt;")
106    |> string.replace(">", "&gt;")
107  }