Packages
lustre
5.5.1
5.7.1
5.7.0
5.6.0
5.5.2
5.5.1
5.5.0
5.4.0
5.3.5
5.3.4
5.3.3
5.3.2
5.3.1
5.3.0
5.2.1
5.2.0
5.1.1
5.1.0
5.0.3
5.0.2
5.0.1
5.0.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.0
4.0.0-rc1
4.0.0-rc.2
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.0.1
2.0.0
1.3.0
1.2.0
1.1.0
1.0.0
Create HTML templates, single page applications, Web Components, and real-time server components in Gleam!
Current section
Files
Jump to
Current section
Files
src/lustre/vdom/vnode.gleam
// IMPORTS ---------------------------------------------------------------------
import gleam/dynamic.{type Dynamic}
import gleam/json.{type Json}
import gleam/list
import gleam/string
import gleam/string_tree.{type StringTree}
import houdini
import lustre/internals/json_object_builder
import lustre/internals/mutable_map.{type MutableMap}
import lustre/internals/ref.{type Ref}
import lustre/vdom/vattr.{type Attribute}
// TYPES -----------------------------------------------------------------------
pub type Element(msg) {
Fragment(
kind: Int,
key: String,
children: List(Element(msg)),
keyed_children: MutableMap(String, Element(msg)),
)
Element(
kind: Int,
key: String,
namespace: String,
tag: String,
// To efficiently compare attributes during the diff, attribute are always
// stored sorted. We do this while constructing the tree to not have to sort
// the attribute in the previous tree again. The order does not matter, as
// long as the new and old tree agree on the same order relation.
//
// When constructing a Node with attributes provided by a user, attributes
// have to be sorted with the `attribute.prepare` function.
//
attributes: List(Attribute(msg)),
children: List(Element(msg)),
keyed_children: MutableMap(String, Element(msg)),
// These two properties are only useful when rendering Elements to strings.
// Certain HTML tags like <img> and <input> are called "void" elements,
// which means they cannot have children and should not have a closing tag.
// On the other hand, XML and SVG documents support self-closing tags like
// <path /> and can *not* be void...
//
self_closing: Bool,
void: Bool,
)
Text(kind: Int, key: String, content: String)
UnsafeInnerHtml(
kind: Int,
key: String,
namespace: String,
tag: String,
//
attributes: List(Attribute(msg)),
inner_html: String,
)
Map(
kind: Int,
key: String,
mapper: fn(Dynamic) -> Dynamic,
child: Element(msg),
)
Memo(kind: Int, key: String, dependencies: List(Ref), view: View(msg))
}
pub type Memos(msg) =
mutable_map.MutableMap(View(msg), Element(msg))
pub type View(msg) =
fn() -> Element(msg)
// CONSTRUCTORS ----------------------------------------------------------------
pub const fragment_kind: Int = 0
pub fn fragment(
key key: String,
children children: List(Element(msg)),
keyed_children keyed_children: MutableMap(String, Element(msg)),
) -> Element(msg) {
Fragment(kind: fragment_kind, key:, children:, keyed_children:)
}
pub const element_kind: Int = 1
pub fn element(
key key: String,
namespace namespace: String,
tag tag: String,
attributes attributes: List(Attribute(msg)),
children children: List(Element(msg)),
keyed_children keyed_children: MutableMap(String, Element(msg)),
self_closing self_closing: Bool,
void void: Bool,
) -> Element(msg) {
Element(
kind: element_kind,
key:,
namespace:,
tag:,
attributes: vattr.prepare(attributes),
children:,
keyed_children:,
self_closing:,
void:,
)
}
pub fn is_void_html_element(tag: String, namespace: String) -> Bool {
case namespace {
"" ->
case tag {
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr" -> True
_ -> False
}
_ -> False
}
}
pub const text_kind: Int = 2
pub fn text(key key: String, content content: String) -> Element(msg) {
Text(kind: text_kind, key: key, content: content)
}
pub const unsafe_inner_html_kind: Int = 3
pub fn unsafe_inner_html(
key key: String,
namespace namespace: String,
tag tag: String,
attributes attributes: List(Attribute(msg)),
inner_html inner_html: String,
) -> Element(msg) {
UnsafeInnerHtml(
kind: unsafe_inner_html_kind,
key:,
namespace:,
tag:,
attributes: vattr.prepare(attributes),
inner_html:,
)
}
pub const map_kind: Int = 4
pub fn map(element: Element(a), mapper: fn(a) -> b) -> Element(b) {
case element {
Map(mapper: child_mapper, ..) ->
Map(
kind: map_kind,
key: element.key,
child: coerce(element.child),
mapper: fn(handler) { coerce(mapper)(child_mapper(handler)) },
)
_ ->
Map(
kind: map_kind,
key: element.key,
mapper: coerce(mapper),
child: coerce(element),
)
}
}
pub const memo_kind: Int = 5
pub fn memo(
key key: String,
dependencies dependencies: List(Ref),
view view: fn() -> Element(msg),
) -> Element(msg) {
Memo(kind: memo_kind, key:, dependencies:, view:)
}
@external(erlang, "gleam@function", "identity")
@external(javascript, "../../../gleam_stdlib/gleam/function.mjs", "identity")
fn coerce(a: a) -> b
// MANIPULATION ----------------------------------------------------------------
pub fn to_keyed(key: String, node: Element(msg)) -> Element(msg) {
case node {
Element(..) -> Element(..node, key:)
Text(..) -> Text(..node, key:)
UnsafeInnerHtml(..) -> UnsafeInnerHtml(..node, key:)
Fragment(..) -> Fragment(..node, key:)
// because we skip Memo nodes when encoding and reconciling, we have
// to set the key on the memo (for the diff) as well as the inner node!
Memo(view:, ..) -> Memo(..node, key:, view: fn() { to_keyed(key, view()) })
// we don't skip Map nodes but I feel safer doing it anyways :-)
Map(child:, ..) -> Map(..node, key:, child: to_keyed(key, child))
}
}
// ENCODERS --------------------------------------------------------------------
pub fn to_json(node: Element(msg), memos: Memos(msg)) -> Json {
case node {
Fragment(kind:, key:, children:, ..) ->
fragment_to_json(kind, key, children, memos)
Element(kind:, key:, namespace:, tag:, attributes:, children:, ..) ->
element_to_json(kind, key, namespace, tag, attributes, children, memos)
Text(kind:, key:, content:) -> text_to_json(kind, key, content)
UnsafeInnerHtml(kind:, key:, namespace:, tag:, attributes:, inner_html:) ->
unsafe_inner_html_to_json(
kind,
key,
namespace,
tag,
attributes,
inner_html,
)
Map(kind:, key:, child:, ..) -> map_to_json(kind, key, child, memos)
Memo(view:, ..) -> memo_to_json(view, memos)
}
}
fn fragment_to_json(kind, key, children, memos) {
json_object_builder.tagged(kind)
|> json_object_builder.string("key", key)
|> json_object_builder.list("children", children, to_json(_, memos))
|> json_object_builder.build
}
fn element_to_json(kind, key, namespace, tag, attributes, children, memos) {
json_object_builder.tagged(kind)
|> json_object_builder.string("key", key)
|> json_object_builder.string("namespace", namespace)
|> json_object_builder.string("tag", tag)
|> json_object_builder.list("attributes", attributes, vattr.to_json)
|> json_object_builder.list("children", children, to_json(_, memos))
|> json_object_builder.build
}
fn text_to_json(kind, key, content) {
json_object_builder.tagged(kind)
|> json_object_builder.string("key", key)
|> json_object_builder.string("content", content)
|> json_object_builder.build
}
fn unsafe_inner_html_to_json(kind, key, namespace, tag, attributes, inner_html) {
json_object_builder.tagged(kind)
|> json_object_builder.string("key", key)
|> json_object_builder.string("namespace", namespace)
|> json_object_builder.string("tag", tag)
|> json_object_builder.list("attributes", attributes, vattr.to_json)
|> json_object_builder.string("inner_html", inner_html)
|> json_object_builder.build
}
fn memo_to_json(view, memos) {
// Memo nodes are transparent during encoding - we encode their cached child.
let child = mutable_map.get_or_compute(memos, view, view)
to_json(child, memos)
}
// Map nodes are encoded with their child.
fn map_to_json(kind, key, child, memos) {
// They mark the boundary of an isolated event subtree, so we need to tell the runtime
// about them to make sure it can construct the correct event paths!
json_object_builder.tagged(kind)
|> json_object_builder.string("key", key)
|> json_object_builder.json("child", to_json(child, memos))
|> json_object_builder.build
}
// STRING RENDERING ------------------------------------------------------------
pub fn to_string(node: Element(msg)) -> String {
node
|> to_string_tree("")
|> string_tree.to_string
}
pub fn to_string_tree(
node: Element(msg),
parent_namespace: String,
) -> StringTree {
case node {
Text(content: "", ..) -> string_tree.new()
Text(content:, ..) -> string_tree.from_string(houdini.escape(content))
Element(key:, namespace:, tag:, attributes:, self_closing:, ..)
if self_closing
-> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append("/>")
}
Element(key:, namespace:, tag:, attributes:, void:, ..) if void -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
}
Element(key:, namespace:, tag:, attributes:, children:, ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> children_to_string_tree(children, namespace)
|> string_tree.append("</" <> tag <> ">")
}
UnsafeInnerHtml(key:, namespace:, tag:, attributes:, inner_html:, ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> string_tree.append(inner_html)
|> string_tree.append("</" <> tag <> ">")
}
Fragment(key:, children:, ..) -> {
marker_comment("lustre:fragment", key)
|> children_to_string_tree(children, parent_namespace)
|> string_tree.append_tree(marker_comment("/lustre:fragment", ""))
}
Map(key:, child:, ..) -> {
marker_comment("lustre:map", key)
|> string_tree.append_tree(to_string_tree(child, parent_namespace))
}
Memo(key:, view:, ..) -> {
marker_comment("lustre:memo", key)
|> string_tree.append_tree(to_string_tree(view(), parent_namespace))
}
}
}
fn children_to_string_tree(
html: StringTree,
children: List(Element(msg)),
namespace: String,
) -> StringTree {
use html, child <- list.fold(children, html)
string_tree.append_tree(html, to_string_tree(child, namespace))
}
pub fn to_snapshot(node: Element(msg), debug: Bool) -> String {
do_to_snapshot_builder(node:, raw: False, debug:, namespace: "", indent: 0)
|> string_tree.to_string
}
fn do_to_snapshot_builder(
node node: Element(msg),
raw raw: Bool,
debug debug: Bool,
namespace parent_namespace: String,
indent indent: Int,
) -> StringTree {
let spaces = string.repeat(" ", indent)
case node {
Text(content: "", ..) -> string_tree.new()
Text(content:, ..) if raw -> string_tree.from_strings([spaces, content])
Text(content:, ..) ->
string_tree.from_strings([spaces, houdini.escape(content)])
Element(key:, namespace:, tag:, attributes:, self_closing: True, ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.prepend(spaces)
|> string_tree.append_tree(attributes)
|> string_tree.append("/>")
}
Element(key:, namespace:, tag:, attributes:, void: True, ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.prepend(spaces)
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
}
Element(key:, namespace:, tag:, attributes:, children: [], ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.prepend(spaces)
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> string_tree.append("</" <> tag <> ">")
}
Element(key:, namespace:, tag:, attributes:, children:, ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.prepend(spaces)
|> string_tree.append_tree(attributes)
|> string_tree.append(">\n")
|> children_to_snapshot_builder(
children:,
raw:,
debug:,
namespace:,
indent: indent + 1,
)
|> string_tree.append(spaces)
|> string_tree.append("</" <> tag <> ">")
}
UnsafeInnerHtml(key:, namespace:, tag:, attributes:, inner_html:, ..) -> {
let html = string_tree.from_string("<" <> tag)
let attributes =
vattr.to_string_tree(key, namespace, parent_namespace, attributes)
html
|> string_tree.prepend(spaces)
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> string_tree.append(inner_html)
|> string_tree.append("</" <> tag <> ">")
}
Fragment(key:, children:, ..) if debug -> {
marker_comment("lustre:fragment", key)
|> string_tree.prepend(spaces)
|> string_tree.append("\n")
|> children_to_snapshot_builder(
children:,
raw:,
debug:,
namespace: parent_namespace,
indent: indent + 1,
)
|> string_tree.append(spaces)
|> string_tree.append_tree(marker_comment("/lustre:fragment", ""))
}
Fragment(children:, ..) ->
children_to_snapshot_builder(
html: string_tree.new(),
children: children,
raw: raw,
debug: debug,
namespace: parent_namespace,
indent: indent,
)
Map(key:, child:, ..) if debug -> {
marker_comment("lustre:map", key)
|> string_tree.prepend(spaces)
|> string_tree.append("\n")
|> string_tree.append_tree(do_to_snapshot_builder(
node: child,
raw:,
debug:,
namespace: parent_namespace,
indent: indent + 1,
))
}
Map(child:, ..) ->
do_to_snapshot_builder(
node: child,
raw:,
debug:,
namespace: parent_namespace,
indent:,
)
Memo(key:, view:, ..) if debug -> {
marker_comment("lustre:memo", key)
|> string_tree.prepend(spaces)
|> string_tree.append("\n")
|> string_tree.append_tree(do_to_snapshot_builder(
node: view(),
raw:,
debug:,
namespace: parent_namespace,
indent: indent + 1,
))
}
Memo(view:, ..) ->
do_to_snapshot_builder(
node: view(),
raw:,
debug:,
namespace: parent_namespace,
indent:,
)
}
}
fn children_to_snapshot_builder(
html html: StringTree,
children children: List(Element(msg)),
raw raw: Bool,
debug debug: Bool,
namespace namespace: String,
indent indent: Int,
) -> StringTree {
case children {
[Text(content: a, ..), Text(content: b, ..), ..rest] ->
children_to_snapshot_builder(
html:,
children: [Text(kind: text_kind, key: "", content: a <> b), ..rest],
raw:,
debug:,
namespace:,
indent:,
)
[child, ..rest] ->
child
|> do_to_snapshot_builder(raw:, debug:, namespace:, indent:)
|> string_tree.append("\n")
|> string_tree.prepend_tree(html)
|> children_to_snapshot_builder(
children: rest,
raw:,
debug:,
namespace:,
indent:,
)
[] -> html
}
}
fn marker_comment(label: String, key: String) {
case key {
"" -> string_tree.from_string("<!-- " <> label <> " -->")
_ ->
string_tree.from_string("<!-- " <> label <> " key=\"")
|> string_tree.append(houdini.escape(key))
|> string_tree.append("\" -->")
}
}