aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-09-19 23:40:03 +0100
committerHayleigh Thompson <me@hayleigh.dev>2023-09-19 23:40:03 +0100
commit92e8596b78982885803994b50c6b35f73f7a403e (patch)
tree13428243987317da540495215ed4d9e3938fb5cb /src
parent985a9b0aa469cbe94fb95c433c97e2b321014341 (diff)
downloadlustre-92e8596b78982885803994b50c6b35f73f7a403e.tar.gz
lustre-92e8596b78982885803994b50c6b35f73f7a403e.zip
:recycle: So long monorepo.
Diffstat (limited to 'src')
-rw-r--r--src/lustre.ffi.mjs231
-rw-r--r--src/lustre.gleam97
-rw-r--r--src/lustre/attribute.gleam326
-rw-r--r--src/lustre/effect.gleam47
-rw-r--r--src/lustre/element.gleam139
-rw-r--r--src/lustre/element/html.gleam889
-rw-r--r--src/lustre/element/svg.gleam416
-rw-r--r--src/lustre/event.gleam178
-rw-r--r--src/runtime.ffi.mjs239
9 files changed, 2562 insertions, 0 deletions
diff --git a/src/lustre.ffi.mjs b/src/lustre.ffi.mjs
new file mode 100644
index 0000000..f29e7ea
--- /dev/null
+++ b/src/lustre.ffi.mjs
@@ -0,0 +1,231 @@
+import {
+ AppAlreadyStarted,
+ AppNotYetStarted,
+ BadComponentName,
+ ComponentAlreadyRegistered,
+ ElementNotFound,
+ NotABrowser,
+} from "./lustre.mjs";
+import { from } from "./lustre/effect.mjs";
+import { morph } from "./runtime.ffi.mjs";
+import { Ok, Error, isEqual } from "./gleam.mjs";
+
+// RUNTIME ---------------------------------------------------------------------
+
+///
+///
+export class App {
+ #root = null;
+ #state = null;
+ #queue = [];
+ #effects = [];
+ #didUpdate = false;
+
+ #init = null;
+ #update = null;
+ #view = null;
+
+ constructor(init, update, render) {
+ this.#init = init;
+ this.#update = update;
+ this.#view = render;
+ }
+
+ start(selector, flags) {
+ if (!is_browser()) return new Error(new NotABrowser());
+ if (this.#root) return new Error(new AppAlreadyStarted());
+
+ this.#root =
+ selector instanceof HTMLElement
+ ? selector
+ : document.querySelector(selector);
+
+ if (!this.#root) return new Error(new ElementNotFound());
+
+ const [next, effects] = this.#init(flags);
+
+ this.#state = next;
+ this.#effects = effects[0].toArray();
+ this.#didUpdate = true;
+
+ window.requestAnimationFrame(() => this.#tick());
+
+ return new Ok((msg) => this.dispatch(msg));
+ }
+
+ dispatch(msg) {
+ this.#queue.push(msg);
+ this.#tick();
+ }
+
+ emit(name, event = null) {
+ this.#root.dispatchEvent(
+ new CustomEvent(name, {
+ bubbles: true,
+ detail: event,
+ composed: true,
+ })
+ );
+ }
+
+ destroy() {
+ if (!this.#root) return new Error(new AppNotYetStarted());
+
+ this.#root.remove();
+ this.#root = null;
+ this.#state = null;
+ this.#queue = [];
+ this.#effects = [];
+ this.#didUpdate = false;
+ this.#update = () => {};
+ this.#view = () => {};
+ }
+
+ #tick() {
+ this.#flush();
+
+ if (this.#didUpdate) {
+ const vdom = this.#view(this.#state);
+
+ this.#root = morph(this.#root, vdom, (msg) => this.dispatch(msg));
+ this.#didUpdate = false;
+ }
+ }
+
+ #flush(times = 0) {
+ if (!this.#root) return;
+ if (this.#queue.length) {
+ while (this.#queue.length) {
+ const [next, effects] = this.#update(this.#state, this.#queue.shift());
+ // If the user returned their model unchanged and not reconstructed then
+ // we don't need to trigger a re-render.
+ this.#didUpdate ||= this.#state !== next;
+ this.#state = next;
+ this.#effects = this.#effects.concat(effects[0].toArray());
+ }
+ }
+
+ // Each update can produce effects which must now be executed.
+ while (this.#effects.length)
+ this.#effects.shift()(
+ (msg) => this.dispatch(msg),
+ (name, data) => this.emit(name, data)
+ );
+
+ // Synchronous effects will immediately queue a message to be processed. If
+ // it is reasonable, we can process those updates too before proceeding to
+ // the next render.
+ if (this.#queue.length) {
+ times >= 5 ? console.warn(tooManyUpdates) : this.#flush(++times);
+ }
+ }
+}
+
+export const setup = (init, update, render) => new App(init, update, render);
+export const start = (app, selector, flags) => app.start(selector, flags);
+export const destroy = (app) => app.destroy();
+
+export const emit = (name, data) =>
+ // Normal `Effect`s constructed in Gleam from `effect.from` don't get told
+ // about the second argument, but it's there 👀.
+ from((_, emit) => {
+ emit(name, data);
+ });
+
+// HTML EVENTS -----------------------------------------------------------------
+
+export const prevent_default = (e) => e.preventDefault?.();
+export const stop_propagation = (e) => e.stopPropagation?.();
+
+// CUSTOM ELEMENTS -------------------------------------------------------------
+
+export const setup_component = (
+ name,
+ init,
+ update,
+ render,
+ on_attribute_change
+) => {
+ if (!name.includes("-")) return new Error(new BadComponentName());
+ if (!is_browser()) return new Error(new NotABrowser());
+ if (customElements.get(name)) {
+ return new Error(new ComponentAlreadyRegistered());
+ }
+
+ customElements.define(
+ name,
+ class extends HTMLElement {
+ static get observedAttributes() {
+ return on_attribute_change.entries().map(([name, _]) => name);
+ }
+
+ #container = document.createElement("div");
+ #app = null;
+ #dispatch = null;
+
+ constructor() {
+ super();
+
+ this.#app = new App(init, update, render);
+ // This is necessary for ✨ reasons ✨. Clearly there's a bug in the
+ // implementation of either the `App` or the runtime but I con't work it
+ // out.
+ //
+ // If we pass the container to the app directly then the component fails
+ // to render anything to the ODM.
+ this.#container.appendChild(document.createElement("div"));
+
+ const dispatch = this.#app.start(this.#container.firstChild);
+ this.#dispatch = dispatch[0];
+
+ on_attribute_change.forEach((decoder, name) => {
+ Object.defineProperty(this, name, {
+ get: () => {
+ return this[`_${name}`] || this.getAttribute(name);
+ },
+
+ set: (value) => {
+ const prev = this[name];
+ const decoded = decoder(value);
+
+ // We need this equality check to prevent constantly dispatching
+ // messages when the value is an object or array: it might not have
+ // changed but its reference might have and we don't want to trigger
+ // useless updates.
+ if (decoded.isOk() && !isEqual(prev, decoded[0])) {
+ this.#dispatch(decoded[0]);
+ }
+
+ if (typeof value === "string") {
+ this.setAttribute(name, value);
+ } else {
+ this[`_${name}`] = value;
+ }
+ },
+ });
+ });
+ }
+
+ connectedCallback() {
+ this.appendChild(this.#container.firstChild);
+ }
+
+ attributeChangedCallback(name, prev, next) {
+ if (prev !== next) {
+ this[name] = next;
+ }
+ }
+
+ disconnectedCallback() {
+ this.#app.destroy();
+ }
+ }
+ );
+
+ return new Ok(null);
+};
+
+// UTLS ------------------------------------------------------------------------
+
+export const is_browser = () => window && window.document;
+export const is_registered = (name) => !!customElements.get(name);
diff --git a/src/lustre.gleam b/src/lustre.gleam
new file mode 100644
index 0000000..4f8249a
--- /dev/null
+++ b/src/lustre.gleam
@@ -0,0 +1,97 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre](https://pkg.hayleigh.dev/lustre/api/lustre)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/dynamic.{Decoder}
+import gleam/map.{Map}
+import lustre/effect.{Effect}
+import lustre/element.{Element}
+
+// TYPES -----------------------------------------------------------------------
+
+///
+pub type App(flags, model, msg)
+
+pub type Error {
+ AppAlreadyStarted
+ AppNotYetStarted
+ BadComponentName
+ ComponentAlreadyRegistered
+ ElementNotFound
+ NotABrowser
+}
+
+// CONSTRUCTORS ----------------------------------------------------------------
+
+@target(javascript)
+///
+pub fn element(element: Element(msg)) -> App(Nil, Nil, msg) {
+ let init = fn(_) { #(Nil, effect.none()) }
+ let update = fn(_, _) { #(Nil, effect.none()) }
+ let view = fn(_) { element }
+
+ application(init, update, view)
+}
+
+@target(javascript)
+///
+pub fn simple(
+ init: fn(flags) -> model,
+ update: fn(model, msg) -> model,
+ view: fn(model) -> Element(msg),
+) -> App(flags, model, msg) {
+ let init = fn(flags) { #(init(flags), effect.none()) }
+ let update = fn(model, msg) { #(update(model, msg), effect.none()) }
+
+ application(init, update, view)
+}
+
+@target(javascript)
+///
+@external(javascript, "./lustre.ffi.mjs", "setup")
+pub fn application(
+ init: fn(flags) -> #(model, Effect(msg)),
+ update: fn(model, msg) -> #(model, Effect(msg)),
+ view: fn(model) -> Element(msg),
+) -> App(flags, model, msg)
+
+@target(javascript)
+@external(javascript, "./lustre.ffi.mjs", "setup_component")
+pub fn component(
+ name: String,
+ init: fn() -> #(model, Effect(msg)),
+ update: fn(model, msg) -> #(model, Effect(msg)),
+ view: fn(model) -> Element(msg),
+ on_attribute_change: Map(String, Decoder(msg)),
+) -> Result(Nil, Error)
+
+// EFFECTS ---------------------------------------------------------------------
+
+@target(javascript)
+///
+@external(javascript, "./lustre.ffi.mjs", "start")
+pub fn start(
+ app: App(flags, model, msg),
+ selector: String,
+ flags: flags,
+) -> Result(fn(msg) -> Nil, Error)
+
+@target(javascript)
+///
+@external(javascript, "./lustre.ffi.mjs", "destroy")
+pub fn destroy(app: App(flags, model, msg)) -> Result(Nil, Error)
+
+// UTILS -----------------------------------------------------------------------
+
+///
+@external(javascript, "./lustre.ffi.mjs", "is_browser")
+pub fn is_browser() -> Bool {
+ False
+}
+
+///
+@external(javascript, "./lustre.ffi.mjs", "is_registered")
+pub fn is_registered(_name: String) -> Bool {
+ False
+}
diff --git a/src/lustre/attribute.gleam b/src/lustre/attribute.gleam
new file mode 100644
index 0000000..0c79692
--- /dev/null
+++ b/src/lustre/attribute.gleam
@@ -0,0 +1,326 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre/attribute](https://pkg.hayleigh.dev/lustre/api/lustre/attribute)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/dynamic.{Dynamic}
+import gleam/function
+import gleam/int
+import gleam/list
+import gleam/result
+import gleam/string
+import gleam/string_builder.{StringBuilder}
+
+// TYPES -----------------------------------------------------------------------
+
+///
+pub opaque type Attribute(msg) {
+ Attribute(String, Dynamic, as_property: Bool)
+ Event(String, fn(Dynamic) -> Result(msg, Nil))
+}
+
+// CONSTRUCTORS ----------------------------------------------------------------
+
+///
+pub fn attribute(name: String, value: String) -> Attribute(msg) {
+ Attribute(name, dynamic.from(value), as_property: False)
+}
+
+///
+pub fn property(name: String, value: any) -> Attribute(msg) {
+ Attribute(name, dynamic.from(value), as_property: True)
+}
+
+///
+pub fn on(
+ name: String,
+ handler: fn(Dynamic) -> Result(msg, error),
+) -> Attribute(msg) {
+ Event("on" <> name, function.compose(handler, result.replace_error(_, Nil)))
+}
+
+// MANIPULATIONS ---------------------------------------------------------------
+
+///
+pub fn map(attr: Attribute(a), f: fn(a) -> b) -> Attribute(b) {
+ case attr {
+ Attribute(name, value, as_property) -> Attribute(name, value, as_property)
+ Event(on, handler) -> Event(on, fn(e) { result.map(handler(e), f) })
+ }
+}
+
+// CONVERSIONS -----------------------------------------------------------------
+
+///
+///
+pub fn to_string(attr: Attribute(msg)) -> String {
+ case attr {
+ Attribute(name, value, as_property: False) -> {
+ case dynamic.classify(value) {
+ "String" -> name <> "=\"" <> dynamic.unsafe_coerce(value) <> "\""
+
+ // Boolean attributes are determined based on their presence, eg we don't
+ // want to render `disabled="false"` if the value is `false` we simply
+ // want to omit the attribute altogether.
+ "Boolean" ->
+ case dynamic.unsafe_coerce(value) {
+ True -> name
+ False -> ""
+ }
+
+ // For everything else we'll just make a best-effort serialisation.
+ _ -> name <> "=\"" <> string.inspect(value) <> "\""
+ }
+ }
+ Attribute(_, _, as_property: True) -> ""
+ Event(on, _) -> "data-lustre-on:" <> on
+ }
+}
+
+///
+pub fn to_string_builder(attr: Attribute(msg)) -> StringBuilder {
+ case attr {
+ Attribute(name, value, as_property: False) -> {
+ case dynamic.classify(value) {
+ "String" ->
+ [name, "=\"", dynamic.unsafe_coerce(value), "\""]
+ |> string_builder.from_strings
+
+ // Boolean attributes are determined based on their presence, eg we don't
+ // want to render `disabled="false"` if the value is `false` we simply
+ // want to omit the attribute altogether.
+ "Boolean" ->
+ case dynamic.unsafe_coerce(value) {
+ True -> string_builder.from_string(name)
+ False -> string_builder.new()
+ }
+
+ // For everything else we'll just make a best-effort serialisation.
+ _ ->
+ [name, "=\"", string.inspect(value), "\""]
+ |> string_builder.from_strings
+ }
+ }
+ Attribute(_, _, as_property: True) -> string_builder.new()
+ Event(on, _) ->
+ ["data-lustre-on:", on]
+ |> string_builder.from_strings
+ }
+}
+
+// COMMON ATTRIBUTES -----------------------------------------------------------
+
+///
+pub fn style(properties: List(#(String, String))) -> Attribute(msg) {
+ attribute(
+ "style",
+ {
+ use styles, #(name, value) <- list.fold(properties, "")
+ styles <> name <> ":" <> value <> ";"
+ },
+ )
+}
+
+///
+pub fn class(name: String) -> Attribute(msg) {
+ attribute("class", name)
+}
+
+///
+pub fn classes(names: List(#(String, Bool))) -> Attribute(msg) {
+ attribute(
+ "class",
+ names
+ |> list.filter_map(fn(class) {
+ case class.1 {
+ True -> Ok(class.0)
+ False -> Error(Nil)
+ }
+ })
+ |> string.join(" "),
+ )
+}
+
+///
+pub fn id(name: String) -> Attribute(msg) {
+ attribute("id", name)
+}
+
+// INPUTS ----------------------------------------------------------------------
+
+///
+pub fn type_(name: String) -> Attribute(msg) {
+ attribute("type", name)
+}
+
+///
+pub fn value(val: Dynamic) -> Attribute(msg) {
+ property("value", val)
+}
+
+///
+pub fn checked(is_checked: Bool) -> Attribute(msg) {
+ property("checked", is_checked)
+}
+
+///
+pub fn placeholder(text: String) -> Attribute(msg) {
+ attribute("placeholder", text)
+}
+
+///
+pub fn selected(is_selected: Bool) -> Attribute(msg) {
+ property("selected", is_selected)
+}
+
+// INPUT HELPERS ---------------------------------------------------------------
+
+///
+pub fn accept(types: List(String)) -> Attribute(msg) {
+ attribute("accept", string.join(types, " "))
+}
+
+///
+pub fn accept_charset(types: List(String)) -> Attribute(msg) {
+ attribute("acceptCharset", string.join(types, " "))
+}
+
+///
+pub fn msg(uri: String) -> Attribute(msg) {
+ attribute("msg", uri)
+}
+
+///
+pub fn autocomplete(name: String) -> Attribute(msg) {
+ attribute("autocomplete", name)
+}
+
+///
+pub fn autofocus(should_autofocus: Bool) -> Attribute(msg) {
+ property("autoFocus", should_autofocus)
+}
+
+///
+pub fn disabled(is_disabled: Bool) -> Attribute(msg) {
+ property("disabled", is_disabled)
+}
+
+///
+pub fn name(name: String) -> Attribute(msg) {
+ attribute("name", name)
+}
+
+///
+pub fn pattern(regex: String) -> Attribute(msg) {
+ attribute("pattern", regex)
+}
+
+///
+pub fn readonly(is_readonly: Bool) -> Attribute(msg) {
+ property("readonly", is_readonly)
+}
+
+///
+pub fn required(is_required: Bool) -> Attribute(msg) {
+ property("required", is_required)
+}
+
+///
+pub fn for(id: String) -> Attribute(msg) {
+ attribute("for", id)
+}
+
+// INPUT RANGES ----------------------------------------------------------------
+
+///
+pub fn max(val: String) -> Attribute(msg) {
+ attribute("max", val)
+}
+
+///
+pub fn min(val: String) -> Attribute(msg) {
+ attribute("min", val)
+}
+
+///
+pub fn step(val: String) -> Attribute(msg) {
+ attribute("step", val)
+}
+
+// INPUT TEXT AREAS ------------------------------------------------------------
+
+///
+pub fn cols(val: Int) -> Attribute(msg) {
+ attribute("cols", int.to_string(val))
+}
+
+///
+pub fn rows(val: Int) -> Attribute(msg) {
+ attribute("rows", int.to_string(val))
+}
+
+///
+pub fn wrap(mode: String) -> Attribute(msg) {
+ attribute("wrap", mode)
+}
+
+// LINKS AND AREAS -------------------------------------------------------------
+
+///
+pub fn href(uri: String) -> Attribute(msg) {
+ attribute("href", uri)
+}
+
+///
+pub fn target(target: String) -> Attribute(msg) {
+ attribute("target", target)
+}
+
+///
+pub fn download(filename: String) -> Attribute(msg) {
+ attribute("download", filename)
+}
+
+///
+pub fn rel(relationship: String) -> Attribute(msg) {
+ attribute("rel", relationship)
+}
+
+// EMBEDDED CONTENT ------------------------------------------------------------
+
+///
+pub fn src(uri: String) -> Attribute(msg) {
+ attribute("src", uri)
+}
+
+///
+pub fn height(val: Int) -> Attribute(msg) {
+ property("height", int.to_string(val))
+}
+
+///
+pub fn width(val: Int) -> Attribute(msg) {
+ property("width", int.to_string(val))
+}
+
+///
+pub fn alt(text: String) -> Attribute(msg) {
+ attribute("alt", text)
+}
+
+// AUDIO AND VIDEO -------------------------------------------------------------
+
+///
+pub fn autoplay(should_autoplay: Bool) -> Attribute(msg) {
+ property("autoplay", should_autoplay)
+}
+
+///
+pub fn controls(visible: Bool) -> Attribute(msg) {
+ property("controls", visible)
+}
+
+///
+pub fn loop(should_loop: Bool) -> Attribute(msg) {
+ property("loop", should_loop)
+}
diff --git a/src/lustre/effect.gleam b/src/lustre/effect.gleam
new file mode 100644
index 0000000..964ddd2
--- /dev/null
+++ b/src/lustre/effect.gleam
@@ -0,0 +1,47 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre/effect](https://pkg.hayleigh.dev/lustre/api/lustre/effect)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/list
+
+// TYPES -----------------------------------------------------------------------
+
+///
+pub opaque type Effect(msg) {
+ Effect(List(fn(fn(msg) -> Nil) -> Nil))
+}
+
+// CONSTRUCTORS ----------------------------------------------------------------
+
+///
+pub fn from(effect: fn(fn(msg) -> Nil) -> Nil) -> Effect(msg) {
+ Effect([effect])
+}
+
+/// Typically our app's `update` function needs to return a tuple of
+/// `#(model, Effect(msg))`. When we don't need to perform any side effects we
+/// can just return `none()`!
+///
+pub fn none() -> Effect(msg) {
+ Effect([])
+}
+
+// MANIPULATIONS ---------------------------------------------------------------
+
+///
+pub fn batch(effects: List(Effect(msg))) -> Effect(msg) {
+ Effect({
+ use b, Effect(a) <- list.fold(effects, [])
+ list.append(b, a)
+ })
+}
+
+///
+pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b) {
+ let Effect(l) = effect
+ Effect(list.map(
+ l,
+ fn(effect) { fn(dispatch) { effect(fn(a) { dispatch(f(a)) }) } },
+ ))
+}
diff --git a/src/lustre/element.gleam b/src/lustre/element.gleam
new file mode 100644
index 0000000..4608db7
--- /dev/null
+++ b/src/lustre/element.gleam
@@ -0,0 +1,139 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre/element](https://pkg.hayleigh.dev/lustre/api/lustre/element)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/list
+import gleam/string
+import gleam/string_builder.{StringBuilder}
+import lustre/attribute.{Attribute}
+
+// TYPES -----------------------------------------------------------------------
+
+///
+pub opaque type Element(msg) {
+ Text(String)
+ Element(String, List(Attribute(msg)), List(Element(msg)))
+ ElementNs(String, List(Attribute(msg)), List(Element(msg)), String)
+}
+
+// CONSTRUCTORS ----------------------------------------------------------------
+
+///
+pub fn element(
+ tag: String,
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ Element(tag, attrs, children)
+}
+
+///
+pub fn namespaced(
+ namespace: String,
+ tag: String,
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ ElementNs(tag, attrs, children, namespace)
+}
+
+///
+pub fn text(content: String) -> Element(msg) {
+ Text(content)
+}
+
+fn escape(escaped: String, content: String) -> String {
+ case string.pop_grapheme(content) {
+ Ok(#("<", xs)) -> escape(escaped <> "&lt;", xs)
+ Ok(#(">", xs)) -> escape(escaped <> "&gt;", xs)
+ Ok(#("&", xs)) -> escape(escaped <> "&amp;", xs)
+ Ok(#("\"", xs)) -> escape(escaped <> "&quot;", xs)
+ Ok(#("'", xs)) -> escape(escaped <> "&#x27;", xs)
+ Ok(#(x, xs)) -> escape(escaped <> x, xs)
+ Error(_) -> escaped <> content
+ }
+}
+
+// MANIPULATIONS ---------------------------------------------------------------
+
+///
+pub fn map(element: Element(a), f: fn(a) -> b) -> Element(b) {
+ case element {
+ Text(content) -> Text(content)
+ Element(tag, attrs, children) ->
+ Element(
+ tag,
+ list.map(attrs, attribute.map(_, f)),
+ list.map(children, map(_, f)),
+ )
+ ElementNs(tag, attrs, children, namespace) ->
+ ElementNs(
+ tag,
+ list.map(attrs, attribute.map(_, f)),
+ list.map(children, map(_, f)),
+ namespace,
+ )
+ }
+}
+
+// CONVERSIONS -----------------------------------------------------------------
+
+///
+pub fn to_string(element: Element(msg)) -> String {
+ to_string_builder(element)
+ |> string_builder.to_string
+}
+
+///
+pub fn to_string_builder(element: Element(msg)) -> StringBuilder {
+ case element {
+ Text(content) -> string_builder.from_string(escape("", content))
+ Element("area" as tag, attrs, _)
+ | Element("base" as tag, attrs, _)
+ | Element("br" as tag, attrs, _)
+ | Element("col" as tag, attrs, _)
+ | Element("embed" as tag, attrs, _)
+ | Element("hr" as tag, attrs, _)
+ | Element("img" as tag, attrs, _)
+ | Element("input" as tag, attrs, _)
+ | Element("link" as tag, attrs, _)
+ | Element("meta" as tag, attrs, _)
+ | Element("param" as tag, attrs, _)
+ | Element("source" as tag, attrs, _)
+ | Element("track" as tag, attrs, _)
+ | Element("wbr" as tag, attrs, _) ->
+ string_builder.from_string("<" <> tag)
+ |> attrs_to_string_builder(attrs)
+ |> string_builder.append(">")
+ Element(tag, attrs, children) ->
+ string_builder.from_string("<" <> tag)
+ |> attrs_to_string_builder(attrs)
+ |> string_builder.append(">")
+ |> children_to_string_builder(children)
+ |> string_builder.append("</" <> tag <> ">")
+ ElementNs(tag, attrs, children, namespace) ->
+ string_builder.from_string("<" <> tag)
+ |> attrs_to_string_builder(attrs)
+ |> string_builder.append(" xmlns=\"" <> namespace <> "\"")
+ |> string_builder.append(">")
+ |> children_to_string_builder(children)
+ |> string_builder.append("</" <> tag <> ">")
+ }
+}
+
+fn attrs_to_string_builder(
+ html: StringBuilder,
+ attrs: List(Attribute(msg)),
+) -> StringBuilder {
+ use html, attr <- list.fold(attrs, html)
+ string_builder.append_builder(html, attribute.to_string_builder(attr))
+}
+
+fn children_to_string_builder(
+ html: StringBuilder,
+ children: List(Element(msg)),
+) -> StringBuilder {
+ use html, child <- list.fold(children, html)
+ string_builder.append_builder(html, to_string_builder(child))
+}
diff --git a/src/lustre/element/html.gleam b/src/lustre/element/html.gleam
new file mode 100644
index 0000000..a69a5fd
--- /dev/null
+++ b/src/lustre/element/html.gleam
@@ -0,0 +1,889 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre/element/html](https://pkg.hayleigh.dev/lustre/api/lustre/element/html)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import lustre/element.{Element, element, namespaced, text}
+import lustre/attribute.{Attribute}
+
+// HTML ELEMENTS: MAIN ROOT ----------------------------------------------------
+
+///
+pub fn html(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("html", attrs, children)
+}
+
+// HTML ELEMENTS: DOCUMENT METADATA --------------------------------------------
+
+///
+pub fn base(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("base", attrs, [])
+}
+
+///
+pub fn head(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("head", attrs, children)
+}
+
+///
+pub fn link(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("link", attrs, [])
+}
+
+///
+pub fn meta(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("meta", attrs, [])
+}
+
+///
+pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg) {
+ element("style", attrs, [text(css)])
+}
+
+///
+pub fn title(attrs: List(Attribute(msg)), content: String) -> Element(msg) {
+ element("title", attrs, [text(content)])
+}
+
+// HTML ELEMENTS: SECTIONING ROOT -----------------------------------------------
+
+///
+pub fn body(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("body", attrs, children)
+}
+
+// HTML ELEMENTS: CONTENT SECTIONING -------------------------------------------
+
+///
+pub fn address(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("address", attrs, children)
+}
+
+///
+pub fn article(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("article", attrs, children)
+}
+
+///
+pub fn aside(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("aside", attrs, children)
+}
+
+///
+pub fn footer(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("footer", attrs, children)
+}
+
+///
+pub fn header(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("header", attrs, children)
+}
+
+///
+pub fn h1(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("h1", attrs, children)
+}
+
+///
+pub fn h2(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("h2", attrs, children)
+}
+
+///
+pub fn h3(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("h3", attrs, children)
+}
+
+///
+pub fn h4(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("h4", attrs, children)
+}
+
+///
+pub fn h5(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("h5", attrs, children)
+}
+
+///
+pub fn h6(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("h6", attrs, children)
+}
+
+///
+pub fn hgroup(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("hgroup", attrs, children)
+}
+
+///
+pub fn main(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("main", attrs, children)
+}
+
+///
+pub fn nav(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("nav", attrs, children)
+}
+
+///
+pub fn section(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("section", attrs, children)
+}
+
+///
+pub fn search(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("search", attrs, children)
+}
+
+// HTML ELEMENTS: TEXT CONTENT -------------------------------------------------
+
+///
+pub fn blockquote(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("blockquote", attrs, children)
+}
+
+///
+pub fn dd(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("dd", attrs, children)
+}
+
+///
+pub fn div(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("div", attrs, children)
+}
+
+///
+pub fn dl(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("dl", attrs, children)
+}
+
+///
+pub fn dt(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("dt", attrs, children)
+}
+
+///
+pub fn figcaption(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("figcaption", attrs, children)
+}
+
+///
+pub fn figure(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("figure", attrs, children)
+}
+
+///
+pub fn hr(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("hr", attrs, [])
+}
+
+///
+pub fn li(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("li", attrs, children)
+}
+
+///
+pub fn menu(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("menu", attrs, children)
+}
+
+///
+pub fn ol(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("ol", attrs, children)
+}
+
+///
+pub fn p(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("p", attrs, children)
+}
+
+///
+pub fn pre(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("pre", attrs, children)
+}
+
+///
+pub fn ul(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("ul", attrs, children)
+}
+
+// HTML ELEMENTS: INLINE TEXT SEMANTICS ----------------------------------------
+
+///
+pub fn a(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("a", attrs, children)
+}
+
+///
+pub fn abbr(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("abbr", attrs, children)
+}
+
+///
+pub fn b(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("b", attrs, children)
+}
+
+///
+pub fn bdi(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("bdi", attrs, children)
+}
+
+///
+pub fn bdo(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("bdo", attrs, children)
+}
+
+///
+pub fn br(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("br", attrs, [])
+}
+
+///
+pub fn cite(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("cite", attrs, children)
+}
+
+///
+pub fn code(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("code", attrs, children)
+}
+
+///
+pub fn data(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("data", attrs, children)
+}
+
+///
+pub fn dfn(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("dfn", attrs, children)
+}
+
+///
+pub fn em(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("em", attrs, children)
+}
+
+///
+pub fn i(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("i", attrs, children)
+}
+
+///
+pub fn kbd(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("kbd", attrs, children)
+}
+
+///
+pub fn mark(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("mark", attrs, children)
+}
+
+///
+pub fn q(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("q", attrs, children)
+}
+
+///
+pub fn rp(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("rp", attrs, children)
+}
+
+///
+pub fn rt(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("rt", attrs, children)
+}
+
+///
+pub fn ruby(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("ruby", attrs, children)
+}
+
+///
+pub fn s(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("s", attrs, children)
+}
+
+///
+pub fn samp(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("samp", attrs, children)
+}
+
+///
+pub fn small(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("small", attrs, children)
+}
+
+///
+pub fn span(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("span", attrs, children)
+}
+
+///
+pub fn strong(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("strong", attrs, children)
+}
+
+///
+pub fn sub(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("sub", attrs, children)
+}
+
+///
+pub fn sup(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("sup", attrs, children)
+}
+
+///
+pub fn time(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("time", attrs, children)
+}
+
+///
+pub fn u(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("u", attrs, children)
+}
+
+///
+pub fn var(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("var", attrs, children)
+}
+
+///
+pub fn wbr(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("wbr", attrs, [])
+}
+
+// HTML ELEMENTS: IMAGE AND MULTIMEDIA -----------------------------------------
+
+///
+pub fn area(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("area", attrs, [])
+}
+
+///
+pub fn audio(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("audio", attrs, children)
+}
+
+///
+pub fn img(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("img", attrs, [])
+}
+
+/// Used with <area> elements to define an image map (a clickable link area).
+///
+pub fn map(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("map", attrs, children)
+}
+
+///
+pub fn track(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("track", attrs, [])
+}
+
+///
+pub fn video(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("video", attrs, children)
+}
+
+// HTML ELEMENTS: EMBEDDED CONTENT ---------------------------------------------
+
+///
+pub fn embed(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("embed", attrs, [])
+}
+
+///
+pub fn iframe(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("iframe", attrs, [])
+}
+
+///
+pub fn object(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("object", attrs, [])
+}
+
+///
+pub fn picture(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("picture", attrs, children)
+}
+
+///
+pub fn portal(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("portal", attrs, [])
+}
+
+///
+pub fn source(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("source", attrs, [])
+}
+
+// HTML ELEMENTS: SVG AND MATHML -----------------------------------------------
+
+///
+pub fn svg(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced("http://www.w3.org/2000/svg", "svg", attrs, children)
+}
+
+///
+pub fn math(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("math", attrs, children)
+}
+
+// HTML ELEMENTS: SCRIPTING ----------------------------------------------------
+
+///
+pub fn canvas(attrs: List(Attribute(msg))) -> Element(msg) {
+ element("canvas", attrs, [])
+}
+
+///
+pub fn noscript(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element("noscript", attrs, children)
+}
+
+///
+pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg) {
+ element("script", attrs, [text(js)])
+}
+
+// HTML ELEMENTS: DEMARCATING EDITS ---------------------------------------------
+
+///
+pub fn del(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("del", attrs, children)
+}
+
+///
+pub fn ins(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("ins", attrs, children)
+}
+
+// HTML ELEMENTS: TABLE CONTENT ------------------------------------------------
+
+///
+pub fn caption(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("caption", attrs, children)
+}
+
+///
+pub fn col(attrs: List(Attribute(msg))) -> Element(msg) {
+ element.element("col", attrs, [])
+}
+
+///
+pub fn colgroup(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("colgroup", attrs, children)
+}
+
+///
+pub fn table(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("table", attrs, children)
+}
+
+///
+pub fn tbody(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("tbody", attrs, children)
+}
+
+///
+pub fn td(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("td", attrs, children)
+}
+
+///
+pub fn tfoot(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("tfoot", attrs, children)
+}
+
+///
+pub fn th(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("th", attrs, children)
+}
+
+///
+pub fn thead(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("thead", attrs, children)
+}
+
+///
+pub fn tr(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("tr", attrs, children)
+}
+
+// HTML ELEMENTS: FORMS --------------------------------------------------------
+
+///
+pub fn button(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("button", attrs, children)
+}
+
+///
+pub fn datalist(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("datalist", attrs, children)
+}
+
+///
+pub fn fieldset(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("fieldset", attrs, children)
+}
+
+///
+pub fn form(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("form", attrs, children)
+}
+
+///
+pub fn input(attrs: List(Attribute(msg))) -> Element(msg) {
+ element.element("input", attrs, [])
+}
+
+///
+pub fn label(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("label", attrs, children)
+}
+
+///
+pub fn legend(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("legend", attrs, children)
+}
+
+///
+pub fn meter(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("meter", attrs, children)
+}
+
+///
+pub fn optgroup(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("optgroup", attrs, children)
+}
+
+///
+pub fn option(attrs: List(Attribute(msg))) -> Element(msg) {
+ element.element("option", attrs, [])
+}
+
+///
+pub fn output(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("output", attrs, children)
+}
+
+///
+pub fn progress(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("progress", attrs, children)
+}
+
+///
+pub fn select(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("select", attrs, children)
+}
+
+///
+pub fn textarea(attrs: List(Attribute(msg))) -> Element(msg) {
+ element.element("textarea", attrs, [])
+}
+
+// HTML ELEMENTS: INTERACTIVE ELEMENTS -----------------------------------------
+
+///
+pub fn details(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("details", attrs, children)
+}
+
+///
+pub fn dialog(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("dialog", attrs, children)
+}
+
+///
+pub fn summary(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("summary", attrs, children)
+}
+
+// HTML ELEMENTS: WEB COMPONENTS -----------------------------------------------
+
+///
+pub fn slot(attrs: List(Attribute(msg))) -> Element(msg) {
+ element.element("slot", attrs, [])
+}
+
+///
+pub fn template(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ element.element("template", attrs, children)
+}
diff --git a/src/lustre/element/svg.gleam b/src/lustre/element/svg.gleam
new file mode 100644
index 0000000..a1fc0ce
--- /dev/null
+++ b/src/lustre/element/svg.gleam
@@ -0,0 +1,416 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre/element/svg](https://pkg.hayleigh.dev/lustre/api/lustre/element/svg)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import lustre/element.{Element, namespaced, text as inline_text}
+import lustre/attribute.{Attribute}
+
+// CONSTANTS -------------------------------------------------------------------
+
+const namespace = "http://www.w3.org/2000/svg"
+
+// The doc comments (and order) for functions in this module are taken from the
+// MDN Element reference:
+//
+// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
+//
+
+// SVG ELEMENTS: ANIMATION ELEMENTS --------------------------------------------
+
+///
+pub fn animate(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "animate", attrs, [])
+}
+
+///
+pub fn animate_motion(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "animateMotion", attrs, [])
+}
+
+///
+pub fn animate_transform(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "animateTransform", attrs, [])
+}
+
+///
+pub fn mpath(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "mpath", attrs, [])
+}
+
+///
+pub fn set(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "set", attrs, [])
+}
+
+// SVG ELEMENTS: BASIC SHAPES --------------------------------------------------
+
+///
+pub fn circle(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "circle", attrs, [])
+}
+
+///
+pub fn ellipse(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "ellipse", attrs, [])
+}
+
+///
+pub fn line(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "line", attrs, [])
+}
+
+///
+pub fn polygon(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "polygon", attrs, [])
+}
+
+///
+pub fn polyline(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "polyline", attrs, [])
+}
+
+///
+pub fn rect(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "rect", attrs, [])
+}
+
+// SVG ELEMENTS: CONTAINER ELEMENTS --------------------------------------------
+
+///
+pub fn a(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "a", attrs, children)
+}
+
+///
+pub fn defs(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "defs", attrs, children)
+}
+
+///
+pub fn g(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "g", attrs, children)
+}
+
+///
+pub fn marker(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "marker", attrs, children)
+}
+
+///
+pub fn mask(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "mask", attrs, children)
+}
+
+///
+pub fn missing_glyph(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "missing-glyph", attrs, children)
+}
+
+///
+pub fn pattern(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "pattern", attrs, children)
+}
+
+///
+pub fn svg(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "svg", attrs, children)
+}
+
+///
+pub fn switch(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "switch", attrs, children)
+}
+
+///
+pub fn symbol(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "symbol", attrs, children)
+}
+
+// SVG ELEMENTS: DESCRIPTIVE ELEMENTS ------------------------------------------
+
+///
+pub fn desc(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "desc", attrs, children)
+}
+
+///
+pub fn metadata(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "metadata", attrs, children)
+}
+
+///
+pub fn title(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "title", attrs, children)
+}
+
+// SVG ELEMENTS: FILTER EFFECTS ------------------------------------------------
+
+///
+pub fn fe_blend(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feBlend", attrs, [])
+}
+
+///
+pub fn fe_color_matrix(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feColorMatrix", attrs, [])
+}
+
+///
+pub fn fe_component_transfer(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feComponentTransfer", attrs, [])
+}
+
+///
+pub fn fe_composite(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feComposite", attrs, [])
+}
+
+///
+pub fn fe_convolve_matrix(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feConvolveMatrix", attrs, [])
+}
+
+///
+pub fn fe_diffuse_lighting(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "feDiffuseLighting", attrs, children)
+}
+
+///
+pub fn fe_displacement_map(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feDisplacementMap", attrs, [])
+}
+
+///
+pub fn fe_drop_shadow(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feDropShadow", attrs, [])
+}
+
+///
+pub fn fe_flood(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feFlood", attrs, [])
+}
+
+///
+pub fn fe_func_a(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feFuncA", attrs, [])
+}
+
+///
+pub fn fe_func_b(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feFuncB", attrs, [])
+}
+
+///
+pub fn fe_func_g(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feFuncG", attrs, [])
+}
+
+///
+pub fn fe_func_r(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feFuncR", attrs, [])
+}
+
+///
+pub fn fe_gaussian_blur(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feGaussianBlur", attrs, [])
+}
+
+///
+pub fn fe_image(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feImage", attrs, [])
+}
+
+///
+pub fn fe_merge(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "feMerge", attrs, children)
+}
+
+///
+pub fn fe_merge_node(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feMergeNode", attrs, [])
+}
+
+///
+pub fn fe_morphology(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feMorphology", attrs, [])
+}
+
+///
+pub fn fe_offset(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feOffset", attrs, [])
+}
+
+///
+pub fn fe_specular_lighting(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "feSpecularLighting", attrs, children)
+}
+
+///
+pub fn fe_tile(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "feTile", attrs, children)
+}
+
+///
+pub fn fe_turbulence(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feTurbulence", attrs, [])
+}
+
+// SVG ELEMENTS: GRADIENT ELEMENTS ---------------------------------------------
+
+///
+pub fn linear_gradient(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "linearGradient", attrs, children)
+}
+
+///
+pub fn radial_gradient(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "radialGradient", attrs, children)
+}
+
+///
+pub fn stop(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "stop", attrs, [])
+}
+
+// SVG ELEMENTS: GRAPHICAL ELEMENTS --------------------------------------------
+
+///
+pub fn image(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "image", attrs, [])
+}
+
+///
+pub fn path(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "path", attrs, [])
+}
+
+///
+pub fn text(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "text", attrs, [])
+}
+
+///
+pub fn use_(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "use", attrs, [])
+}
+
+// SVG ELEMENTS: LIGHTING ELEMENTS ---------------------------------------------
+
+///
+pub fn fe_distant_light(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feDistantLight", attrs, [])
+}
+
+///
+pub fn fe_point_light(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "fePointLight", attrs, [])
+}
+
+///
+pub fn fe_spot_light(attrs: List(Attribute(msg))) -> Element(msg) {
+ namespaced(namespace, "feSpotLight", attrs, [])
+}
+
+// SVG ELEMENTS: NEVER-RENDERED ELEMENTS ---------------------------------------
+
+///
+pub fn clip_path(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "clipPath", attrs, children)
+}
+
+///
+pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg) {
+ namespaced(namespace, "script", attrs, [inline_text(js)])
+}
+
+///
+pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg) {
+ namespaced(namespace, "style", attrs, [inline_text(css)])
+}
+
+// SVG ELEMENTS: RENDERABLE ELEMENTS -------------------------------------------
+
+///
+pub fn foreign_object(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "foreignObject", attrs, children)
+}
+
+///
+pub fn text_path(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "textPath", attrs, children)
+}
+
+///
+pub fn tspan(
+ attrs: List(Attribute(msg)),
+ children: List(Element(msg)),
+) -> Element(msg) {
+ namespaced(namespace, "tspan", attrs, children)
+}
diff --git a/src/lustre/event.gleam b/src/lustre/event.gleam
new file mode 100644
index 0000000..7913ef5
--- /dev/null
+++ b/src/lustre/event.gleam
@@ -0,0 +1,178 @@
+//// To read the full documentation for this module, please visit
+//// [https://pkg.hayleigh.dev/lustre/api/lustre/event](https://pkg.hayleigh.dev/lustre/api/lustre/event)
+
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/dynamic.{DecodeError, Dynamic}
+import gleam/result
+import lustre/attribute.{Attribute}
+import lustre/effect.{Effect}
+
+// TYPES -----------------------------------------------------------------------
+
+type Decoded(a) =
+ Result(a, List(DecodeError))
+
+// EFFECTS ---------------------------------------------------------------------
+
+@target(javascript)
+///
+@external(javascript, "../lustre.ffi.mjs", "emit")
+pub fn emit(event: String, data: any) -> Effect(msg)
+
+// CUSTOM EVENTS ---------------------------------------------------------------
+
+///
+pub fn on(
+ name: String,
+ handler: fn(Dynamic) -> Result(msg, error),
+) -> Attribute(msg) {
+ attribute.on(name, handler)
+}
+
+// MOUSE EVENTS ----------------------------------------------------------------
+
+///
+pub fn on_click(msg: msg) -> Attribute(msg) {
+ use _ <- on("click")
+ Ok(msg)
+}
+
+///
+pub fn on_mouse_down(msg: msg) -> Attribute(msg) {
+ use _ <- on("mousedown")
+ Ok(msg)
+}
+
+///
+pub fn on_mouse_up(msg: msg) -> Attribute(msg) {
+ use _ <- on("mouseup")
+ Ok(msg)
+}
+
+///
+pub fn on_mouse_enter(msg: msg) -> Attribute(msg) {
+ use _ <- on("mouseenter")
+ Ok(msg)
+}
+
+///
+pub fn on_mouse_leave(msg: msg) -> Attribute(msg) {
+ use _ <- on("mouseleave")
+ Ok(msg)
+}
+
+///
+pub fn on_mouse_over(msg: msg) -> Attribute(msg) {
+ use _ <- on("mouseover")
+ Ok(msg)
+}
+
+///
+pub fn on_mouse_out(msg: msg) -> Attribute(msg) {
+ use _ <- on("mouseout")
+ Ok(msg)
+}
+
+// KEYBOARD EVENTS -------------------------------------------------------------
+
+/// Listens for key presses on an element, and dispatches a message with the
+/// current key being pressed.
+///
+pub fn on_keypress(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event <- on("keypress")
+
+ event
+ |> dynamic.field("key", dynamic.string)
+ |> result.map(msg)
+}
+
+/// Listens for key dow events on an element, and dispatches a message with the
+/// current key being pressed.
+///
+pub fn on_keydown(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event <- on("keydown")
+
+ event
+ |> dynamic.field("key", dynamic.string)
+ |> result.map(msg)
+}
+
+/// Listens for key up events on an element, and dispatches a message with the
+/// current key being released.
+///
+pub fn on_keyup(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event <- on("keyup")
+
+ event
+ |> dynamic.field("key", dynamic.string)
+ |> result.map(msg)
+}
+
+// FORM EVENTS -----------------------------------------------------------------
+
+///
+pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) {
+ use event <- on("input")
+
+ value(event)
+ |> result.map(msg)
+}
+
+pub fn on_check(msg: fn(Bool) -> msg) -> Attribute(msg) {
+ use event <- on("change")
+
+ checked(event)
+ |> result.map(msg)
+}
+
+pub fn on_submit(msg: msg) -> Attribute(msg) {
+ use event <- on("submit")
+ let _ = prevent_default(event)
+
+ Ok(msg)
+}
+
+// FOCUS EVENTS ----------------------------------------------------------------
+
+pub fn on_focus(msg: msg) -> Attribute(msg) {
+ use _ <- on("focus")
+ Ok(msg)
+}
+
+pub fn on_blur(msg: msg) -> Attribute(msg) {
+ use _ <- on("blur")
+ Ok(msg)
+}
+
+// DECODERS --------------------------------------------------------------------
+
+///
+pub fn value(event: Dynamic) -> Decoded(String) {
+ event
+ |> dynamic.field("target", dynamic.field("value", dynamic.string))
+}
+
+///
+pub fn checked(event: Dynamic) -> Decoded(Bool) {
+ event
+ |> dynamic.field("target", dynamic.field("checked", dynamic.bool))
+}
+
+///
+pub fn mouse_position(event: Dynamic) -> Decoded(#(Float, Float)) {
+ use x <- result.then(dynamic.field("clientX", dynamic.float)(event))
+ use y <- result.then(dynamic.field("clientY", dynamic.float)(event))
+
+ Ok(#(x, y))
+}
+
+// UTILS -----------------------------------------------------------------------
+
+@target(javascript)
+@external(javascript, "../lustre.ffi.mjs", "prevent_default")
+pub fn prevent_default(event: Dynamic) -> Nil
+
+@target(javascript)
+@external(javascript, "../lustre.ffi.mjs", "stop_propagation")
+pub fn stop_propagation(event: Dynamic) -> Nil
diff --git a/src/runtime.ffi.mjs b/src/runtime.ffi.mjs
new file mode 100644
index 0000000..dd02f8b
--- /dev/null
+++ b/src/runtime.ffi.mjs
@@ -0,0 +1,239 @@
+import { Empty } from "./gleam.mjs";
+import { map as result_map } from "../gleam_stdlib/gleam/result.mjs";
+
+export function morph(prev, curr, dispatch, parent) {
+ if (curr[3]) {
+ return prev?.nodeType === 1 &&
+ prev.nodeName === curr[0].toUpperCase() &&
+ prev.namespaceURI === curr[3]
+ ? morphElement(prev, curr, curr[3], dispatch, parent)
+ : createElement(prev, curr, curr[3], dispatch, parent);
+ }
+
+ if (curr[2]) {
+ return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase()
+ ? morphElement(prev, curr, null, dispatch, parent)
+ : createElement(prev, curr, null, dispatch, parent);
+ }
+
+ if (curr[0] && typeof curr[0] === "string") {
+ return prev?.nodeType === 3
+ ? morphText(prev, curr)
+ : createText(prev, curr);
+ }
+
+ return document.createComment(
+ [
+ "[internal lustre error] I couldn't work out how to render this element. This",
+ "function should only be called internally by lustre's runtime: if you think",
+ "this is an error, please open an issue at",
+ "https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new",
+ ].join(" ")
+ );
+}
+
+// ELEMENTS --------------------------------------------------------------------
+
+function createElement(prev, curr, ns, dispatch, parent = null) {
+ const el = ns
+ ? document.createElementNS(ns, curr[0])
+ : document.createElement(curr[0]);
+
+ el.$lustre = {};
+
+ let attr = curr[1];
+ while (attr.head) {
+ morphAttr(
+ el,
+ attr.head[0],
+ attr.head[0] === "class" && el.className
+ ? `${el.className} ${attr.head[1]}`
+ : attr.head[1],
+ dispatch
+ );
+
+ attr = attr.tail;
+ }
+
+ if (customElements.get(curr[0])) {
+ el._slot = curr[2];
+ } else if (curr[0] === "slot") {
+ let child = new Empty();
+ let parentWithSlot = parent;
+
+ while (parentWithSlot) {
+ if (parentWithSlot._slot) {
+ child = parentWithSlot._slot;
+ break;
+ } else {
+ parentWithSlot = parentWithSlot.parentNode;
+ }
+ }
+
+ while (child.head) {
+ el.appendChild(morph(null, child.head, dispatch, el));
+ child = child.tail;
+ }
+ } else {
+ let child = curr[2];
+ while (child.head) {
+ el.appendChild(morph(null, child.head, dispatch, el));
+ child = child.tail;
+ }
+
+ if (prev) prev.replaceWith(el);
+ }
+
+ return el;
+}
+
+function morphElement(prev, curr, ns, dispatch, parent) {
+ const prevAttrs = prev.attributes;
+ const currAttrs = new Map();
+
+ let currAttr = curr[1];
+ while (currAttr.head) {
+ currAttrs.set(
+ currAttr.head[0],
+ currAttr.head[0] === "class" && currAttrs.has("class")
+ ? `${currAttrs.get("class")} ${currAttr.head[1]}`
+ : currAttr.head[1]
+ );
+
+ currAttr = currAttr.tail;
+ }
+
+ for (const { name, value: prevValue } of prevAttrs) {
+ if (!currAttrs.has(name)) {
+ prev.removeAttribute(name);
+ } else {
+ const value = currAttrs.get(name);
+
+ if (value !== prevValue) {
+ morphAttr(prev, name, value, dispatch);
+ currAttrs.delete(name);
+ }
+ }
+ }
+
+ for (const [name, value] of currAttrs) {
+ morphAttr(prev, name, value, dispatch);
+ }
+
+ if (customElements.get(curr[0])) {
+ prev._slot = curr[2];
+ } else if (curr[0] === "slot") {
+ let prevChild = prev.firstChild;
+ let currChild = new Empty();
+ let parentWithSlot = parent;
+
+ while (parentWithSlot) {
+ if (parentWithSlot._slot) {
+ currChild = parentWithSlot._slot;
+ break;
+ } else {
+ parentWithSlot = parentWithSlot.parentNode;
+ }
+ }
+
+ while (prevChild) {
+ if (currChild.head) {
+ morph(prevChild, currChild.head, dispatch, prev);
+ currChild = currChild.tail;
+ }
+
+ prevChild = prevChild.nextSibling;
+ }
+
+ while (currChild.head) {
+ prev.appendChild(morph(null, currChild.head, dispatch, prev));
+ currChild = currChild.tail;
+ }
+ } else {
+ let prevChild = prev.firstChild;
+ let currChild = curr[2];
+
+ while (prevChild) {
+ if (currChild.head) {
+ const next = prevChild.nextSibling;
+ morph(prevChild, currChild.head, dispatch, prev);
+ currChild = currChild.tail;
+ prevChild = next;
+ } else {
+ const next = prevChild.nextSibling;
+ prevChild.remove();
+ prevChild = next;
+ }
+ }
+
+ while (currChild.head) {
+ prev.appendChild(morph(null, currChild.head, dispatch, prev));
+ currChild = currChild.tail;
+ }
+ }
+
+ return prev;
+}
+
+// ATTRIBUTES ------------------------------------------------------------------
+
+function morphAttr(el, name, value, dispatch) {
+ switch (typeof value) {
+ case "string":
+ if (el.getAttribute(name) !== value) el.setAttribute(name, value);
+ if (value === "") el.removeAttribute(name);
+ if (name === "value" && el.value !== value) el.value = value;
+ break;
+
+ // Event listeners need to be handled slightly differently because we need
+ // to be able to support custom events. We
+ case name.startsWith("on") && "function": {
+ if (el.$lustre[name] === value) break;
+
+ const event = name.slice(2).toLowerCase();
+ const handler = (e) => result_map(value(e), dispatch);
+
+ if (el.$lustre[`${name}Handler`]) {
+ el.removeEventListener(event, el.$lustre[`${name}Handler`]);
+ }
+
+ el.addEventListener(event, handler);
+
+ el.$lustre[name] = value;
+ el.$lustre[`${name}Handler`] = handler;
+
+ break;
+ }
+
+ default:
+ el[name] = value;
+ }
+}
+
+// TEXT ------------------------------------------------------------------------
+
+function createText(prev, curr) {
+ if (!curr[0]) {
+ prev?.remove();
+ return null;
+ }
+
+ const el = document.createTextNode(curr[0]);
+
+ if (prev) prev.replaceWith(el);
+ return el;
+}
+
+function morphText(prev, curr) {
+ const prevValue = prev.nodeValue;
+ const currValue = curr[0];
+
+ if (!currValue) {
+ prev?.remove();
+ return null;
+ }
+
+ if (prevValue !== currValue) prev.nodeValue = currValue;
+
+ return prev;
+}