use super::{component_builder::component_to_tokens, IdeTagHelper};
use crate::attribute_value;
use itertools::Either;
use leptos_hot_reload::parsing::{
block_to_primitive_expression, is_component_node, value_to_string,
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, quote_spanned, ToTokens};
use rstml::node::{
KeyedAttribute, Node, NodeAttribute, NodeBlock, NodeElement,
};
use syn::spanned::Spanned;
pub(crate) fn render_template(nodes: &[Node]) -> TokenStream {
let template_uid = Ident::new("__TEMPLATE", Span::call_site());
match nodes.first() {
Some(Node::Element(node)) => {
root_element_to_tokens(&template_uid, node)
}
_ => {
abort!(Span::call_site(), "template! takes a single root element.")
}
}
}
fn root_element_to_tokens(
template_uid: &Ident,
node: &NodeElement,
) -> TokenStream {
let mut template = String::new();
let mut navigations = Vec::new();
let mut stmts_for_ide = IdeTagHelper::new();
let mut expressions = Vec::new();
if is_component_node(node) {
component_to_tokens(node, None)
} else {
element_to_tokens(
node,
&Ident::new("root", Span::call_site()),
None,
&mut 0,
&mut 0,
&mut template,
&mut navigations,
&mut stmts_for_ide,
&mut expressions,
true,
);
let generate_root = quote! {
let root = #template_uid.with(|tpl| tpl.content().clone_node_with_deep(true))
.unwrap()
.first_child()
.unwrap();
};
let tag_name = node.name().to_string();
let stmts_for_ide = stmts_for_ide.into_iter();
quote! {
{
thread_local! {
static #template_uid: ::leptos::web_sys::HtmlTemplateElement = {
let document = ::leptos::document();
let el = document.create_element("template").unwrap();
el.set_inner_html(#template);
::leptos::wasm_bindgen::JsCast::unchecked_into(el)
}
}
#(#stmts_for_ide)*
#generate_root
#(#navigations)*
#(#expressions;)*
::leptos::leptos_dom::View::Element(leptos::leptos_dom::Element {
#[cfg(debug_assertions)]
name: #tag_name.into(),
element: ::leptos::wasm_bindgen::JsCast::unchecked_into(root),
#[cfg(debug_assertions)]
view_marker: None
})
}
}
}
}
#[derive(Clone, Debug)]
enum PrevSibChange {
Sib(Ident),
Parent,
Skip,
}
fn attributes(node: &NodeElement) -> impl Iterator<Item = &KeyedAttribute> {
node.attributes().iter().filter_map(|node| {
if let NodeAttribute::Attribute(attribute) = node {
Some(attribute)
} else {
None
}
})
}
#[allow(clippy::too_many_arguments)]
fn element_to_tokens(
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>,
stmts_for_ide: &mut IdeTagHelper,
expressions: &mut Vec<TokenStream>,
is_root_el: bool,
) -> Ident {
*next_el_id += 1;
let this_el_ident = child_ident(*next_el_id, Span::call_site());
let name_str = node.name().to_string();
let span = node.open_tag.span();
template.push('<');
template.push_str(&name_str);
for attr in attributes(node) {
attr_to_tokens(attr, &this_el_ident, template, expressions);
}
let debug_name = node.name().to_string();
let this_nav = if is_root_el {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident =
::leptos::wasm_bindgen::JsCast::unchecked_into::<leptos::web_sys::Node>(#parent.clone());
}
} 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(|| panic!("error : {} => {} ", #debug_name, "nextSibling"));
}
} else {
quote_spanned! {
span => let #this_el_ident = #debug_name;
let #this_el_ident = #parent.first_child().unwrap_or_else(|| panic!("error: {} => {}", #debug_name, "firstChild"));
}
};
navigations.push(this_nav);
stmts_for_ide.save_element_completion(node);
if matches!(
name_str.as_str(),
"area"
| "base"
| "br"
| "col"
| "embed"
| "hr"
| "img"
| "input"
| "link"
| "meta"
| "param"
| "source"
| "track"
| "wbr"
) {
template.push_str("/>");
return this_el_ident;
} else {
template.push('>');
}
let mut prev_sib = prev_sib;
for (idx, child) in node.children.iter().enumerate() {
let next_sib =
match next_sibling_node(&node.children, idx + 1, next_el_id) {
Ok(next_sib) => next_sib,
Err(err) => abort!(span, "{}", err),
};
let curr_id = child_to_tokens(
child,
&this_el_ident,
if idx == 0 { None } else { prev_sib.clone() },
next_sib,
next_el_id,
next_co_id,
template,
navigations,
stmts_for_ide,
expressions,
);
prev_sib = match curr_id {
PrevSibChange::Sib(id) => Some(id),
PrevSibChange::Parent => None,
PrevSibChange::Skip => prev_sib,
};
}
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,
) -> Result<Option<Ident>, String> {
if children.len() <= idx {
Ok(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 {
Ok(Some(child_ident(
*next_el_id + 1,
sibling.name().span(),
)))
}
}
Node::Block(sibling) => {
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
}
Node::Text(sibling) => {
Ok(Some(child_ident(*next_el_id + 1, sibling.span())))
}
_ => Err("expected either an element or a block".to_string()),
}
}
}
fn attr_to_tokens(
node: &KeyedAttribute,
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
) {
let name = node.key.to_string();
let name = name.strip_prefix('_').unwrap_or(&name);
let name = name.strip_prefix("attr:").unwrap_or(name);
let value = match &node.value() {
Some(expr) => match expr {
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" {
abort!(span, "node_ref not yet supported in template! macro")
}
else if name.starts_with("on:") {
let (event_type, handler) =
crate::view::event_from_attribute_node(node, false);
expressions.push(quote! {
::leptos::leptos_dom::add_event_helper(::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #event_type, #handler);
})
}
else if let Some(name) = name.strip_prefix("prop:") {
let value = attribute_value(node);
expressions.push(quote_spanned! {
span => ::leptos::leptos_dom::property(::leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name, #value.into_property())
});
}
else if let Some(name) = name.strip_prefix("class:") {
let value = attribute_value(node);
expressions.push(quote_spanned! {
span => ::leptos::leptos_dom::class_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), #value.into_class())
});
}
else {
match value {
AttributeValue::Empty => {
template.push(' ');
template.push_str(name);
}
AttributeValue::Static(value) => {
template.push(' ');
template.push_str(name);
template.push_str("=\"");
template.push_str(&value);
template.push('"');
}
AttributeValue::Dynamic(value) => {
expressions.push(quote_spanned! {
span => ::leptos::leptos_dom::attribute_helper(leptos::wasm_bindgen::JsCast::unchecked_ref(&#el_id), #name.into(), {#value}.into_attribute())
});
}
}
}
}
enum AttributeValue<'a> {
Static(String),
Dynamic(&'a syn::Expr),
Empty,
}
#[allow(clippy::too_many_arguments)]
fn child_to_tokens(
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>,
stmts_for_ide: &mut IdeTagHelper,
expressions: &mut Vec<TokenStream>,
) -> PrevSibChange {
match node {
Node::Element(node) => {
if is_component_node(node) {
proc_macro_error::emit_error!(
node.name().span(),
"component children not allowed in template!, use view! \
instead"
);
PrevSibChange::Skip
} else {
PrevSibChange::Sib(element_to_tokens(
node,
parent,
prev_sib,
next_el_id,
next_co_id,
template,
navigations,
stmts_for_ide,
expressions,
false,
))
}
}
Node::Text(node) => block_to_tokens(
Either::Left(node.value_string()),
node.value.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
Node::RawText(node) => block_to_tokens(
Either::Left(node.to_string_best()),
node.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
Node::Block(NodeBlock::ValidBlock(b)) => {
let value = match block_to_primitive_expression(b)
.and_then(value_to_string)
{
Some(v) => Either::Left(v),
None => Either::Right(b.into_token_stream()),
};
block_to_tokens(
value,
b.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
)
}
Node::Block(b @ NodeBlock::Invalid { .. }) => block_to_tokens(
Either::Right(b.into_token_stream()),
b.span(),
parent,
prev_sib,
next_sib,
next_el_id,
template,
expressions,
navigations,
),
_ => abort!(Span::call_site(), "unexpected child node type"),
}
}
#[allow(clippy::too_many_arguments)]
fn block_to_tokens(
value: Either<String, TokenStream>,
span: Span,
parent: &Ident,
prev_sib: Option<Ident>,
next_sib: Option<Ident>,
next_el_id: &mut usize,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
) -> PrevSibChange {
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(|| panic!("error : {} => {} ", "{block}", "nextSibling"));
}
} else {
quote_spanned! {
span => let #name = #parent.first_child().unwrap_or_else(|| panic!("error : {} => {} ", "{block}", "firstChild"));
}
};
(Some(name), location)
};
let mount_kind = match &next_sib {
Some(child) => {
quote! { ::leptos::leptos_dom::MountKind::Before(&#child.clone()) }
}
None => {
quote! { ::leptos::leptos_dom::MountKind::Append(&#parent) }
}
};
match value {
Either::Left(v) => {
navigations.push(location);
template.push_str(&v);
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
Either::Right(value) => {
template.push_str("<!>");
navigations.push(location);
expressions.push(quote! {
::leptos::leptos_dom::mount_child(#mount_kind, &{#value}.into_view());
});
if let Some(name) = name {
PrevSibChange::Sib(name)
} else {
PrevSibChange::Parent
}
}
}
}
fn child_ident(el_id: usize, span: Span) -> Ident {
let id = format!("_el{el_id}");
Ident::new(&id, span)
}