use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Expr, ExprLit, Ident, Lit};
use crate::view::ir::*;
pub struct Codegen {
pub cx: Ident,
}
impl Codegen {
pub fn view_root(&self, view_root: &ViewRoot) -> TokenStream {
match &view_root.0[..] {
[] => quote! {
::sycamore::view::View::empty()
},
[node] => self.view_node(node),
nodes => {
let append_nodes: TokenStream = nodes
.iter()
.map(|node| {
let quoted = self.view_node(node);
quote! { #quoted, }
})
.collect();
quote! {
::sycamore::view::View::new_fragment({
::std::vec![
#append_nodes
]
})
}
}
}
}
pub fn view_node(&self, view_node: &ViewNode) -> TokenStream {
let cx = &self.cx;
match view_node {
ViewNode::Element(elem) => {
let elem = self.element(elem);
quote! {
::sycamore::view::View::new_node(#elem)
}
}
ViewNode::Component(comp) => self.component(comp),
ViewNode::Text(Text { value }) => quote! {
::sycamore::view::View::new_node(::sycamore::generic_node::GenericNode::text_node(#value))
},
ViewNode::Dyn(d @ Dyn { value }) => {
let needs_cx = d.needs_cx(&cx.to_string());
match needs_cx {
true => quote! {
::sycamore::view::View::new_dyn_scoped(#cx, move |#cx|
::sycamore::view::IntoView::create(&(#value))
)
},
false => quote! {
::sycamore::view::View::new_dyn(#cx, move ||
::sycamore::view::IntoView::create(&(#value))
)
},
}
}
}
}
pub fn element(&self, elem: &Element) -> TokenStream {
let cx = &self.cx;
let Element {
tag,
attrs,
children,
} = elem;
let quote_tag = match tag {
ElementTag::Builtin(id) => quote! {
let __el = ::sycamore::generic_node::GenericNode::element::<::sycamore::web::html::#id>();
},
ElementTag::Custom(tag_s) => quote! {
let __el = ::sycamore::generic_node::GenericNode::element_from_tag(#tag_s);
},
};
let quote_attrs: TokenStream = attrs.iter().map(|attr| self.attribute(attr)).collect();
let quote_children = {
let multi = children.len() >= 2;
let mut children = children.iter().peekable();
let mut quoted = TokenStream::new();
while let Some(child) = children.next() {
let is_dyn = child.is_dynamic();
if is_dyn {
let codegen_ssr_markers = cfg!(feature = "ssr") && multi;
let mut marker_is_some = true;
let marker = if let Some(ViewNode::Element(elem)) =
children.next_if(|x| matches!(x, ViewNode::Element(_)))
{
let elem = self.element(elem);
quote! {
let __marker = #elem;
::sycamore::generic_node::GenericNode::append_child(&__el, &__marker);
let __marker = ::std::option::Option::Some(&__marker);
}
} else if let Some(ViewNode::Text(Text { value })) =
children.next_if(|x| matches!(x, ViewNode::Text(_)))
{
quote! {
let __marker = ::sycamore::generic_node::GenericNode::text_node(#value);
::sycamore::generic_node::GenericNode::append_child(&__el, &__marker);
let __marker = ::std::option::Option::Some(&__marker);
}
} else if children.peek().is_none() {
marker_is_some = false;
quote! {
let __marker = ::std::option::Option::None;
}
} else {
quote! {
let __marker = ::sycamore::generic_node::GenericNode::marker();
::sycamore::generic_node::GenericNode::append_child(&__el, &__marker);
let __marker = ::std::option::Option::Some(&__marker);
}
};
let marker_or_none = marker_is_some.then(|| marker.clone()).unwrap_or_default();
let initial = quote! { ::sycamore::utils::initial_node(&__el) };
let ssr_markers = quote! {
::sycamore::generic_node::GenericNode::append_child(
&__el,
&::sycamore::generic_node::GenericNode::marker_with_text("#"),
);
let __end_marker = ::sycamore::generic_node::GenericNode::marker_with_text("/");
::sycamore::generic_node::GenericNode::append_child(&__el, &__end_marker);
};
quoted.extend(match child {
ViewNode::Component(comp) => {
let comp = self.component(comp);
let quoted = quote! {
#marker
::sycamore::utils::render::insert(#cx, &__el, __comp, __initial, __marker, #multi);
};
codegen_ssr_markers.then(|| quote! {
let __comp = #comp;
let __initial = #initial;
if ::std::any::Any::type_id(&__el) == ::std::any::TypeId::of::<::sycamore::web::SsrNode>() {
#ssr_markers
::sycamore::utils::render::insert(#cx, &__el, __comp, __initial, Some(&__end_marker), #multi);
#marker_or_none
} else { #quoted }
}).unwrap_or(quote! {
let __comp = #comp;
let __initial = #initial;
#quoted
})
}
ViewNode::Dyn(d @ Dyn { value}) => {
let needs_cx = d.needs_cx(&self.cx.to_string());
let view_quoted = match needs_cx {
true => quote! {
::sycamore::view::View::new_dyn_scoped(#cx, move |#cx|
::sycamore::view::IntoView::create(&(#value))
)
},
false => quote! {
::sycamore::view::View::new_dyn(#cx, move ||
::sycamore::view::IntoView::create(&(#value))
)
}
};
let quoted = quote! {
#marker
::sycamore::utils::render::insert(#cx, &__el, __view, __initial, __marker, #multi);
};
codegen_ssr_markers.then(|| quote! {
let __view = #view_quoted;
let __initial = #initial;
if ::std::any::Any::type_id(&__el) == ::std::any::TypeId::of::<::sycamore::web::SsrNode>() {
#ssr_markers
::sycamore::utils::render::insert(
#cx, &__el, __view, __initial, Some(&__end_marker), #multi
);
#marker_or_none
} else { #quoted }
}).unwrap_or(quote! {
let __view = #view_quoted;
let __initial = #initial;
#quoted
})
},
_ => unreachable!("only component and dyn node can be dynamic"),
});
continue;
}
match child {
ViewNode::Element(elem) => quoted.extend({
let elem = self.element(elem);
quote! {
::sycamore::generic_node::GenericNode::append_child(&__el, &#elem);
}
}),
ViewNode::Component(_) => unreachable!("component is always dynamic"),
ViewNode::Text(Text { value }) => {
let intern = quote! {
#[cfg(target_arch = "wasm32")]
::sycamore::rt::intern(#value);
};
quoted.extend(match multi {
true => quote! {
#intern
::sycamore::generic_node::GenericNode::append_child(
&__el,
&::sycamore::generic_node::GenericNode::text_node(#value),
);
},
false => quote! {
#intern
::sycamore::generic_node::GenericNode::update_inner_text(&__el, #value);
},
});
}
ViewNode::Dyn(Dyn { value }) => quoted.extend(quote! {
::sycamore::utils::render::insert(#cx, &__el,
::sycamore::view::IntoView::create(&(#value)),
None, None, #multi
);
}),
}
}
quoted
};
quote! {{
#quote_tag
#quote_attrs
#quote_children
__el
}}
}
pub fn attribute(&self, attr: &Attribute) -> TokenStream {
let cx = &self.cx;
let mut tokens = TokenStream::new();
let expr = &attr.value;
let is_dynamic = !matches!(expr, Expr::Lit(ExprLit { .. }));
match &attr.ty {
AttributeType::Str { name } => {
let name = name.to_string();
let is_class = name == "class";
let quoted_text = if let Expr::Lit(ExprLit {
lit: Lit::Str(text),
..
}) = expr
{
quote! {
if ::std::cfg!(target_arch = "wasm32") {
::sycamore::rt::intern(#text)
} else {
#text
}
}
} else {
quote! {
&::std::string::ToString::to_string(&#expr)
}
};
let quoted_set_attribute = if is_class {
quote! {
::sycamore::generic_node::GenericNode::set_class_name(&__el, #quoted_text);
}
} else {
quote! {
::sycamore::generic_node::GenericNode::set_attribute(&__el, #name, #quoted_text);
}
};
if is_dynamic {
tokens.extend(quote! {
::sycamore::reactive::create_effect(#cx, {
let __el = ::std::clone::Clone::clone(&__el);
move || { #quoted_set_attribute }
});
});
} else {
tokens.extend(quote! { #quoted_set_attribute });
};
}
AttributeType::Bool { name } => {
let name = name.to_string();
let quoted_set_attribute = quote! {
if #expr {
::sycamore::generic_node::GenericNode::set_attribute(&__el, #name, "");
} else {
::sycamore::generic_node::GenericNode::remove_attribute(&__el, #name);
}
};
if is_dynamic {
tokens.extend(quote! {
::sycamore::reactive::create_effect(#cx, {
let __el = ::std::clone::Clone::clone(&__el);
move || {
#quoted_set_attribute
}
});
});
} else {
tokens.extend(quote! {
#quoted_set_attribute
});
};
}
AttributeType::DangerouslySetInnerHtml => {
if is_dynamic {
tokens.extend(quote! {
::sycamore::reactive::create_effect(#cx, {
let __el = ::std::clone::Clone::clone(&__el);
move || {
::sycamore::generic_node::GenericNode::dangerously_set_inner_html(
&__el,
#expr,
);
}
});
});
} else {
tokens.extend(quote! {
::sycamore::generic_node::GenericNode::dangerously_set_inner_html(
&__el,
#expr,
);
});
};
}
AttributeType::Event { event } => {
tokens.extend(quote! {
::sycamore::generic_node::GenericNode::event(
&__el,
#cx,
#event,
#expr,
);
});
}
AttributeType::Property { prop } => {
let set_property = quote! {
::sycamore::generic_node::GenericNode::set_property(
&__el,
#prop,
&::std::convert::Into::<::sycamore::rt::JsValue>::into(#expr)
);
};
if is_dynamic {
tokens.extend(quote! {
::sycamore::reactive::create_effect(#cx, {
let __el = ::std::clone::Clone::clone(&__el);
move || { #set_property }
});
});
} else {
tokens.extend(set_property);
}
}
AttributeType::Bind { prop } => {
#[derive(Clone, Copy)]
enum JsPropertyType {
Bool,
String,
}
let (event_name, property_ty) = match prop.as_str() {
"value" => ("input", JsPropertyType::String),
"checked" => ("change", JsPropertyType::Bool),
_ => {
tokens.extend(
syn::Error::new(
prop.span(),
&format!("property `{}` is not supported with `bind:`", prop),
)
.to_compile_error(),
);
return tokens;
}
};
let convert_into_jsvalue_fn = match property_ty {
JsPropertyType::Bool => {
quote! { ::sycamore::rt::JsValue::from_bool(*#expr.get()) }
}
JsPropertyType::String => {
quote! {
::sycamore::rt::JsValue::from_str(
&::std::string::ToString::to_string(&#expr.get())
)
}
}
};
let event_target_prop = quote! {
::sycamore::rt::Reflect::get(
&event.target().unwrap(),
&::std::convert::Into::<::sycamore::rt::JsValue>::into(#prop)
).unwrap()
};
let convert_from_jsvalue_fn = match property_ty {
JsPropertyType::Bool => quote! {
::sycamore::rt::JsValue::as_bool(&#event_target_prop).unwrap()
},
JsPropertyType::String => quote! {
::sycamore::rt::JsValue::as_string(&#event_target_prop).unwrap()
},
};
tokens.extend(quote! {
#[cfg(target_arch = "wasm32")]
::sycamore::reactive::create_effect(#cx, {
let __el = ::std::clone::Clone::clone(&__el);
move ||::sycamore::generic_node::GenericNode::set_property(
&__el,
#prop,
&#convert_into_jsvalue_fn,
)
});
::sycamore::generic_node::GenericNode::event(&__el, #cx, #event_name,
::std::boxed::Box::new(|event: ::sycamore::rt::Event| {
#expr.set(#convert_from_jsvalue_fn);
}),
);
});
}
AttributeType::Ref => {
tokens.extend(quote! {{
::sycamore::noderef::NodeRef::set(&#expr, ::std::clone::Clone::clone(&__el));
}});
}
}
tokens
}
pub fn component(&self, comp: &Component) -> TokenStream {
let cx = &self.cx;
match comp {
Component::Legacy(comp) => {
let LegacyComponent { ident, args } = comp;
quote! { ::sycamore::component::component_scope(move || #ident(#cx, #args)) }
}
Component::New(comp) => {
let NewComponent {
ident,
props,
children,
..
} = comp;
if props.is_empty()
&& (children.is_none() || children.as_ref().unwrap().0.is_empty())
{
quote! {
::sycamore::component::component_scope(move || #ident(#cx))
}
} else {
let name = props.iter().map(|x| &x.name);
let value = props.iter().map(|x| &x.value);
let children_quoted = children
.as_ref()
.map(|children| {
let children = self.view_root(children);
quote! {
.children(
::sycamore::component::Children::new(#cx, move |#cx| {
#[allow(unused_variables)]
let #cx: ::sycamore::reactive::BoundedScope = #cx;
#children
})
)
}
})
.unwrap_or_default();
quote! {{
let __component = &#ident; ::sycamore::component::component_scope(move || __component(
#cx,
::sycamore::component::element_like_component_builder(__component)
#(.#name(#value))*
#children_quoted
.build()
))
}}
}
}
}
}
}