Packages
lustre
5.3.3
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/function
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/vdom/vattr.{type Attribute}
// TYPES -----------------------------------------------------------------------
pub type Element(msg) {
Fragment(
kind: Int,
key: String,
mapper: fn(Dynamic) -> Dynamic,
children: List(Element(msg)),
keyed_children: MutableMap(String, Element(msg)),
)
Element(
kind: Int,
key: String,
mapper: fn(Dynamic) -> Dynamic,
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, mapper: fn(Dynamic) -> Dynamic, content: String)
UnsafeInnerHtml(
kind: Int,
key: String,
mapper: fn(Dynamic) -> Dynamic,
namespace: String,
tag: String,
//
attributes: List(Attribute(msg)),
inner_html: String,
)
}
// CONSTRUCTORS ----------------------------------------------------------------
pub const fragment_kind: Int = 0
pub fn fragment(
key key: String,
mapper mapper: fn(Dynamic) -> Dynamic,
children children: List(Element(msg)),
keyed_children keyed_children: MutableMap(String, Element(msg)),
) -> Element(msg) {
Fragment(kind: fragment_kind, key:, mapper:, children:, keyed_children:)
}
pub const element_kind: Int = 1
pub fn element(
key key: String,
mapper mapper: fn(Dynamic) -> Dynamic,
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:,
mapper:,
namespace:,
tag:,
attributes: vattr.prepare(attributes),
children:,
keyed_children:,
self_closing:,
void: void || is_void_element(tag, namespace),
)
}
fn is_void_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,
mapper mapper: fn(Dynamic) -> Dynamic,
content content: String,
) -> Element(msg) {
Text(kind: text_kind, key: key, mapper: mapper, content: content)
}
pub const unsafe_inner_html_kind: Int = 3
pub fn unsafe_inner_html(
key key: String,
mapper mapper: fn(Dynamic) -> Dynamic,
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:,
mapper:,
namespace:,
tag:,
attributes: vattr.prepare(attributes),
inner_html:,
)
}
// 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:)
}
}
// ENCODERS --------------------------------------------------------------------
pub fn to_json(node: Element(msg)) -> Json {
case node {
Fragment(kind:, key:, children:, ..) ->
fragment_to_json(kind, key, children)
Element(kind:, key:, namespace:, tag:, attributes:, children:, ..) ->
element_to_json(kind, key, namespace, tag, attributes, children)
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,
)
}
}
fn fragment_to_json(kind, key, children) {
json_object_builder.tagged(kind)
|> json_object_builder.string("key", key)
|> json_object_builder.list("children", children, to_json)
|> json_object_builder.build
}
fn element_to_json(kind, key, namespace, tag, attributes, children) {
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)
|> 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
}
// 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)) -> StringTree {
case node {
Text(content: "", ..) -> string_tree.new()
Text(content:, ..) -> string_tree.from_string(houdini.escape(content))
Fragment(children:, ..) ->
children_to_string_tree(string_tree.new(), children)
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, 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, 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, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> children_to_string_tree(children)
|> 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, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> string_tree.append(inner_html)
|> string_tree.append("</" <> tag <> ">")
}
}
}
fn children_to_string_tree(
html: StringTree,
children: List(Element(msg)),
) -> StringTree {
use html, child <- list.fold(children, html)
child
|> to_string_tree
|> string_tree.append_tree(html, _)
}
pub fn to_snapshot(node: Element(msg)) -> String {
node
|> do_to_snapshot_builder(False, 0)
|> string_tree.to_string
}
fn do_to_snapshot_builder(
node: Element(msg),
raw_text: Bool,
indent: Int,
) -> StringTree {
let spaces = string.repeat(" ", indent)
case node {
Text(content: "", ..) -> string_tree.new()
Text(content:, ..) if raw_text ->
string_tree.from_strings([spaces, content])
Text(content:, ..) ->
string_tree.from_strings([spaces, houdini.escape(content)])
Fragment(children: [], ..) -> string_tree.new()
Fragment(children:, ..) ->
string_tree.new()
|> children_to_snapshot_builder(children, raw_text, indent)
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, attributes)
html
|> string_tree.prepend(spaces)
|> 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, 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, 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, attributes)
html
|> string_tree.prepend(spaces)
|> string_tree.append_tree(attributes)
|> string_tree.append(">\n")
|> children_to_snapshot_builder(children, raw_text, 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, attributes)
html
|> string_tree.append_tree(attributes)
|> string_tree.append(">")
|> string_tree.append(inner_html)
|> string_tree.append("</" <> tag <> ">")
}
}
}
fn children_to_snapshot_builder(
html: StringTree,
children: List(Element(msg)),
raw_text: Bool,
indent: Int,
) -> StringTree {
case children {
[Text(content: a, ..), Text(content: b, ..), ..rest] ->
children_to_snapshot_builder(
html,
[
Text(
kind: text_kind,
key: "",
mapper: function.identity,
content: a <> b,
),
..rest
],
raw_text,
indent,
)
[Fragment(..) as child, ..rest] ->
child
|> do_to_snapshot_builder(raw_text, indent)
|> string_tree.append_tree(html, _)
|> children_to_snapshot_builder(rest, raw_text, indent)
[child, ..rest] ->
child
|> do_to_snapshot_builder(raw_text, indent)
|> string_tree.append("\n")
|> string_tree.append_tree(html, _)
|> children_to_snapshot_builder(rest, raw_text, indent)
[] -> html
}
}