// =============================================================================
// html.mbt — standalone Html/Attr/Event types + render to string
//
// Modelled after rabbita's html API and luna's render output.
// No external dependencies. Works on all MoonBit targets (JS, wasm, native).
//
// Overview
// --------
// - `Html` is the node tree (elements, text, fragments, raw/unsafe html).
// - `Attr` is anything that can sit inside a tag's opening ``,
// including both static attributes/styles and event handlers.
// - `Event` enumerates the DOM events an `Attr::On(_)` can carry; each
// variant holds the JS to run as a plain string (no closures — this is a
// pure string-templating DSL, not a virtual DOM).
// - Element builder functions (`div`, `p`, `a`, ...) are thin, labeled-arg
// wrappers around `Node(tag, attrs, children)` that mirror common HTML
// attributes as keyword parameters so call sites read like HTML.
// - `render` walks an `Html` tree and produces the final HTML string,
// escaping text content and static attribute values, and skipping
// nothing silently — event handlers ARE rendered as inline `on*` attrs.
//
// =============================================================================
///|
/// The `type` attribute value for `` elements.
/// Passed to `input(input_type~)`; rendered as the matching HTML type string
/// (e.g. `Email` -> `type="email"`).
pub(all) enum InputType {
Text
Password
Email
Number
Checkbox
Radio
File
Hidden
Submit
Reset
Button
Range
Date
Color
Search
Tel
Url
}
///|
/// The `target` attribute value for `` (and other linking elements).
/// `Self_` is the default browsing context; the others map 1:1 to the
/// standard `_blank` / `_parent` / `_top` keywords.
pub(all) enum Target {
Self_
Blank
Parent
Top
}
///|
/// All DOM events that can be attached to an element via `Attr::On`.
/// Each variant carries the JS to run (as a raw string, e.g.
/// `"console.log('hi')"`) which is rendered verbatim as an inline
/// `on*="..."` attribute — it is NOT escaped, so callers are responsible
/// for producing valid/safe JS.
pub(all) enum Event {
/// mouse / pointer
Click(String)
DblClick(String)
MouseEnter(String)
MouseLeave(String)
/// form
Change(String)
Input(String)
Submit(String)
Focus(String)
Blur(String)
/// keyboard
KeyDown(String)
KeyUp(String)
KeyPress(String)
}
///|
/// HTML attribute — either a static property/style or a event handler.
/// `Attr` values are order-independent; `render_attr` decides how each one
/// is serialized inside a tag's opening bracket.
pub(all) enum Attr {
// ── static string attributes ─────────────────────────────────────────
Id(String)
Class(String)
Href(String)
Src(String)
Alt(String)
Type_(String) // "type" is a keyword so we use Type_
Value(String)
Placeholder(String)
Name(String)
For_(String) // "for" is a keyword
Action(String)
Method(String)
Charset(String)
Rel(String)
Lang(String)
Disabled(Bool)
Checked(Bool)
Required(Bool)
Readonly(Bool)
AutoFocus(Bool)
Multiple(Bool)
Target_(Target)
Defer(Bool)
Async(Bool)
// ── extra static attributes (forms, media, tables, misc) ─────────────
Rows(Int)
Cols(Int)
Controls(Bool)
Autoplay(Bool)
Loop_(Bool) // "loop" reads oddly as a bare identifier, so we use Loop_
Muted(Bool)
Poster(String)
Min(String)
Max(String)
Step(String)
Selected(Bool)
Label_(String) // clashes with the `label` element builder otherwise
DateTime(String)
Colspan(Int)
Rowspan(Int)
Width(Int)
Height(Int)
Title_(String) // "title" clashes with the `title` element builder
Cite(String)
// ── style ─────────────────────────────────────────────────────────────
// inline styles as (property, value) pairs, e.g. [("color","red")]
Style(Array[(String, String)])
// Shortcuts to prefixxed attributes
Aria(String, String)
Data(String, String)
// arbitrary attribute for anything not listed above
Prop(String, String)
// ── event handlers ────────────────────────────────────────────────────
On(Event)
OnRawEvent(String, String)
}
///|
/// HTML node — the tree that `render` turns into a string.
pub enum Html {
/// An element: tag name, attributes, children
Node(String, Array[Attr], Array[Html])
/// Raw escaped text content
Text(String)
/// Multiple nodes with no wrapping element
Fragment(Array[Html])
/// Raw, un-escaped HTML/JS dropped verbatim into the output. Use with
/// care — nothing here is sanitized, so never build this from untrusted
/// input.
Unsafe(String)
/// Renders nothing (useful as a conditional placeholder)
Nothing
/// An HTML comment; the data is sanitized by `comment` so the rendered
/// output is always a valid comment.
Comment(String)
}
// =============================================================================
// Constructor helpers — mirrors rabbita's public API style
// =============================================================================
///|
/// on_click("console.log(`Hello`)")
pub fn on_click(msg : String) -> Attr {
On(Event::Click(msg))
}
///|
/// on_dblclick("console.log(`Hello`)")
pub fn on_dblclick(msg : String) -> Attr {
On(Event::DblClick(msg))
}
///|
/// on_mouseenter("console.log(`Hello`)")
pub fn on_mouseenter(msg : String) -> Attr {
On(Event::MouseEnter(msg))
}
///|
/// on_mouseleave("console.log(`Hello`)")
pub fn on_mouseleave(msg : String) -> Attr {
On(Event::MouseLeave(msg))
}
///|
/// on_change("console.log(`Hello`)")
pub fn on_change(msg : String) -> Attr {
On(Event::Change(msg))
}
///|
/// on_input("console.log(`Hello`)")
pub fn on_input(msg : String) -> Attr {
On(Event::Input(msg))
}
///|
/// on_submit("console.log(`Hello`)")
pub fn on_submit(msg : String) -> Attr {
On(Event::Submit(msg))
}
///|
/// on_focus("console.log(`Hello`)")
pub fn on_focus(msg : String) -> Attr {
On(Event::Focus(msg))
}
///|
/// on_blur("console.log(`Hello`)")
pub fn on_blur(msg : String) -> Attr {
On(Event::Blur(msg))
}
///|
/// on_keydown("console.log(`Hello`)")
pub fn on_keydown(msg : String) -> Attr {
On(Event::KeyDown(msg))
}
///|
/// on_keyup("console.log(`Hello`)")
pub fn on_keyup(msg : String) -> Attr {
On(Event::KeyUp(msg))
}
///|
/// on_keypress("console.log(`Hello`)")
pub fn on_keypress(msg : String) -> Attr {
On(Event::KeyPress(msg))
}
// =============================================================================
// Element builders — one per common HTML tag
// mirrors rabbita's approach: labeled args for attrs, positional for children
//
// Unless noted otherwise, every "block-style" builder below shares the same
// four optional keyword parameters: `id`, `class`, `style`, and `attrs`
// (for anything not otherwise covered), followed by a positional
// `children : Array[Html]`.
// =============================================================================
///|
/// `` — generic block-level container; the catch-all building block
/// when no more specific element applies.
///
/// Example:
/// `div(class="card", [text("Hello")])` -> `Hello`
pub fn div(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("div", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — generic inline container, e.g. for styling a run of text.
///
/// Example:
/// `span(class="highlight", [text("new")])`
/// -> `new`
pub fn span(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("span", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — a paragraph of text.
///
/// Example:
/// `p([text("Hello, world.")])` -> `
Hello, world.
`
pub fn p(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("p", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — the top-level (most important) section heading.
///
/// Example:
/// `h1([text("Welcome")])` -> `Welcome
`
pub fn h1(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("h1", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — second-level section heading.
///
/// Example:
/// `h2([text("About")])` -> `About
`
pub fn h2(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("h2", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — third-level section heading.
///
/// Example:
/// `h3([text("Details")])` -> `Details
`
pub fn h3(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("h3", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — fourth-level section heading.
///
/// Example:
/// `h4([text("Notes")])` -> `Notes
`
pub fn h4(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("h4", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — fifth-level section heading.
///
/// Example:
/// `h5([text("Fine print")])` -> `Fine print
`
pub fn h5(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("h5", build_attrs(id~, class~, attrs~), children)
}
///|
/// `` — sixth-level (lowest) section heading.
///
/// Example:
/// `h6([text("Footnote")])` -> `Footnote
`
pub fn h6(
id? : String = "",
class? : String = "",
attrs? : Array[Attr] = [],
children : Array[Html],
) -> Html {
Node("h6", build_attrs(id~, class~, attrs~), children)
}
///|
/// `