///|
/// An HTML fragment that can stream UTF-8 bytes asynchronously.
///
/// `Html` separates trusted markup from dynamic text. Raw fragments are written
/// unchanged, while `String` values written through template interpolation are
/// HTML-escaped.
///
/// # Example
/// ```mbt check
/// test {
/// let title = "Hello, "
/// let badge = @moonback.Html::raw("new")
/// let _card = @moonback.Html(b => {
/// b <+ ""
/// b <+ "\{title}
"
/// b <+ "\{badge}"
/// b <+ " "
/// })
/// }
/// ```
#valtype
struct Html {
f : async (async (Bytes) -> Unit) -> Unit
}
///|
/// A value that knows how to write itself into an `HtmlBuilder`.
///
/// Implement this trait for types that should be usable in HTML template
/// interpolation, for example `b <+ "\{value}
"`.
pub trait WriteToHtmlBuilder {
/// Writes this value into an `HtmlBuilder`.
async fn write_to(Self, HtmlBuilder) -> Unit
}
///|
/// A streaming builder used by `Html`.
///
/// Literal parts of a template are written as trusted markup. Interpolated
/// values go through `WriteToHtmlBuilder`; the built-in `String`
/// implementation escapes HTML-sensitive characters.
#valtype
struct HtmlBuilder {
k : async (Bytes) -> Unit
}
///|
/// Creates an HTML fragment from trusted raw markup.
///
/// Use this only for markup you already trust. For dynamic text, use
/// `Html::escape` or template interpolation with a `String`.
///
/// # Example
/// ```mbt check
/// test {
/// let _html = @moonback.Html::raw("safe")
/// }
/// ```
pub fn Html::raw(raw : String) -> Html {
{ f: k => k(@utf8.encode(raw)) }
}
///|
/// Creates an HTML fragment by escaping a plain text string.
///
/// # Example
/// ```mbt check
/// test {
/// let _html = @moonback.Html::escape("Tom & ")
/// }
/// ```
pub fn Html::escape(str : String) -> Html {
Html::raw(html_escape(str))
}
///|
/// Creates an HTML fragment from trusted UTF-8 bytes.
///
/// The bytes are written unchanged. The caller is responsible for ensuring that
/// they contain valid UTF-8 and trusted markup.
pub fn Html::raw_utf8_chunk(chunk : Bytes) -> Html {
{ f: k => k(chunk) }
}
///|
/// Builds an HTML fragment with an `HtmlBuilder`.
///
/// This is the main entry point for template writing. Static template text is
/// treated as trusted markup. Interpolated `String` values are escaped, while
/// interpolated `Html` fragments are written as-is.
///
/// # Example
/// ```mbt check
/// test {
/// let title = "Hello, "
/// let badge = @moonback.Html::raw("new")
/// let _card = @moonback.Html(b => {
/// b <+ ""
/// b <+ "\{title}
"
/// b <+ "\{badge}"
/// b <+ " "
/// })
/// }
/// ```
pub fn Html::Html(f : async (HtmlBuilder) -> Unit) -> Html {
{ f: k => f({ k, }) }
}
///|
/// Writes trusted text to the output without escaping.
///
/// Prefer template interpolation for dynamic values. `write_string` is the
/// low-level primitive used for already-escaped strings and trusted literal
/// markup.
pub async fn HtmlBuilder::write_string(
self : HtmlBuilder,
str : String,
) -> Unit {
(self.k)(@utf8.encode(str))
}
///|
/// Writes an interpolated template value.
///
/// This method is used by MoonBit template writing. `String` values are escaped.
/// `Html` values are written unchanged.
///
/// # Example
/// ```mbt check
/// test {
/// let _page = @moonback.Html(b => {
/// let str = ""
/// let br = @moonback.Html::raw("
")
/// b <+ "\{str}"
/// b <+ "\{br}"
/// })
/// }
/// ```
pub async fn[T : WriteToHtmlBuilder] HtmlBuilder::write_string_interpolation(
self : HtmlBuilder,
value : T,
) -> Unit {
value.write_to(self)
}
///|
pub impl WriteToHtmlBuilder for String with fn write_to(
self : String,
builder : HtmlBuilder,
) {
builder.write_string(html_escape(self))
}
///|
pub impl WriteToHtmlBuilder for Html with fn write_to(
self : Html,
builder : HtmlBuilder,
) {
(self.f)(raw => (builder.k)(raw))
}
///|
pub extend Html with WriteToHtmlBuilder::{write_to}
///|
fn html_escape(s : String) -> String {
// TODO: Use lexmatch
let b = StringBuilder()
for ch in s {
match ch {
'&' => b.write_string("&")
'<' => b.write_string("<")
'>' => b.write_string(">")
'"' => b.write_string(""")
'\'' => b.write_string("'")
_ => b.write_char(ch)
}
}
b.to_string()
}