feat: enhance Html and Http stdlib modules

Html: add RawHtml, Attribute, meta/link/script/iframe/figure/figcaption
elements, attr() helper, rawHtml() helper, seoDocument() for SEO meta
tags, fix document() to use Attribute instead of DataAttr for standard
HTML attributes.

Http: add serveStaticFile(), parseFormBody(), getFormField(),
sendResponse() convenience helpers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 10:36:56 -05:00
parent f2688072ac
commit ec78286165
2 changed files with 103 additions and 4 deletions

View File

@@ -14,6 +14,7 @@
pub type Html<M> = pub type Html<M> =
| Element(String, List<Attr<M>>, List<Html<M>>) | Element(String, List<Attr<M>>, List<Html<M>>)
| Text(String) | Text(String)
| RawHtml(String)
| Empty | Empty
// Attributes that can be applied to elements // Attributes that can be applied to elements
@@ -41,6 +42,7 @@ pub type Attr<M> =
| OnKeyDown(fn(String): M) | OnKeyDown(fn(String): M)
| OnKeyUp(fn(String): M) | OnKeyUp(fn(String): M)
| DataAttr(String, String) | DataAttr(String, String)
| Attribute(String, String)
// ============================================================================ // ============================================================================
// Element builders - Container elements // Element builders - Container elements
@@ -180,6 +182,28 @@ pub fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> = pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("audio", attrs, children) Element("audio", attrs, children)
// ============================================================================
// Element builders - Document / Head elements
// ============================================================================
pub fn meta<M>(attrs: List<Attr<M>>): Html<M> =
Element("meta", attrs, [])
pub fn link<M>(attrs: List<Attr<M>>): Html<M> =
Element("link", attrs, [])
pub fn script<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("script", attrs, children)
pub fn iframe<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("iframe", attrs, children)
pub fn figure<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("figure", attrs, children)
pub fn figcaption<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
Element("figcaption", attrs, children)
// ============================================================================ // ============================================================================
// Element builders - Tables // Element builders - Tables
// ============================================================================ // ============================================================================
@@ -285,6 +309,12 @@ pub fn onKeyUp<M>(h: fn(String): M): Attr<M> =
pub fn data<M>(name: String, value: String): Attr<M> = pub fn data<M>(name: String, value: String): Attr<M> =
DataAttr(name, value) DataAttr(name, value)
pub fn attr<M>(name: String, value: String): Attr<M> =
Attribute(name, value)
pub fn rawHtml<M>(content: String): Html<M> =
RawHtml(content)
// ============================================================================ // ============================================================================
// Utility functions // Utility functions
// ============================================================================ // ============================================================================
@@ -319,6 +349,7 @@ pub fn renderAttr<M>(attr: Attr<M>): String =
Checked(false) => "", Checked(false) => "",
Name(n) => " name=\"" + n + "\"", Name(n) => " name=\"" + n + "\"",
DataAttr(name, value) => " data-" + name + "=\"" + value + "\"", DataAttr(name, value) => " data-" + name + "=\"" + value + "\"",
Attribute(name, value) => " " + name + "=\"" + value + "\"",
// Event handlers are ignored in static rendering // Event handlers are ignored in static rendering
OnClick(_) => "", OnClick(_) => "",
OnInput(_) => "", OnInput(_) => "",
@@ -355,6 +386,7 @@ pub fn render<M>(html: Html<M>): String =
} }
}, },
Text(content) => escapeHtml(content), Text(content) => escapeHtml(content),
RawHtml(content) => content,
Empty => "" Empty => ""
} }
@@ -368,15 +400,47 @@ pub fn escapeHtml(s: String): String = {
s4 s4
} }
// Render a full HTML document // Render a full HTML document (basic)
pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = { pub fn document(title: String, headExtra: List<Html<M>>, bodyContent: List<Html<M>>): String = {
let headElements = List.concat([ let headElements = List.concat([
[Element("meta", [DataAttr("charset", "UTF-8")], [])], [Element("meta", [Attribute("charset", "UTF-8")], [])],
[Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])], [Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
[Element("title", [], [Text(title)])], [Element("title", [], [Text(title)])],
headExtra headExtra
]) ])
let doc = Element("html", [DataAttr("lang", "en")], [ let doc = Element("html", [Attribute("lang", "en")], [
Element("head", [], headElements),
Element("body", [], bodyContent)
])
"<!DOCTYPE html>\n" + render(doc)
}
// Render a full HTML document with SEO meta tags
pub fn seoDocument(
title: String,
description: String,
url: String,
ogImage: String,
headExtra: List<Html<M>>,
bodyContent: List<Html<M>>
): String = {
let headElements = List.concat([
[Element("meta", [Attribute("charset", "UTF-8")], [])],
[Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])],
[Element("title", [], [Text(title)])],
[Element("meta", [Name("description"), Attribute("content", description)], [])],
[Element("meta", [Attribute("property", "og:title"), Attribute("content", title)], [])],
[Element("meta", [Attribute("property", "og:description"), Attribute("content", description)], [])],
[Element("meta", [Attribute("property", "og:type"), Attribute("content", "website")], [])],
[Element("meta", [Attribute("property", "og:url"), Attribute("content", url)], [])],
[Element("meta", [Attribute("property", "og:image"), Attribute("content", ogImage)], [])],
[Element("meta", [Name("twitter:card"), Attribute("content", "summary_large_image")], [])],
[Element("meta", [Name("twitter:title"), Attribute("content", title)], [])],
[Element("meta", [Name("twitter:description"), Attribute("content", description)], [])],
[Element("link", [Attribute("rel", "canonical"), Href(url)], [])],
headExtra
])
let doc = Element("html", [Attribute("lang", "en")], [
Element("head", [], headElements), Element("head", [], headElements),
Element("body", [], bodyContent) Element("body", [], bodyContent)
]) ])

View File

@@ -625,6 +625,41 @@ pub fn router(routes: List<Route>, notFound: fn(Request): Response): Handler =
} }
} }
// ============================================================
// Static File Serving
// ============================================================
// Serve a static file from disk
pub fn serveStaticFile(basePath: String, requestPath: String): Response with {File} = {
let filePath = basePath + requestPath
if File.exists(filePath) then {
let content = File.read(filePath)
let mime = getMimeType(filePath)
{ status: 200, headers: [("Content-Type", mime)], body: content }
} else
{ status: 404, headers: textHeaders(), body: "Not Found" }
}
// ============================================================
// Form Body Parsing
// ============================================================
// Parse URL-encoded form body (same format as query strings)
pub fn parseFormBody(body: String): List<(String, String)> =
parseQueryParams(body)
// Get a form field value by name
pub fn getFormField(fields: List<(String, String)>, name: String): Option<String> =
getParam(fields, name)
// ============================================================
// Response Helpers
// ============================================================
// Send a Response using HttpServer effect (convenience wrapper)
pub fn sendResponse(resp: Response): Unit with {HttpServer} =
HttpServer.respondWithHeaders(resp.status, resp.body, resp.headers)
// ============================================================ // ============================================================
// Example Usage // Example Usage
// ============================================================ // ============================================================