///|
#cfg(target="js")
fn first_child(node : @dom.Node) -> @dom.Node? {
if node.get_child_count() == 0 {
None
} else {
Some(node.get_first_child())
}
}
///|
/// Returns whether `node` can be claimed by `vnode` during hydration.
///
/// Text content and ordinary attributes are reconciled after claiming, so matching
/// normally uses only the node kind and rendered tag. The `id` attribute and
/// identity-bearing attributes of `meta`, `link`, and `script` are also checked to
/// avoid claiming the wrong same-tag sibling while skipping externally injected nodes.
#cfg(target="js")
fn node_matches(vnode : VNode, node : @dom.Node) -> Bool {
fn element_identity_matches(
element : @dom.Element,
tag : String,
props : Props,
) -> Bool {
fn attribute_matches(name) {
if props.attrs.get(name) is Some(expected) {
element.get_attribute(name) == Some(expected)
} else {
true
}
}
if element.get_node_name().to_lower() != rendered_tag(tag).to_lower() {
return false
}
if !attribute_matches("id") {
return false
}
match rendered_tag(tag) {
"meta" =>
attribute_matches("charset") &&
attribute_matches("name") &&
attribute_matches("property") &&
attribute_matches("http-equiv")
"link" => attribute_matches("rel") && attribute_matches("href")
"script" => attribute_matches("src") && attribute_matches("type")
_ => true
}
}
match vnode {
Text(_) => node.get_node_type() == 3
Elem(tag, props, _, ..) =>
if node.to_element() is Some(element) {
element_identity_matches(element, tag, props)
} else {
false
}
_ => false
}
}
///|
#cfg(target="js")
fn is_boundary(node : @dom.Node, boundary : @dom.Node?) -> Bool {
if boundary is Some(boundary) {
node.is_same_node(boundary)
} else {
false
}
}
///|
#cfg(target="js")
fn fragment_marker(node : @dom.Node, marker : String) -> @dom.Comment? {
if node.get_node_type() == 8 && node.get_node_value() == marker {
node.to_comment()
} else {
None
}
}
///|
/// Finds a DOM node that `vnode` can claim without crossing `boundary`.
#cfg(target="js")
fn find_hydratable(
vnode : VNode,
current : @dom.Node?,
boundary : @dom.Node?,
allow_skip~ : Bool,
) -> @dom.Node? {
let mut current = current
while current is Some(node) {
if is_boundary(node, boundary) {
return None
}
if node_matches(vnode, node) {
return Some(node)
}
if !allow_skip {
return None
}
current = node.get_next_sibling().to_option()
}
None
}
///|
#cfg(target="js")
fn recover_mismatch(
vnode : VNode,
scheduler : &Scheduler,
captured_link_listener : @dom.Listener,
parent : @dom.Node,
current : @dom.Node?,
boundary : @dom.Node?,
) -> (INode, @dom.Node?) {
let inode = vnode.insert(
scheduler,
captured_link_listener,
parent,
@js.Nullable::from_option(current),
)
if current is Some(node) && !is_boundary(node, boundary) {
let next = node.get_next_sibling().to_option()
parent.remove_child(node)
(inode, next)
} else {
(inode, current)
}
}
///|
#cfg(target="js")
fn remove_until(
parent : @dom.Node,
current : @dom.Node?,
boundary : @dom.Node?,
) -> Unit {
let mut current = current
while current is Some(node) && !is_boundary(node, boundary) {
let next = node.get_next_sibling().to_option()
parent.remove_child(node)
current = next
}
}
///|
#cfg(target="js")
fn hydrate_existing_element(
tag : String,
props : Props,
children : Children[VNode],
namespace_uri : String?,
element : @dom.Element,
scheduler : &Scheduler,
captured_link_listener : @dom.Listener,
allow_skip_children~ : Bool,
) -> INode {
let mounted_props = insert_props(
element, tag, props, scheduler, captured_link_listener,
)
let has_raw_children = props.props.contains("innerHTML") ||
props.props.contains("textContent")
let mounted_children = if is_html_void_element(tag) || has_raw_children {
Array([])
} else {
let (mounted, next) = hydrate_children(
children,
first_child(element.as_node()),
scheduler,
captured_link_listener,
element.as_node(),
boundary=None,
allow_skip=allow_skip_children,
)
if !allow_skip_children {
remove_until(element.as_node(), next, None)
}
mounted
}
Elem(tag, mounted_props, mounted_children, namespace_uri~, element)
}
///|
#cfg(target="js")
fn hydrate_fragment(
children : Array[VNode],
scheduler : &Scheduler,
captured_link_listener : @dom.Listener,
parent : @dom.Node,
current : @dom.Node?,
boundary~ : @dom.Node?,
allow_skip~ : Bool,
) -> (INode, @dom.Node?) {
fn find_fragment_start(current : @dom.Node?) -> @dom.Comment? {
let mut current = current
while current is Some(node) && !is_boundary(node, boundary) {
if fragment_marker(node, FRAGMENT_START_MARKER) is Some(start) {
return Some(start)
}
if !allow_skip {
return None
}
current = node.get_next_sibling().to_option()
}
None
}
fn find_fragment_end(current : @dom.Node?) -> @dom.Comment? {
let mut current = current
let mut depth = 0
while current is Some(node) && !is_boundary(node, boundary) {
if fragment_marker(node, FRAGMENT_START_MARKER) is Some(_) {
depth += 1
} else if fragment_marker(node, FRAGMENT_END_MARKER) is Some(end) {
if depth == 0 {
return Some(end)
}
depth -= 1
}
current = node.get_next_sibling().to_option()
}
None
}
if find_fragment_start(current) is Some(start) {
let closing = find_fragment_end(start.get_next_sibling().to_option())
let child_boundary = if closing is Some(end) {
Some(end.as_node())
} else {
boundary
}
let (mounted, next) = hydrate_children(
Array(children),
start.get_next_sibling().to_option(),
scheduler,
captured_link_listener,
parent,
boundary=child_boundary,
allow_skip~,
)
let end = if closing is Some(end) {
if !allow_skip {
remove_until(parent, next, Some(end.as_node()))
}
end
} else {
let end = @dom.document().create_comment(FRAGMENT_END_MARKER)
parent.insert_before(end.as_node(), @js.Nullable::from_option(next))
end
}
let children = if mounted is Array(children) { children } else { [] }
(Frag(children, start, end), end.get_next_sibling().to_option())
} else {
recover_mismatch(
Frag(children),
scheduler,
captured_link_listener,
parent,
current,
boundary,
)
}
}
///|
#cfg(target="js")
fn hydrate_node(
vnode : VNode,
scheduler : &Scheduler,
captured_link_listener : @dom.Listener,
parent : @dom.Node,
current : @dom.Node?,
boundary~ : @dom.Node?,
allow_skip~ : Bool,
) -> (INode, @dom.Node?) {
match vnode {
Thunk(hash, render) => {
let (inode, next) = hydrate_node(
render(),
scheduler,
captured_link_listener,
parent,
current,
boundary~,
allow_skip~,
)
(Thunk(hash, inode), next)
}
Frag(children) =>
hydrate_fragment(
children,
scheduler,
captured_link_listener,
parent,
current,
boundary~,
allow_skip~,
)
Text(text) =>
if find_hydratable(vnode, current, boundary, allow_skip~) is Some(node) {
if node.get_node_value() != text {
node.set_node_value(nullable(text))
}
let element : @dom.Element = @js.Value::cast_from(node).cast()
(Text(text, element), node.get_next_sibling().to_option())
} else {
recover_mismatch(
vnode, scheduler, captured_link_listener, parent, current, boundary,
)
}
Elem(tag, props, children, namespace_uri~) =>
if find_hydratable(vnode, current, boundary, allow_skip~) is Some(node) &&
node.to_element() is Some(element) {
let inode = hydrate_existing_element(
tag,
props,
children,
namespace_uri,
element,
scheduler,
captured_link_listener,
allow_skip_children=false,
)
(inode, node.get_next_sibling().to_option())
} else {
recover_mismatch(
vnode, scheduler, captured_link_listener, parent, current, boundary,
)
}
}
}
///|
#cfg(target="js")
fn hydrate_children(
children : Children[VNode],
current : @dom.Node?,
scheduler : &Scheduler,
captured_link_listener : @dom.Listener,
parent : @dom.Node,
boundary~ : @dom.Node?,
allow_skip~ : Bool,
) -> (Children[INode], @dom.Node?) {
let mut current = current
match children {
RawHtml(html) => {
parent.to_element().unwrap().set_inner_html(html)
(RawHtml(html), boundary)
}
Array(children) => {
let mounted = []
for child in children {
let (inode, next) = hydrate_node(
child,
scheduler,
captured_link_listener,
parent,
current,
boundary~,
allow_skip~,
)
mounted.push(inode)
current = next
}
(Array(mounted), current)
}
Map(children) => {
let mounted : Map[String, INode] = Map([])
for key, child in children {
let (inode, next) = hydrate_node(
child,
scheduler,
captured_link_listener,
parent,
current,
boundary~,
allow_skip~,
)
mounted[key] = inode
current = next
}
(Map(mounted), current)
}
}
}
///|
#cfg(target="js")
fn hydrate_root(
vnode : VNode,
scheduler : &Scheduler,
captured_link_listener : @dom.Listener,
) -> INode {
match vnode {
Elem(
"html",
props,
Array(
[
Elem(
"head",
head_props,
head_children,
namespace_uri=head_namespace_uri
),
Elem(
"body",
body_props,
body_children,
namespace_uri=body_namespace_uri
),
]
),
..
) => {
let document = @dom.document()
let html = document.get_document_element().unwrap()
let head_element = document.get_head().unwrap().as_element()
let body_element = document.get_body().unwrap().as_element()
let html_props = insert_props(
html, "html", props, scheduler, captured_link_listener,
)
let head = hydrate_existing_element(
"head",
head_props,
head_children,
head_namespace_uri,
head_element,
scheduler,
captured_link_listener,
allow_skip_children=true,
)
let body = hydrate_existing_element(
"body",
body_props,
body_children,
body_namespace_uri,
body_element,
scheduler,
captured_link_listener,
allow_skip_children=true,
)
Elem("html", html_props, Array([head, body]), html, namespace_uri=None)
}
_ => panic()
}
}