use std::collections::{BTreeMap, BTreeSet};
use crate::{
DomId,
dev::{CallbackId, command::DriverDomCommand},
driver_module::StaticString,
};
mod logs {
use std::rc::Rc;
use vertigo_macro::store;
use crate::{
computed::{
DropResource,
struct_mut::{ValueMut, VecMut},
},
dev::inspect::DriverDomCommand,
};
struct LogActive {
_drop_inspect: DropResource,
}
pub struct Inspect {
log_enabled: ValueMut<Option<LogActive>>,
log_vec: Rc<VecMut<DriverDomCommand>>,
}
impl Inspect {
pub fn log_start(&self) {
let drop_inspect = {
use crate::driver_module::get_driver_dom;
let log_vec = self.log_vec.clone();
get_driver_dom().inspect_command(move |command| {
log_vec.push(command);
})
};
self.log_enabled.set(Some(LogActive {
_drop_inspect: drop_inspect,
}));
}
pub fn log_take(&self) -> Vec<DriverDomCommand> {
let logs = self.log_vec.take();
self.log_enabled.set(None);
logs
}
}
#[store]
pub fn get_inspect() -> Rc<Inspect> {
Rc::new(Inspect {
log_enabled: ValueMut::new(None),
log_vec: Rc::new(VecMut::new()),
})
}
}
pub fn log_start() {
logs::get_inspect().log_start()
}
pub fn log_take() -> Vec<DriverDomCommand> {
logs::get_inspect().log_take()
}
#[derive(Clone, Debug)]
pub struct DomDebugFragment {
pub map: BTreeMap<DomId, DomDebugNode>,
pub css: BTreeMap<String, String>,
pub root_node: Option<DomId>,
}
#[derive(Clone, Debug, Default)]
pub struct DomDebugNode {
pub id: DomId,
pub parent_id: DomId,
pub name: StaticString,
pub attrs: BTreeMap<StaticString, String>,
pub callbacks: BTreeMap<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 = BTreeMap::<DomId, DomDebugNode>::new();
let mut css = BTreeMap::<String, String>::new();
let mut classes_seen_map = BTreeMap::new();
for cmd in cmds {
match cmd {
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".into() {
unpack_styles(node, &css, &mut classes_seen_map, value);
} else {
node.attrs.insert(name, value);
}
}
}
DriverDomCommand::RemoveAttr { id, name } => {
if let Some(node) = map.get_mut(&id) {
if name == "class".into() {
} else {
node.attrs.remove(&name);
}
}
}
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 } => {
if let Some(selector) = selector {
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_id())
.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("");
if children.is_empty() {
format!("<{}{attrs}{callbacks} />", node.name.as_str())
} else {
format!(
"<{}{attrs}{callbacks}>{children}</{}>",
node.name.as_str(),
node.name.as_str()
)
}
}
} else {
String::default()
}
}
}
impl DomDebugNode {
pub fn from_name(id: DomId, name: StaticString) -> 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()
}
}
}
fn unpack_styles(
node: &mut DomDebugNode,
css: &BTreeMap<String, String>,
classes_seen_map: &mut BTreeMap<DomId, BTreeSet<String>>,
value: String,
) {
let mut custom_classes = vec![];
let mut new_styles = BTreeSet::new();
if let Some(old_styles) = node.attrs.get(&("style".into())) {
new_styles.insert(old_styles.clone());
}
let classes_seen = classes_seen_map.entry(node.id).or_default();
for class_name in value.split(' ') {
if !classes_seen.contains(class_name) {
if let Some(styles_from_autocss) = css.get(&format!(".{class_name}")) {
new_styles.insert(styles_from_autocss.clone());
} else {
custom_classes.push(class_name);
}
classes_seen.insert(class_name.to_string());
}
}
if !custom_classes.is_empty() {
node.attrs.insert("class".into(), custom_classes.join(" "));
}
if !new_styles.is_empty() {
node.attrs.insert(
"style".into(),
new_styles.into_iter().collect::<Vec<_>>().join("; "),
);
}
}
#[cfg(test)]
mod tests {
use super::{DomDebugFragment, log_start};
use crate::{self as vertigo, css, dom};
#[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' v-css='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 click=1>something</div>");
}
#[test]
fn pseudo_html_attrs_order() {
log_start();
let _el = dom! {
<img id="one" alt="two" title="three" src="four.png" />
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(
html,
"<img alt='two' id='one' src='four.png' title='three' />"
);
}
#[test]
fn pseudo_html_css_unwrap() {
let css1 = css!("color: red;");
let css2 = css!("background: green;");
log_start();
let _el = dom! {
<div id="one" css={css1} css={css2} />
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(
html,
"<div id='one' style='background: green; color: red' v-css='css2' />"
);
}
#[test]
fn pseudo_html_css_unwrap_with_custom_class_names() {
let css1 = css!("color: red;");
let css2 = css!("background: green;");
log_start();
let _el = dom! {
<div id="one" css={css1} class="m-10 py-5" css={css2} />
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(
html,
"<div class='m-10 py-5' id='one' style='background: green; color: red' v-css='css2' />"
);
}
#[test]
fn pseudo_html_css_in_two_elements() {
let css1 = css!("color: red;");
log_start();
let _el = dom! {
<div>
<div id="one" css={&css1} />
<div id="two" css={&css1} />
</div>
};
let html = DomDebugFragment::from_log().to_pseudo_html();
assert_eq!(
html,
"<div><div id='one' style='color: red' v-css='&css1' /><div id='two' style='color: red' v-css='&css1' /></div>"
);
}
}