// 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 =
| Element(String, List>, List>)
| Text(String)
| RawHtml(String)
| Empty
// Attributes that can be applied to elements
pub type Attr =
| 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(attrs: List>, children: List>): Html =
Element("div", attrs, children)
pub fn span(attrs: List>, children: List>): Html =
Element("span", attrs, children)
pub fn section(attrs: List>, children: List>): Html =
Element("section", attrs, children)
pub fn article(attrs: List>, children: List>): Html =
Element("article", attrs, children)
pub fn header(attrs: List>, children: List>): Html =
Element("header", attrs, children)
pub fn footer(attrs: List>, children: List>): Html =
Element("footer", attrs, children)
pub fn nav(attrs: List>, children: List>): Html =
Element("nav", attrs, children)
pub fn main(attrs: List>, children: List>): Html =
Element("main", attrs, children)
pub fn aside(attrs: List>, children: List>): Html =
Element("aside", attrs, children)
// ============================================================================
// Element builders - Text elements
// ============================================================================
pub fn h1(attrs: List>, children: List>): Html =
Element("h1", attrs, children)
pub fn h2(attrs: List>, children: List>): Html =
Element("h2", attrs, children)
pub fn h3(attrs: List>, children: List>): Html =
Element("h3", attrs, children)
pub fn h4(attrs: List>, children: List>): Html =
Element("h4", attrs, children)
pub fn h5(attrs: List>, children: List>): Html =
Element("h5", attrs, children)
pub fn h6(attrs: List>, children: List>): Html =
Element("h6", attrs, children)
pub fn p(attrs: List>, children: List>): Html =
Element("p", attrs, children)
pub fn pre(attrs: List>, children: List>): Html =
Element("pre", attrs, children)
pub fn code(attrs: List>, children: List>): Html =
Element("code", attrs, children)
pub fn blockquote(attrs: List>, children: List>): Html =
Element("blockquote", attrs, children)
// ============================================================================
// Element builders - Inline elements
// ============================================================================
pub fn a(attrs: List>, children: List>): Html =
Element("a", attrs, children)
pub fn strong(attrs: List>, children: List>): Html =
Element("strong", attrs, children)
pub fn em(attrs: List>, children: List>): Html =
Element("em", attrs, children)
pub fn small(attrs: List>, children: List>): Html =
Element("small", attrs, children)
pub fn br(): Html =
Element("br", [], [])
pub fn hr(): Html =
Element("hr", [], [])
// ============================================================================
// Element builders - Lists
// ============================================================================
pub fn ul(attrs: List>, children: List>): Html =
Element("ul", attrs, children)
pub fn ol(attrs: List>, children: List>): Html =
Element("ol", attrs, children)
pub fn li(attrs: List>, children: List>): Html =
Element("li", attrs, children)
// ============================================================================
// Element builders - Forms
// ============================================================================
pub fn form(attrs: List>, children: List>): Html =
Element("form", attrs, children)
pub fn input(attrs: List>): Html =
Element("input", attrs, [])
pub fn textarea(attrs: List>, children: List>): Html =
Element("textarea", attrs, children)
pub fn button(attrs: List>, children: List>): Html =
Element("button", attrs, children)
pub fn label(attrs: List>, children: List>): Html =
Element("label", attrs, children)
pub fn select(attrs: List>, children: List>): Html =
Element("select", attrs, children)
pub fn option(attrs: List>, children: List>): Html =
Element("option", attrs, children)
// ============================================================================
// Element builders - Media
// ============================================================================
pub fn img(attrs: List>): Html =
Element("img", attrs, [])
pub fn video(attrs: List>, children: List>): Html =
Element("video", attrs, children)
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
// ============================================================================
pub fn table(attrs: List>, children: List>): Html =
Element("table", attrs, children)
pub fn thead(attrs: List>, children: List>): Html =
Element("thead", attrs, children)
pub fn tbody(attrs: List>, children: List>): Html =
Element("tbody", attrs, children)
pub fn tr(attrs: List>, children: List>): Html =
Element("tr", attrs, children)
pub fn th(attrs: List>, children: List>): Html =
Element("th", attrs, children)
pub fn td(attrs: List>, children: List>): Html =
Element("td", attrs, children)
// ============================================================================
// Text and empty nodes
// ============================================================================
pub fn text(content: String): Html =
Text(content)
pub fn empty(): Html =
Empty
// ============================================================================
// Attribute helpers
// ============================================================================
pub fn class(name: String): Attr =
Class(name)
pub fn id(name: String): Attr =
Id(name)
pub fn style(property: String, value: String): Attr =
Style(property, value)
pub fn href(url: String): Attr =
Href(url)
pub fn src(url: String): Attr =
Src(url)
pub fn alt(description: String): Attr =
Alt(description)
pub fn inputType(t: String): Attr =
Type(t)
pub fn value(v: String): Attr =
Value(v)
pub fn placeholder(p: String): Attr =
Placeholder(p)
pub fn disabled(d: Bool): Attr =
Disabled(d)
pub fn checked(c: Bool): Attr =
Checked(c)
pub fn name(n: String): Attr =
Name(n)
pub fn onClick(msg: M): Attr =
OnClick(msg)
pub fn onInput(h: fn(String): M): Attr =
OnInput(h)
pub fn onSubmit(msg: M): Attr =
OnSubmit(msg)
pub fn onChange(h: fn(String): M): Attr =
OnChange(h)
pub fn onMouseEnter(msg: M): Attr =
OnMouseEnter(msg)
pub fn onMouseLeave(msg: M): Attr =
OnMouseLeave(msg)
pub fn onFocus(msg: M): Attr =
OnFocus(msg)
pub fn onBlur(msg: M): Attr =
OnBlur(msg)
pub fn onKeyDown(h: fn(String): M): Attr =
OnKeyDown(h)
pub fn onKeyUp(h: fn(String): M): Attr =
OnKeyUp(h)
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
// ============================================================================
// Conditionally include an element
pub fn when(condition: Bool, element: Html): Html =
if condition then element else Empty
// Conditionally apply attributes
pub fn attrIf(condition: Bool, attr: Attr): List> =
if condition then [attr] else []
// ============================================================================
// Static HTML Rendering (for SSG)
// ============================================================================
// Render an attribute to a string
pub fn renderAttr(attr: Attr): 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(attrs: List>): 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(html: Html): 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>, 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)])],
headExtra
])
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)
])
"\n" + render(doc)
}