src/lightspeed/transport/wisp_html.gleam

//// Wisp-style server-rendered initial HTML adapter.

import gleam/int
import gleam/string
import lightspeed/diff
import lightspeed/protocol
import lightspeed/transport/contract

/// Initial HTTP render request context.
pub type HtmlRequest {
  HtmlRequest(
    session_id: String,
    route: String,
    csrf_token: String,
    origin: String,
  )
}

/// Minimal HTTP response shape emitted by the adapter.
pub type HtmlResponse {
  HtmlResponse(status: Int, headers: List(#(String, String)), body: String)
}

/// Render initial HTML with adapter metadata markers.
pub fn render_initial(
  request: HtmlRequest,
  rendered_html: String,
  websocket_path: String,
  auth_hook: contract.AuthHook,
) -> Result(HtmlResponse, contract.AdapterError) {
  let context =
    contract.AuthContext(
      session_id: request.session_id,
      route: request.route,
      csrf_token: request.csrf_token,
      origin: request.origin,
    )

  case contract.authenticate(auth_hook, context) {
    contract.Denied(reason) -> Error(contract.AuthenticationFailed(reason))

    contract.Authorized(owner) ->
      Ok(HtmlResponse(
        status: 200,
        headers: [
          #("content-type", "text/html; charset=utf-8"),
        ],
        body: build_document(request, rendered_html, websocket_path, owner),
      ))
  }
}

/// Access response status.
pub fn status(response: HtmlResponse) -> Int {
  response.status
}

/// Access response body.
pub fn body(response: HtmlResponse) -> String {
  response.body
}

fn build_document(
  request: HtmlRequest,
  rendered_html: String,
  websocket_path: String,
  owner: String,
) -> String {
  "<!doctype html>"
  <> "<html lang=\"en\">"
  <> "<head><meta charset=\"utf-8\"><title>Lightspeed</title></head>"
  <> "<body>"
  <> "<main id=\"app\""
  <> " data-ls-session=\""
  <> escape_attr(request.session_id)
  <> "\""
  <> " data-ls-owner=\""
  <> escape_attr(owner)
  <> "\""
  <> " data-ls-route=\""
  <> escape_attr(request.route)
  <> "\""
  <> " data-ls-ws=\""
  <> escape_attr(websocket_path)
  <> "\""
  <> " data-ls-protocol=\""
  <> escape_attr(protocol.protocol_name)
  <> "\""
  <> " data-ls-version=\""
  <> int.to_string(protocol.protocol_version)
  <> "\""
  <> " data-ls-patch-stream-version=\""
  <> int.to_string(diff.patch_stream_version)
  <> "\""
  <> ">"
  <> rendered_html
  <> "</main>"
  <> "</body></html>"
}

fn escape_attr(value: String) -> String {
  value
  |> string.replace("&", "&amp;")
  |> string.replace("\"", "&quot;")
  |> string.replace("<", "&lt;")
  |> string.replace(">", "&gt;")
}