use std::collections::HashMap;
use crate::{get_driver, DomId, driver_module::callbacks::CallbackId, DriverDomCommand};
pub fn log_start() {
get_driver().inner.dom.log_start()
}
pub fn log_take() -> Vec<DriverDomCommand> {
get_driver().inner.dom.log_take()
}
#[derive(Clone, Debug)]
pub struct DomDebugFragment {
pub map: HashMap<DomId, DomDebugNode>,
pub css: HashMap<String, String>,
pub root_node: Option<DomId>,
}
#[derive(Clone, Debug, Default)]
pub struct DomDebugNode {
pub id: DomId,
pub parent_id: DomId,
pub name: &'static str,
pub attrs: HashMap<&'static str, String>,
pub callbacks: HashMap<String, CallbackId>,
pub children: Vec<DomId>,
pub text: Option<String>,
}
impl DomDebugFragment {
pub fn from_log() -> Self {
Self::from_cmds(log_take())
}
pub fn from_cmds(cmds: Vec<DriverDomCommand>) -> Self {
let mut map = HashMap::<DomId, DomDebugNode>::new();
let mut css = HashMap::<String, String>::new();
for cmd in cmds {
match cmd {
DriverDomCommand::MountNode { id } => {
map.entry(id).and_modify(|node| node.parent_id = DomId::root());
},
DriverDomCommand::CreateNode { id, name } => {
map.insert(id, DomDebugNode::from_name(id, name));
}
DriverDomCommand::CreateText { id, value } => {
map.insert(id, DomDebugNode::from_text(id, value));
}
DriverDomCommand::UpdateText { id, value } => {
map.entry(id).and_modify(|node| node.text = Some(value));
}
DriverDomCommand::SetAttr { id, name, value } => {
if let Some(node) = map.get_mut(&id) {
if name == "class" {
if let Some(new_styles) = css.get(&format!(".{value}")) {
let mut styles = String::new();
if let Some(old_styles) = node.attrs.get("style") {
styles.push_str(old_styles);
}
styles.push_str(new_styles);
node.attrs.insert("style", styles);
}
} else {
node.attrs.insert(name, value);
}
}
},
DriverDomCommand::RemoveNode { id } |
DriverDomCommand::RemoveText { id } |
DriverDomCommand::RemoveComment { id } => {
if let Some(parent_id) = map.get(&id).map(|node| node.parent_id) {
map.entry(parent_id)
.and_modify(|parent| parent.children.retain(|child_id| *child_id != id));
}
map.remove(&id);
},
DriverDomCommand::InsertBefore { parent, child, ref_id } => {
let child_parent_pair = if let Some(child) = map.get_mut(&child) {
let old_parent = child.parent_id;
child.parent_id = parent;
Some((old_parent, child.clone()))
} else {
None
};
if let Some((old_parent, child)) = child_parent_pair {
if let Some(old_parent) = map.get_mut(&old_parent) {
old_parent.children.retain(|id| *id != child.id);
}
if let Some(parent) = map.get_mut(&parent) {
if let Some(ref_id) = ref_id {
if let Some(index) = parent.children.iter().position(|elem_id| *elem_id == ref_id) {
parent.children.insert(index, child.id)
} else {
parent.children.push(child.id);
}
} else {
parent.children.push(child.id);
}
}
}
},
DriverDomCommand::InsertCss { selector, value } => {
println!("InsertCss {selector} {value}");
css.insert(selector, value);
},
DriverDomCommand::CreateComment { id, value } => {
map.insert(id, DomDebugNode::from_text(id, format!("<!-- {value} -->")));
}
DriverDomCommand::CallbackAdd { id, event_name, callback_id } => {
map.entry(id).and_modify(|node| { node.callbacks.insert(event_name, callback_id); });
},
DriverDomCommand::CallbackRemove { id, event_name, callback_id: _callback_id } => {
map.entry(id).and_modify(|node| { node.callbacks.remove(&event_name); });
},
}
}
let root_node = if let Some(root_node) = map.iter()
.find(|(_, child)| child.parent_id == DomId::root())
.map(|(id, _)| id)
.cloned()
{
Some(root_node)
} else {
map.iter()
.find(|(_, child)| child.parent_id == DomId::from_u64(0))
.map(|(id, _)| id)
.cloned()
};
Self { map, css, root_node }
}
pub fn to_pseudo_html(&self) -> String {
self.root_node.map(|rn| self.render(&rn)).unwrap_or_default()
}
fn render(&self, node_id: &DomId) -> String {
if let Some(node) = self.map.get(node_id) {
if node.name.is_empty() {
node.text.clone().unwrap_or_default()
} else {
let children = node.children.iter()
.map(|c| self.render(c))
.collect::<Vec<_>>()
.join("");
let attrs = node.attrs.iter()
.map(|(k,v)| format!(" {k}='{v}'"))
.collect::<Vec<_>>()
.join("");
let callbacks = node.callbacks.iter()
.map(|(k, v)| format!(" {k}={}", v.as_u64()))
.collect::<Vec<_>>()
.join("");
format!("<{}{attrs}{callbacks}>{children}</{}>", node.name, node.name)
}
} else {
String::default()
}
}
}
impl DomDebugNode {
pub fn from_name(id: DomId, name: &'static str) -> Self {
Self {
id,
parent_id: DomId::from_u64(0),
name,
..Default::default()
}
}
pub fn from_text(id: DomId, text: String) -> Self {
Self {
id,
parent_id: DomId::from_u64(0),
text: Some(text),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use crate::{self as vertigo, dom, css};
use super::{log_start, DomDebugFragment};
#[test]
fn pseudo_html_list() {
log_start();
let _el = dom! {
<div>
<ol>
<li>"item1"</li>
<li>"item2"</li>
<li>"item3"</li>
</ol>
</div>
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(html, "<div><ol><li>item1</li><li>item2</li><li>item3</li></ol></div>");
}
#[test]
fn pseudo_html_css() {
let green = css!( "color: green;" );
log_start();
let _el = dom! {
<div css={green}>"something"</div>
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(html, "<div style='color: green'>something</div>");
}
#[test]
fn pseudo_html_callback() {
let callback = || ();
log_start();
let _el = dom! {
<div on_click={callback}>"something"</div>
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(html, "<div mousedown=2>something</div>");
}
}