use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, TokenStreamExt, quote};
use rstml::node::{KeyedAttribute, Node, NodeAttribute, NodeBlock};
use syn::{Ident, spanned::Spanned};
use crate::trace_tailwind::add_to_tailwind;
use super::commons::{
extract_spread_block, generate_debug_class_name, is_component_name, is_default_block,
parse_block_of_statements, take_block_or_literal_expr, unwrap_block_if_single,
};
use super::component::{convert_child_to_component, convert_to_component};
const HTML_ATTR_FORMAT_ERROR: &str =
"in html node. Expected key=\"value\", key={value}, key={}, {value} or {..value} attribute.";
pub(super) fn convert_node(node: &Node, convert_to_dom_node: bool) -> TokenStream2 {
let parent_span = node.span();
let element = match node {
Node::Fragment(_) => {
emit_error!(parent_span, "Fragments not supported");
return TokenStream2::new();
}
Node::Element(element) => element,
_ => {
emit_error!(parent_span, "Expected Element");
return TokenStream2::new();
}
};
let node_name = element.name().to_string();
if is_component_name(&node_name) {
return convert_to_component(node);
}
let mut out_attr = Vec::new();
let mut out_spread_attrs = Vec::new();
let mut out_child = Vec::new();
let mut class_values = Vec::new();
let mut push_attr = |name, value| push_attribute(name, value, &mut out_attr, &mut class_values);
for attr_item in element.attributes() {
let span = attr_item.span();
match attr_item {
NodeAttribute::Attribute(KeyedAttribute {
key,
possible_value,
}) => {
let matches = take_block_or_literal_expr(possible_value, HTML_ATTR_FORMAT_ERROR);
match matches {
(Some(value), None) => {
let outcome = if is_default_block(&value.block) {
quote! { vertigo::AttrValue::String(Default::default()) }
} else {
unwrap_block_if_single(&value.block)
};
push_attr(key.to_string(), outcome)
}
(None, Some(lit)) => push_attr(key.to_string(), lit.to_token_stream()),
_ => (),
}
}
NodeAttribute::Block(block) => {
if let NodeBlock::ValidBlock(block) = block {
match block.stmts.len() {
0 => emit_error!(span, "Empty block {}", HTML_ATTR_FORMAT_ERROR),
_ => {
if let Some(new_block) =
extract_spread_block(block, |inmost_value| quote! { #inmost_value })
{
out_spread_attrs.push(quote! {
.add_attr_group({
#new_block
})
});
continue;
}
let (key, value, _) = parse_block_of_statements(block);
push_attr(key.to_string(), quote! { #value })
}
}
} else {
emit_error!(span, "Invalid block {}", HTML_ATTR_FORMAT_ERROR);
continue;
}
}
}
}
if !class_values.is_empty() {
if class_values.len() == 1 {
let class_value = &class_values[0];
out_attr.push(quote! {
.attr("class", #class_value)
});
} else {
let mut output = quote! {};
for class_value in class_values {
output.append_all(quote! { vertigo::AttrValue::from(#class_value), });
}
out_attr.push(quote! {
.attr("class", vertigo::AttrValue::combine(vec![#output]))
});
}
}
if let Some(children) = node.children() {
for child in children {
child_to_tokens(child, &mut out_child);
}
}
let dom_element = quote! {
vertigo::DomElement::new(#node_name)
#(#out_attr)*
#(#out_spread_attrs)*
#(#out_child)*
};
if convert_to_dom_node {
quote! {
vertigo::DomNode::from(
#dom_element
)
}
} else {
dom_element
}
}
fn push_attribute(
name: String,
value: TokenStream2,
out_attr: &mut Vec<TokenStream2>,
class_values: &mut Vec<TokenStream2>,
) {
if name.as_str() == "tw" {
let span = value.span();
let output = match add_to_tailwind(value) {
Ok(output) => output,
Err(err) => {
emit_error!(span, err);
return;
}
};
class_values.push(quote! { #output });
return;
}
if name.as_str() == "class" {
class_values.push(quote! { #value });
return;
}
let method_str = match name.as_str() {
"hook_key_down" | "on_blur" | "on_change" | "on_click" | "on_dropfile" | "on_input"
| "on_key_down" | "on_load" | "on_mouse_down" | "on_mouse_enter" | "on_mouse_leave"
| "on_mouse_up" | "on_submit" => &name,
"form" => "on_submit",
"css" => {
let class_name = generate_debug_class_name(&value);
out_attr.push(quote! { .css_with_class_name(#value, #class_name) });
return;
}
_ => {
out_attr.push(quote! { .attr(#name, #value) });
return;
}
};
let method = Ident::new(method_str, value.span());
out_attr.push(quote! {
.#method(#value)
});
}
fn child_to_tokens(child: &Node, out_child: &mut Vec<TokenStream2>) {
match child {
Node::Text(txt) => {
out_child.push(quote! { .child(vertigo::DomText::new(#txt)) });
}
Node::Element(element) => {
let tag_name = element.name().to_string();
if is_component_name(&tag_name) {
out_child.push(convert_child_to_component(child))
} else {
let node_ready = convert_node(child, false);
out_child.push(quote! { .child(#node_ready) });
}
}
Node::Block(block) => {
if let NodeBlock::ValidBlock(block) = block {
if block.stmts.is_empty() {
emit_warning!(block.span(), "Missing expression");
} else {
let spread_block = extract_spread_block(block, |value| {
quote! {
#value
.into_iter()
.map(|item| vertigo::EmbedDom::embed(item))
.collect::<Vec<_>>()
}
});
let new_block = if let Some(new_block) = spread_block {
quote! { .children({ #new_block }) }
} else {
let value = unwrap_block_if_single(block);
quote! { .child(vertigo::EmbedDom::embed(#value)) }
};
out_child.push(new_block)
}
} else {
emit_error!(block.span(), "Invalid block");
}
}
node => {
emit_error!(
node.span(),
"Unsupported node type {} as a child",
node.r#type()
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
fn run_push_attribute(
name: &str,
value: TokenStream2,
) -> (Vec<TokenStream2>, Vec<TokenStream2>) {
let mut out_attr = Vec::new();
let mut class_values = Vec::new();
push_attribute(name.to_string(), value, &mut out_attr, &mut class_values);
(out_attr, class_values)
}
#[test]
fn test_push_attribute_plain_attr() {
let (out_attr, class_values) = run_push_attribute("id", quote! { "main" });
assert_eq!(out_attr.len(), 1);
let s: String = out_attr[0]
.to_string()
.chars()
.filter(|c| !c.is_whitespace())
.collect();
assert!(s.contains(".attr("), "Expected .attr call, got: {s}");
assert!(s.contains("\"id\""), "Expected attribute name, got: {s}");
assert!(class_values.is_empty());
}
#[test]
fn test_push_attribute_event_handlers() {
let event_attrs = [
"on_click",
"on_change",
"on_input",
"on_blur",
"on_submit",
"on_key_down",
"on_load",
"on_mouse_down",
"on_mouse_enter",
"on_mouse_leave",
"on_mouse_up",
"on_dropfile",
"hook_key_down",
];
for attr in event_attrs {
let (out_attr, class_values) = run_push_attribute(attr, quote! { my_handler });
assert_eq!(out_attr.len(), 1, "Expected 1 attr entry for {attr}");
let s = out_attr[0].to_string();
assert!(s.contains(attr), "Expected method name {attr} in: {s}");
assert!(
class_values.is_empty(),
"Unexpected class_values for {attr}"
);
}
}
#[test]
fn test_push_attribute_form_alias() {
let (out_attr, _) = run_push_attribute("form", quote! { my_handler });
assert_eq!(out_attr.len(), 1);
let s = out_attr[0].to_string();
assert!(
s.contains("on_submit"),
"Expected on_submit alias, got: {s}"
);
}
#[test]
fn test_push_attribute_class() {
let (out_attr, class_values) = run_push_attribute("class", quote! { "my-class" });
assert!(
out_attr.is_empty(),
"class should not go to out_attr directly"
);
assert_eq!(class_values.len(), 1);
}
#[test]
fn test_push_attribute_css() {
let (out_attr, class_values) = run_push_attribute("css", quote! { my_css_value });
assert_eq!(out_attr.len(), 1);
let s = out_attr[0].to_string();
assert!(
s.contains("css_with_class_name"),
"Expected css_with_class_name, got: {s}"
);
assert!(class_values.is_empty());
}
#[test]
fn test_push_attribute_multiple_classes_accumulated() {
let mut out_attr = Vec::new();
let mut class_values = Vec::new();
push_attribute(
"class".to_string(),
quote! { "foo" },
&mut out_attr,
&mut class_values,
);
push_attribute(
"class".to_string(),
quote! { "bar" },
&mut out_attr,
&mut class_values,
);
assert_eq!(class_values.len(), 2);
assert!(out_attr.is_empty());
}
}