//// 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("&", "&")
|> string.replace("\"", """)
|> string.replace("<", "<")
|> string.replace(">", ">")
}