From ec78286165ff77d43e2b48861926ae92f0d8b592 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 20 Feb 2026 10:36:56 -0500 Subject: [PATCH] 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 --- stdlib/html.lux | 72 ++++++++++++++++++++++++++++++++++++++++++++++--- stdlib/http.lux | 35 ++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/stdlib/html.lux b/stdlib/html.lux index b31e68d..d9935d6 100644 --- a/stdlib/html.lux +++ b/stdlib/html.lux @@ -14,6 +14,7 @@ pub type Html = | Element(String, List>, List>) | Text(String) + | RawHtml(String) | Empty // Attributes that can be applied to elements @@ -41,6 +42,7 @@ pub type Attr = | OnKeyDown(fn(String): M) | OnKeyUp(fn(String): M) | DataAttr(String, String) + | Attribute(String, String) // ============================================================================ // Element builders - Container elements @@ -180,6 +182,28 @@ pub fn video(attrs: List>, children: List>): Html = pub fn audio(attrs: List>, children: List>): Html = Element("audio", attrs, children) +// ============================================================================ +// Element builders - Document / Head elements +// ============================================================================ + +pub fn meta(attrs: List>): Html = + Element("meta", attrs, []) + +pub fn link(attrs: List>): Html = + Element("link", attrs, []) + +pub fn script(attrs: List>, children: List>): Html = + Element("script", attrs, children) + +pub fn iframe(attrs: List>, children: List>): Html = + Element("iframe", attrs, children) + +pub fn figure(attrs: List>, children: List>): Html = + Element("figure", attrs, children) + +pub fn figcaption(attrs: List>, children: List>): Html = + Element("figcaption", attrs, children) + // ============================================================================ // Element builders - Tables // ============================================================================ @@ -285,6 +309,12 @@ pub fn onKeyUp(h: fn(String): M): Attr = pub fn data(name: String, value: String): Attr = DataAttr(name, value) +pub fn attr(name: String, value: String): Attr = + Attribute(name, value) + +pub fn rawHtml(content: String): Html = + RawHtml(content) + // ============================================================================ // Utility functions // ============================================================================ @@ -319,6 +349,7 @@ pub fn renderAttr(attr: Attr): String = Checked(false) => "", Name(n) => " name=\"" + n + "\"", DataAttr(name, value) => " data-" + name + "=\"" + value + "\"", + Attribute(name, value) => " " + name + "=\"" + value + "\"", // Event handlers are ignored in static rendering OnClick(_) => "", OnInput(_) => "", @@ -355,6 +386,7 @@ pub fn render(html: Html): String = } }, Text(content) => escapeHtml(content), + RawHtml(content) => content, Empty => "" } @@ -368,15 +400,47 @@ pub fn escapeHtml(s: String): String = { s4 } -// Render a full HTML document +// Render a full HTML document (basic) pub fn document(title: String, headExtra: List>, bodyContent: List>): String = { let headElements = List.concat([ - [Element("meta", [DataAttr("charset", "UTF-8")], [])], - [Element("meta", [Name("viewport"), Value("width=device-width, initial-scale=1.0")], [])], + [Element("meta", [Attribute("charset", "UTF-8")], [])], + [Element("meta", [Name("viewport"), Attribute("content", "width=device-width, initial-scale=1.0")], [])], [Element("title", [], [Text(title)])], headExtra ]) - let doc = Element("html", [DataAttr("lang", "en")], [ + let doc = Element("html", [Attribute("lang", "en")], [ + Element("head", [], headElements), + Element("body", [], bodyContent) + ]) + "\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>, + bodyContent: List> +): 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("body", [], bodyContent) ]) diff --git a/stdlib/http.lux b/stdlib/http.lux index ec73e5b..6f338e3 100644 --- a/stdlib/http.lux +++ b/stdlib/http.lux @@ -625,6 +625,41 @@ pub fn router(routes: List, 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 = + 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 // ============================================================