use std::collections::HashMap;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{spanned::Spanned, ExprPath};
use syn_rsx::{Node, NodeAttribute, NodeElement, NodeName, NodeValueExpr};
use uuid::Uuid;
use crate::{is_component_node, Mode};
const NON_BUBBLING_EVENTS: [&str; 11] = [
"load",
"unload",
"scroll",
"focus",
"blur",
"loadstart",
"progress",
"error",
"abort",
"load",
"loadend",
];
lazy_static::lazy_static! {
static ref EVENTS: HashMap<&'static str, &'static str> = {
let mut m = HashMap::new();
m.insert("auxclick", "MouseEvent");
m.insert("click", "MouseEvent");
m.insert("contextmenu", "MouseEvent");
m.insert("dblclick", "MouseEvent");
m.insert("drag", "DragEvent");
m.insert("dragend", "DragEvent");
m.insert("dragenter", "DragEvent");
m.insert("dragexit", "DragEvent");
m.insert("dragleave", "DragEvent");
m.insert("dragover", "DragEvent");
m.insert("dragstart", "DragEvent");
m.insert("drop", "DragEvent");
m.insert("blur", "FocusEvent");
m.insert("focus", "FocusEvent");
m.insert("focusin", "FocusEvent");
m.insert("focusout", "FocusEvent");
m.insert("keydown", "KeyboardEvent");
m.insert("keypress", "KeyboardEvent");
m.insert("keyup", "KeyboardEvent");
m.insert("loadstart", "ProgressEvent");
m.insert("progress", "ProgressEvent");
m.insert("loadend", "ProgressEvent");
m.insert("mousedown", "MouseEvent");
m.insert("mouseenter", "MouseEvent");
m.insert("mouseleave", "MouseEvent");
m.insert("mousemove", "MouseEvent");
m.insert("mouseout", "MouseEvent");
m.insert("mouseover", "MouseEvent");
m.insert("mouseup", "MouseEvent");
m.insert("wheel", "WheelEvent");
m.insert("input", "InputEvent");
m.insert("submit", "SubmitEvent");
m.insert("animationcancel", "AnimationEvent");
m.insert("animationend", "AnimationEvent");
m.insert("animationiteration", "AnimationEvent");
m.insert("animationstart", "AnimationEvent");
m.insert("gotpointercapture", "PointerEvent");
m.insert("lostpointercapture", "PointerEvent");
m.insert("pointercancel", "PointerEvent");
m.insert("pointerdown", "PointerEvent");
m.insert("pointerenter", "PointerEvent");
m.insert("pointerleave", "PointerEvent");
m.insert("pointermove", "PointerEvent");
m.insert("pointerout", "PointerEvent");
m.insert("pointerover", "PointerEvent");
m.insert("pointerup", "PointerEvent");
m.insert("touchcancel", "TouchEvent");
m.insert("touchend", "TouchEvent");
m.insert("transitioncancel", "TransitionEvent");
m.insert("transitionend", "TransitionEvent");
m.insert("transitionrun", "TransitionEvent");
m.insert("transitionstart", "TransitionEvent");
m
};
}
pub(crate) fn render_view(cx: &Ident, nodes: &[Node], mode: Mode) -> TokenStream {
let template_uid = Ident::new(
&format!("TEMPLATE_{}", Uuid::new_v4().simple()),
Span::call_site(),
);
if nodes.len() == 1 {
first_node_to_tokens(cx, &template_uid, &nodes[0], mode)
} else {
let nodes = nodes
.iter()
.map(|node| first_node_to_tokens(cx, &template_uid, node, mode));
quote! {
{
vec![
#(#nodes),*
]
}
}
}
}
fn first_node_to_tokens(cx: &Ident, template_uid: &Ident, node: &Node, mode: Mode) -> TokenStream {
match node {
Node::Doctype(_) | Node::Comment(_) => quote! {},
Node::Fragment(node) => {
let nodes = node
.children
.iter()
.map(|node| first_node_to_tokens(cx, template_uid, node, mode));
quote! {
{
vec![
#(#nodes),*
]
}
}
}
Node::Element(node) => root_element_to_tokens(cx, template_uid, node, mode),
Node::Block(node) => {
let value = node.value.as_ref();
quote! {
#value
}
}
Node::Text(node) => {
let value = node.value.as_ref();
quote! {
#value
}
}
_ => panic!("Root nodes need to be a Fragment (<></>), Element, or text."),
}
}
fn root_element_to_tokens(
cx: &Ident,
template_uid: &Ident,
node: &NodeElement,
mode: Mode,
) -> TokenStream {
let mut template = String::new();
let mut navigations = Vec::new();
let mut expressions = Vec::new();
if is_component_node(node) {
create_component(cx, node, mode)
} else {
element_to_tokens(
cx,
node,
&Ident::new("root", Span::call_site()),
None,
&mut 0,
&mut 0,
&mut template,
&mut navigations,
&mut expressions,
true,
mode,
);
match mode {
Mode::Ssr => {
quote! {{
#(#navigations);*;
let mut leptos_buffer = String::new();
#(#expressions);*
leptos_buffer
}}
}
_ => {
let generate_root = match mode {
Mode::Ssr => unreachable!(),
Mode::Client => quote! {
let root = #template_uid.with(|template| leptos_dom::clone_template(template));
},
Mode::Hydrate => {
quote! {
let root = #template_uid.with(|template| #cx.get_next_element(template));
}
}
};
let span = node.name.span();
let navigations = if navigations.is_empty() {
quote! {}
} else {
quote! { #(#navigations);* }
};
let expressions = if expressions.is_empty() {
quote! {}
} else {
quote! { #(#expressions;);* }
};
quote_spanned! {
span => {
thread_local! {
static #template_uid: web_sys::HtmlTemplateElement = leptos_dom::create_template(#template)
}
#generate_root
#navigations
#expressions
root
}
}
}
}
}
}
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
Parent,
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &NodeAttribute> {
node.attributes.iter().filter_map(|node| {
if let Node::Attribute(attribute) = node {
Some(attribute)
} else {
None
}
})
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens(
cx: &Ident,
node: &NodeElement,
parent: &Ident,
prev_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
mode: Mode,
) -> Ident {
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, node.name.span());
let name_str = node.name.to_string();
let span = node.name.span();
if mode == Mode::Ssr {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push('<');
leptos_buffer.push_str(#name_str);
});
} else {
template.push('<');
template.push_str(&name_str);
}
if mode == Mode::Ssr && is_root_el {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(" data-hk=\"");
leptos_buffer.push_str(&#cx.next_hydration_key().to_string());
leptos_buffer.push('"');
});
}
if mode == Mode::Ssr {
let class_attr = attributes(node)
.find(|a| a.key.to_string() == "class")
.map(|node| {
(
node.key.span(),
node.value
.as_ref()
.and_then(|n| String::try_from(n).ok())
.unwrap_or_default()
.trim()
.to_string(),
)
});
let class_attrs = attributes(node).filter_map(|node| {
let name = node.key.to_string();
if name.starts_with("class:") || name.starts_with("class-") {
let name = if name.starts_with("class:") {
name.replacen("class:", "", 1)
} else if name.starts_with("class-") {
name.replacen("class-", "", 1)
} else {
name
};
let value = node.value.as_ref().expect("class: attributes need values").as_ref();
let span = node.key.span();
Some(quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(&{#value}.into_class(#cx).as_value_string(#name));
})
} else {
None
}
})
.collect::<Vec<_>>();
if class_attr.is_some() || !class_attrs.is_empty() {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(" class=\"");
});
if let Some((span, value)) = class_attr {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(#value);
});
}
for attr in class_attrs {
expressions.push(attr);
}
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push('"');
});
}
}
for attr in attributes(node) {
if !(mode == Mode::Ssr && attr.key.to_string() == "class") {
attr_to_tokens(
cx,
attr,
&this_el_ident,
template,
expressions,
mode,
);
}
}
let debug_name = node.name.to_string();
if mode != Mode::Ssr {
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.clone().unchecked_into::<web_sys::Node>();
}
} else if let Some(prev_sib) = &prev_sib {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #prev_sib.next_sibling().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#debug_name, "nextSibling"));
}
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.first_child().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#debug_name, "firstChild"));
}
};
navigations.push(this_nav);
}
if matches!(
name_str.as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
) {
if mode == Mode::Ssr {
expressions.push(quote::quote! {
leptos_buffer.push_str("/>");
});
} else {
template.push_str("/>");
}
return this_el_ident;
} else if mode == Mode::Ssr {
expressions.push(quote::quote! {
leptos_buffer.push('>');
});
} else {
template.push('>');
}
let mut prev_sib = prev_sib;
let multi = !node.children.is_empty();
for (idx, child) in node.children.iter().enumerate() {
let next_sib = next_sibling_node(&node.children, idx + 1, next_el_id);
let curr_id = child_to_tokens(
cx,
child,
&this_el_ident,
if idx == 0 { None } else { prev_sib.clone() },
next_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
multi,
mode,
idx == 0,
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
if mode == Mode::Ssr {
expressions.push(quote::quote! {
leptos_buffer.push_str("</");
leptos_buffer.push_str(#name_str);
leptos_buffer.push('>');
})
} else {
template.push_str("</");
template.push_str(&name_str);
template.push('>');
}
this_el_ident
}
fn next_sibling_node(children: &[Node], idx: usize, next_el_id: &mut usize) -> Option<Ident> {
if children.len() <= idx {
None
} else {
let sibling = &children[idx];
match sibling {
Node::Element(sibling) => {
if is_component_node(sibling) {
next_sibling_node(children, idx + 1, next_el_id)
} else {
Some(child_ident(*next_el_id + 1, sibling.name.span()))
}
}
Node::Block(sibling) => Some(child_ident(*next_el_id + 1, sibling.value.span())),
Node::Text(sibling) => Some(child_ident(*next_el_id + 1, sibling.value.span())),
_ => panic!("expected either an element or a block"),
}
}
}
fn attr_to_tokens(
cx: &Ident,
node: &NodeAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
mode: Mode,
) {
let name = node.key.to_string();
let name = if name.starts_with('_') {
name.replacen('_', "", 1)
} else {
name
};
let name = if name.starts_with("attr:") {
name.replacen("attr:", "", 1)
} else {
name
};
let value = match &node.value {
Some(expr) => match expr.as_ref() {
syn::Expr::Lit(expr_lit) => {
if let syn::Lit::Str(s) = &expr_lit.lit {
AttributeValue::Static(s.value())
} else {
AttributeValue::Dynamic(expr)
}
}
_ => AttributeValue::Dynamic(expr),
},
None => AttributeValue::Empty,
};
let span = node.key.span();
if name == "ref" {
if mode != Mode::Ssr {
expressions.push(match &node.value {
Some(expr) => {
if let Some(ident) = expr_to_ident(expr) {
quote_spanned! {
span =>
#ident.load(#el_id.unchecked_ref::<web_sys::Element>());
}
} else {
panic!("'ref' needs to be passed a variable name")
}
}
_ => panic!("'ref' needs to be passed a variable name"),
})
}
}
else if name.starts_with("on:") {
let handler = node
.value
.as_ref()
.expect("event listener attributes need a value")
.as_ref();
let name = name.replacen("on:", "", 1);
let event_type = EVENTS.get(&name.as_str()).copied().unwrap_or("Event");
let event_type = event_type.parse::<TokenStream>().expect("couldn't parse event name");
if mode != Mode::Ssr {
if NON_BUBBLING_EVENTS.contains(&name.as_str()) {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener_undelegated::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
} else {
expressions.push(quote_spanned! {
span => ::leptos::add_event_listener::<web_sys::#event_type>(#el_id.unchecked_ref(), #name, #handler);
});
}
} else {
expressions.push(quote_spanned! {
span => let _ = ssr_event_listener::<web_sys::#event_type>(#handler);
});
}
}
else if name.starts_with("prop:") {
let name = name.replacen("prop:", "", 1);
if mode != Mode::Ssr {
let value = node
.value
.as_ref()
.expect("prop: blocks need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::property(#cx, #el_id.unchecked_ref(), #name, #value.into_property(#cx))
});
}
}
else if name.starts_with("class:") {
let name = name.replacen("class:", "", 1);
if mode == Mode::Ssr {
} else {
let value = node
.value
.as_ref()
.expect("class: attributes need values")
.as_ref();
expressions.push(quote_spanned! {
span => leptos_dom::class(#cx, #el_id.unchecked_ref(), #name, #value.into_class(#cx))
});
}
}
else {
match (value, mode) {
(AttributeValue::Empty, Mode::Ssr) => {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(#name);
});
}
(AttributeValue::Empty, _) => {
template.push(' ');
template.push_str(&name);
}
(AttributeValue::Static(value), Mode::Ssr) => {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(#name);
leptos_buffer.push_str("=\"");
leptos_buffer.push_str(&leptos_dom::escape_attr(&#value));
leptos_buffer.push('"');
});
}
(AttributeValue::Static(value), _) => {
template.push(' ');
template.push_str(&name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
}
(AttributeValue::Dynamic(value), Mode::Ssr) => {
expressions.push(quote_spanned! {
span => leptos_buffer.push(' ');
leptos_buffer.push_str(&{#value}.into_attribute(#cx).as_value_string(#name));
});
}
(AttributeValue::Dynamic(value), _) => {
expressions.push(quote_spanned! {
span => leptos_dom::attribute(#cx, #el_id.unchecked_ref(), #name, {#value}.into_attribute(#cx))
});
}
}
}
}
enum AttributeValue<'a> {
Static(String),
Dynamic(&'a syn::Expr),
Empty,
}
#[allow(clippy::too_many_arguments)]
fn child_to_tokens(
cx: &Ident,
node: &Node,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
navigations: &mut Vec<TokenStream>,
expressions: &mut Vec<TokenStream>,
multi: bool,
mode: Mode,
is_first_child: bool,
) -> PrevSibChange {
match node {
Node::Element(node) => {
if is_component_node(node) {
component_to_tokens(
cx,
node,
Some(parent),
prev_sib,
next_sib,
template,
expressions,
navigations,
next_el_id,
next_co_id,
multi,
mode,
is_first_child,
)
} else {
PrevSibChange::Sib(element_to_tokens(
cx,
node,
parent,
prev_sib,
next_el_id,
next_co_id,
template,
navigations,
expressions,
false,
mode,
))
}
}
Node::Text(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
next_co_id,
template,
expressions,
navigations,
mode,
),
Node::Block(node) => block_to_tokens(
cx,
&node.value,
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
next_co_id,
template,
expressions,
navigations,
mode,
),
_ => panic!("unexpected child node type"),
}
}
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
cx: &Ident,
value: &NodeValueExpr,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
next_co_id: &mut usize,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
mode: Mode,
) -> PrevSibChange {
let value = value.as_ref();
let str_value = match value {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(s) => Some(s.value()),
syn::Lit::Char(c) => Some(c.value().to_string()),
syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
syn::Lit::Float(f) => Some(f.base10_digits().to_string()),
_ => None,
},
_ => None,
};
let current: Option<Ident> = None;
let (name, location) = {
*next_el_id += 1;
let name = child_ident(*next_el_id, span);
let location = if let Some(sibling) = &prev_sib {
quote_spanned! {
span => let #name = #sibling.next_sibling().unwrap_or_else(|| ::leptos::__leptos_renderer_error("{block}", "nextSibling"));
}
} else {
quote_spanned! {
span => let #name = #parent.first_child().unwrap_or_else(|| ::leptos::__leptos_renderer_error("{block}", "firstChild"));
}
};
(Some(name), location)
};
let before = match &next_sib {
Some(child) => quote! { leptos::Marker::BeforeChild(#child.clone()) },
None => {
quote! { leptos::Marker::LastChild }
}
};
if let Some(v) = str_value {
if mode == Mode::Ssr {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str(&leptos_dom::escape_text(&#v));
});
} else {
navigations.push(location);
template.push_str(&v);
}
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
} else {
match mode {
Mode::Client => {
template.push_str("<!>");
navigations.push(location);
let current = match current {
Some(i) => quote! { Some(#i.into_child(#cx)) },
None => quote! { None },
};
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#value.into_child(#cx),
#before,
#current,
);
});
}
Mode::Hydrate => {
let el = child_ident(*next_el_id, span);
*next_co_id += 1;
let co = comment_ident(*next_co_id, span);
template.push_str("<!#><!/>");
navigations.push(quote! {
#location;
let (#el, #co) = #cx.get_next_marker(&#name);
});
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#value.into_child(#cx),
#before,
Some(Child::Nodes(#co)),
);
});
}
Mode::Ssr => expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str("<!--#-->");
leptos_buffer.push_str(&#value.into_child(#cx).as_child_string());
leptos_buffer.push_str("<!--/-->");
}),
}
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
#[allow(clippy::too_many_arguments)]
fn component_to_tokens(
cx: &Ident,
node: &NodeElement,
parent: Option<&Ident>,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
next_el_id: &mut usize,
next_co_id: &mut usize,
multi: bool,
mode: Mode,
is_first_child: bool,
) -> PrevSibChange {
let component_name = ident_from_tag_name(&node.name);
let component_name = format!("<{component_name}/>");
let create_component = create_component(cx, node, mode);
let span = node.name.span();
let mut current = None;
if let Some(parent) = parent {
let before = match &next_sib {
Some(child) => quote! { leptos::Marker::BeforeChild(#child.clone()) },
None => {
if multi {
quote! { leptos::Marker::LastChild }
} else {
quote! { leptos::Marker::NoChildren }
}
}
};
if mode == Mode::Ssr {
expressions.push(quote::quote_spanned! {
span => leptos_buffer.push_str("<!--#-->");
leptos_buffer.push_str(&#create_component.into_child(#cx).as_child_string());
leptos_buffer.push_str("<!--/-->");
});
} else if mode == Mode::Hydrate {
*next_el_id += 1;
let el = child_ident(*next_el_id, node.name.span());
*next_co_id += 1;
let co = comment_ident(*next_co_id, node.name.span());
let starts_at = if let Some(prev_sib) = prev_sib {
quote::quote! {{
#prev_sib.next_sibling().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#component_name, "nextSibling"))
}}
} else {
quote::quote! {{
#parent.first_child().unwrap_or_else(|| ::leptos::__leptos_renderer_error(#component_name, "firstChild"))
}}
};
current = Some(el.clone());
template.push_str("<!#><!/>");
navigations.push(quote! {
let (#el, #co) = #cx.get_next_marker(&#starts_at);
});
let before = if next_sib.is_none() {
quote::quote! { Marker::LastChild }
} else {
quote::quote! { Marker::BeforeChild(#el) }
};
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#create_component.into_child(#cx),
#before,
Some(Child::Nodes(#co)),
);
});
} else {
expressions.push(quote! {
leptos::insert(
#cx,
#parent.clone(),
#create_component.into_child(#cx),
#before,
None,
);
});
}
} else {
expressions.push(create_component)
}
match current {
Some(el) => PrevSibChange::Sib(el),
None => {
if is_first_child {
PrevSibChange::Parent
} else {
PrevSibChange::Skip
}
}
}
}
fn create_component(cx: &Ident, node: &NodeElement, mode: Mode) -> TokenStream {
let component_name = ident_from_tag_name(&node.name);
let span = node.name.span();
let component_props_name = Ident::new(&format!("{component_name}Props"), span);
let (initialize_children, children) = if node.children.is_empty() {
(quote! {}, quote! {})
} else if node.children.len() == 1 {
let child = render_view(cx, &node.children, mode);
if mode == Mode::Hydrate {
(
quote_spanned! { span => let children = vec![#child]; },
quote_spanned! { span => .children(Box::new(move || children.clone())) },
)
} else {
(
quote! {},
quote_spanned! { span => .children(Box::new(move || vec![#child])) },
)
}
} else {
let children = render_view(cx, &node.children, mode);
if mode == Mode::Hydrate {
(
quote_spanned! { span => let children = Box::new(move || #children); },
quote_spanned! { span => .children(children) },
)
} else {
(
quote! {},
quote_spanned! { span => .children(Box::new(move || #children)) },
)
}
};
let props = attributes(node).filter_map(|attr| {
let attr_name = attr.key.to_string();
if attr_name.starts_with("on:")
|| attr_name.starts_with("prop:")
|| attr_name.starts_with("class:")
|| attr_name.starts_with("attr:")
{
None
} else {
let name = ident_from_tag_name(&attr.key);
let span = attr.key.span();
let value = attr
.value
.as_ref()
.map(|v| {
let v = v.as_ref();
quote_spanned! { span => #v }
})
.unwrap_or_else(|| quote_spanned! { span => #name });
Some(quote_spanned! {
span => .#name(#value)
})
}
});
let mut other_attrs = attributes(node).filter_map(|attr| {
let attr_name = attr.key.to_string();
let span = attr.key.span();
let value = attr.value.as_ref().map(|e| e.as_ref());
if let Some(event_name) = attr_name.strip_prefix("on:") {
let handler = attr
.value
.as_ref()
.expect("on: event listener attributes need a value")
.as_ref();
if NON_BUBBLING_EVENTS.contains(&event_name) {
Some(quote_spanned! {
span => ::leptos::add_event_listener_undelegated(#component_name.unchecked_ref(), #event_name, #handler);
})
} else if let Some(event_type) = EVENTS.get(event_name).map(|&e| e.parse::<TokenStream>().unwrap_or_default()) {
Some(quote_spanned! {
span => ::leptos::add_event_listener::<#event_type>(#component_name.unchecked_ref(), #event_name, #handler);
})
} else {
Some(quote_spanned! {
span => ::leptos::add_event_listener::<web_sys::Event>(#component_name.unchecked_ref(), #event_name, #handler)
})
}
}
else if let Some(name) = attr_name.strip_prefix("prop:") {
Some(quote_spanned! {
span => leptos_dom::property(#cx, #component_name.unchecked_ref(), #name, #value.into_property(#cx))
})
}
else if let Some(name) = attr_name.strip_prefix("class:") {
Some(quote_spanned! {
span => leptos_dom::class(#cx, #component_name.unchecked_ref(), #name, #value.into_class(#cx))
})
}
else { attr_name.strip_prefix("attr:").map(|name| quote_spanned! {
span => leptos_dom::attribute(#cx, #component_name.unchecked_ref(), #name, #value.into_attribute(#cx))
}) }
}).peekable();
if other_attrs.peek().is_none() {
quote_spanned! {
span => create_component(#cx, move || {
#initialize_children
#component_name(
#cx,
#component_props_name::builder()
#(#props)*
#children
.build(),
)
})
}
} else {
quote_spanned! {
span => create_component(#cx, move || {
#initialize_children
let #component_name = #component_name(
#cx,
#component_props_name::builder()
#(#props)*
#children
.build(),
);
#(#other_attrs);*;
#component_name
})
}
}
}
fn child_ident(el_id: usize, span: Span) -> Ident {
let id = format!("_el{el_id}");
Ident::new(&id, span)
}
fn comment_ident(co_id: usize, span: Span) -> Ident {
let id = format!("_co{co_id}");
Ident::new(&id, span)
}
fn ident_from_tag_name(tag_name: &NodeName) -> Ident {
match tag_name {
NodeName::Path(path) => path
.path
.segments
.iter()
.last()
.map(|segment| segment.ident.clone())
.expect("element needs to have a name"),
NodeName::Block(_) => panic!("blocks not allowed in tag-name position"),
_ => Ident::new(
&tag_name.to_string().replace(['-', ':'], "_"),
tag_name.span(),
),
}
}
fn expr_to_ident(expr: &syn::Expr) -> Option<&ExprPath> {
match expr {
syn::Expr::Block(block) => block.block.stmts.last().and_then(|stmt| {
if let syn::Stmt::Expr(expr) = stmt {
expr_to_ident(expr)
} else {
None
}
}),
syn::Expr::Path(path) => Some(path),
_ => None,
}
}