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>
449 lines
15 KiB
Plaintext
449 lines
15 KiB
Plaintext
// Html Module - DSL for building HTML/DOM structures
|
|
//
|
|
// This module provides a type-safe way to describe HTML elements that can be
|
|
// compiled to efficient JavaScript for browser rendering.
|
|
//
|
|
// Example usage:
|
|
// let myView = div([class("container")], [
|
|
// h1([id("title")], [text("Hello!")]),
|
|
// button([onClick(Increment)], [text("+")])
|
|
// ])
|
|
|
|
// Html type represents a DOM structure
|
|
// Parameterized by Msg - the type of messages emitted by event handlers
|
|
pub type Html<M> =
|
|
| Element(String, List<Attr<M>>, List<Html<M>>)
|
|
| Text(String)
|
|
| RawHtml(String)
|
|
| Empty
|
|
|
|
// Attributes that can be applied to elements
|
|
pub type Attr<M> =
|
|
| Class(String)
|
|
| Id(String)
|
|
| Style(String, String)
|
|
| Href(String)
|
|
| Src(String)
|
|
| Alt(String)
|
|
| Type(String)
|
|
| Value(String)
|
|
| Placeholder(String)
|
|
| Disabled(Bool)
|
|
| Checked(Bool)
|
|
| Name(String)
|
|
| OnClick(M)
|
|
| OnInput(fn(String): M)
|
|
| OnSubmit(M)
|
|
| OnChange(fn(String): M)
|
|
| OnMouseEnter(M)
|
|
| OnMouseLeave(M)
|
|
| OnFocus(M)
|
|
| OnBlur(M)
|
|
| OnKeyDown(fn(String): M)
|
|
| OnKeyUp(fn(String): M)
|
|
| DataAttr(String, String)
|
|
| Attribute(String, String)
|
|
|
|
// ============================================================================
|
|
// Element builders - Container elements
|
|
// ============================================================================
|
|
|
|
pub fn div<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("div", attrs, children)
|
|
|
|
pub fn span<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("span", attrs, children)
|
|
|
|
pub fn section<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("section", attrs, children)
|
|
|
|
pub fn article<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("article", attrs, children)
|
|
|
|
pub fn header<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("header", attrs, children)
|
|
|
|
pub fn footer<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("footer", attrs, children)
|
|
|
|
pub fn nav<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("nav", attrs, children)
|
|
|
|
pub fn main<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("main", attrs, children)
|
|
|
|
pub fn aside<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("aside", attrs, children)
|
|
|
|
// ============================================================================
|
|
// Element builders - Text elements
|
|
// ============================================================================
|
|
|
|
pub fn h1<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("h1", attrs, children)
|
|
|
|
pub fn h2<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("h2", attrs, children)
|
|
|
|
pub fn h3<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("h3", attrs, children)
|
|
|
|
pub fn h4<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("h4", attrs, children)
|
|
|
|
pub fn h5<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("h5", attrs, children)
|
|
|
|
pub fn h6<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("h6", attrs, children)
|
|
|
|
pub fn p<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("p", attrs, children)
|
|
|
|
pub fn pre<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("pre", attrs, children)
|
|
|
|
pub fn code<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("code", attrs, children)
|
|
|
|
pub fn blockquote<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("blockquote", attrs, children)
|
|
|
|
// ============================================================================
|
|
// Element builders - Inline elements
|
|
// ============================================================================
|
|
|
|
pub fn a<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("a", attrs, children)
|
|
|
|
pub fn strong<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("strong", attrs, children)
|
|
|
|
pub fn em<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("em", attrs, children)
|
|
|
|
pub fn small<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("small", attrs, children)
|
|
|
|
pub fn br<M>(): Html<M> =
|
|
Element("br", [], [])
|
|
|
|
pub fn hr<M>(): Html<M> =
|
|
Element("hr", [], [])
|
|
|
|
// ============================================================================
|
|
// Element builders - Lists
|
|
// ============================================================================
|
|
|
|
pub fn ul<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("ul", attrs, children)
|
|
|
|
pub fn ol<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("ol", attrs, children)
|
|
|
|
pub fn li<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("li", attrs, children)
|
|
|
|
// ============================================================================
|
|
// Element builders - Forms
|
|
// ============================================================================
|
|
|
|
pub fn form<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("form", attrs, children)
|
|
|
|
pub fn input<M>(attrs: List<Attr<M>>): Html<M> =
|
|
Element("input", attrs, [])
|
|
|
|
pub fn textarea<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("textarea", attrs, children)
|
|
|
|
pub fn button<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("button", attrs, children)
|
|
|
|
pub fn label<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("label", attrs, children)
|
|
|
|
pub fn select<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("select", attrs, children)
|
|
|
|
pub fn option<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("option", attrs, children)
|
|
|
|
// ============================================================================
|
|
// Element builders - Media
|
|
// ============================================================================
|
|
|
|
pub fn img<M>(attrs: List<Attr<M>>): Html<M> =
|
|
Element("img", attrs, [])
|
|
|
|
pub fn video<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("video", attrs, children)
|
|
|
|
pub fn audio<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
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
|
|
// ============================================================================
|
|
|
|
pub fn table<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("table", attrs, children)
|
|
|
|
pub fn thead<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("thead", attrs, children)
|
|
|
|
pub fn tbody<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("tbody", attrs, children)
|
|
|
|
pub fn tr<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("tr", attrs, children)
|
|
|
|
pub fn th<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("th", attrs, children)
|
|
|
|
pub fn td<M>(attrs: List<Attr<M>>, children: List<Html<M>>): Html<M> =
|
|
Element("td", attrs, children)
|
|
|
|
// ============================================================================
|
|
// Text and empty nodes
|
|
// ============================================================================
|
|
|
|
pub fn text<M>(content: String): Html<M> =
|
|
Text(content)
|
|
|
|
pub fn empty<M>(): Html<M> =
|
|
Empty
|
|
|
|
// ============================================================================
|
|
// Attribute helpers
|
|
// ============================================================================
|
|
|
|
pub fn class<M>(name: String): Attr<M> =
|
|
Class(name)
|
|
|
|
pub fn id<M>(name: String): Attr<M> =
|
|
Id(name)
|
|
|
|
pub fn style<M>(property: String, value: String): Attr<M> =
|
|
Style(property, value)
|
|
|
|
pub fn href<M>(url: String): Attr<M> =
|
|
Href(url)
|
|
|
|
pub fn src<M>(url: String): Attr<M> =
|
|
Src(url)
|
|
|
|
pub fn alt<M>(description: String): Attr<M> =
|
|
Alt(description)
|
|
|
|
pub fn inputType<M>(t: String): Attr<M> =
|
|
Type(t)
|
|
|
|
pub fn value<M>(v: String): Attr<M> =
|
|
Value(v)
|
|
|
|
pub fn placeholder<M>(p: String): Attr<M> =
|
|
Placeholder(p)
|
|
|
|
pub fn disabled<M>(d: Bool): Attr<M> =
|
|
Disabled(d)
|
|
|
|
pub fn checked<M>(c: Bool): Attr<M> =
|
|
Checked(c)
|
|
|
|
pub fn name<M>(n: String): Attr<M> =
|
|
Name(n)
|
|
|
|
pub fn onClick<M>(msg: M): Attr<M> =
|
|
OnClick(msg)
|
|
|
|
pub fn onInput<M>(h: fn(String): M): Attr<M> =
|
|
OnInput(h)
|
|
|
|
pub fn onSubmit<M>(msg: M): Attr<M> =
|
|
OnSubmit(msg)
|
|
|
|
pub fn onChange<M>(h: fn(String): M): Attr<M> =
|
|
OnChange(h)
|
|
|
|
pub fn onMouseEnter<M>(msg: M): Attr<M> =
|
|
OnMouseEnter(msg)
|
|
|
|
pub fn onMouseLeave<M>(msg: M): Attr<M> =
|
|
OnMouseLeave(msg)
|
|
|
|
pub fn onFocus<M>(msg: M): Attr<M> =
|
|
OnFocus(msg)
|
|
|
|
pub fn onBlur<M>(msg: M): Attr<M> =
|
|
OnBlur(msg)
|
|
|
|
pub fn onKeyDown<M>(h: fn(String): M): Attr<M> =
|
|
OnKeyDown(h)
|
|
|
|
pub fn onKeyUp<M>(h: fn(String): M): Attr<M> =
|
|
OnKeyUp(h)
|
|
|
|
pub fn data<M>(name: String, value: String): Attr<M> =
|
|
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
|
|
// ============================================================================
|
|
|
|
// Conditionally include an element
|
|
pub fn when<M>(condition: Bool, element: Html<M>): Html<M> =
|
|
if condition then element else Empty
|
|
|
|
// Conditionally apply attributes
|
|
pub fn attrIf<M>(condition: Bool, attr: Attr<M>): List<Attr<M>> =
|
|
if condition then [attr] else []
|
|
|
|
// ============================================================================
|
|
// Static HTML Rendering (for SSG)
|
|
// ============================================================================
|
|
|
|
// Render an attribute to a string
|
|
pub fn renderAttr<M>(attr: Attr<M>): String =
|
|
match attr {
|
|
Class(name) => " class=\"" + name + "\"",
|
|
Id(name) => " id=\"" + name + "\"",
|
|
Style(prop, val) => " style=\"" + prop + ": " + val + "\"",
|
|
Href(url) => " href=\"" + url + "\"",
|
|
Src(url) => " src=\"" + url + "\"",
|
|
Alt(desc) => " alt=\"" + desc + "\"",
|
|
Type(t) => " type=\"" + t + "\"",
|
|
Value(v) => " value=\"" + v + "\"",
|
|
Placeholder(p) => " placeholder=\"" + p + "\"",
|
|
Disabled(true) => " disabled",
|
|
Disabled(false) => "",
|
|
Checked(true) => " checked",
|
|
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(_) => "",
|
|
OnSubmit(_) => "",
|
|
OnChange(_) => "",
|
|
OnMouseEnter(_) => "",
|
|
OnMouseLeave(_) => "",
|
|
OnFocus(_) => "",
|
|
OnBlur(_) => "",
|
|
OnKeyDown(_) => "",
|
|
OnKeyUp(_) => ""
|
|
}
|
|
|
|
// Render attributes list to string
|
|
pub fn renderAttrs<M>(attrs: List<Attr<M>>): String =
|
|
List.fold(attrs, "", fn(acc, attr) => acc + renderAttr(attr))
|
|
|
|
// Self-closing tags
|
|
pub fn isSelfClosing(tag: String): Bool =
|
|
tag == "br" || tag == "hr" || tag == "img" || tag == "input" ||
|
|
tag == "meta" || tag == "link" || tag == "area" || tag == "base" ||
|
|
tag == "col" || tag == "embed" || tag == "source" || tag == "track" || tag == "wbr"
|
|
|
|
// Render Html to string
|
|
pub fn render<M>(html: Html<M>): String =
|
|
match html {
|
|
Element(tag, attrs, children) => {
|
|
let attrStr = renderAttrs(attrs)
|
|
if isSelfClosing(tag) then
|
|
"<" + tag + attrStr + " />"
|
|
else {
|
|
let childrenStr = List.fold(children, "", fn(acc, child) => acc + render(child))
|
|
"<" + tag + attrStr + ">" + childrenStr + "</" + tag + ">"
|
|
}
|
|
},
|
|
Text(content) => escapeHtml(content),
|
|
RawHtml(content) => content,
|
|
Empty => ""
|
|
}
|
|
|
|
// Escape HTML special characters
|
|
pub fn escapeHtml(s: String): String = {
|
|
// Simple replacement - a full implementation would handle all entities
|
|
let s1 = String.replace(s, "&", "&")
|
|
let s2 = String.replace(s1, "<", "<")
|
|
let s3 = String.replace(s2, ">", ">")
|
|
let s4 = String.replace(s3, "\"", """)
|
|
s4
|
|
}
|
|
|
|
// Render a full HTML document (basic)
|
|
pub fn document(title: 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)])],
|
|
headExtra
|
|
])
|
|
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("body", [], bodyContent)
|
|
])
|
|
"<!DOCTYPE html>\n" + render(doc)
|
|
}
|