use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::LitStr;
use crate::parse::{RsxAttr, RsxAttrValue, RsxNode};
use crate::props::{VOID_ELEMENTS, component_fn_name, known_typed_setters};
pub fn emit_root(nodes: Vec<RsxNode>) -> TokenStream {
if nodes.len() == 1 {
emit_node_as_markup(&nodes.into_iter().next().unwrap())
} else {
emit_fragment_as_markup(&nodes)
}
}
fn emit_node_as_markup(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Component {
name,
attrs,
children,
span,
} => emit_component(name, attrs, children, *span),
RsxNode::Element {
name,
attrs,
children,
span,
} => emit_element(name, attrs, children, *span),
RsxNode::Fragment(children) => emit_fragment_as_markup(children),
RsxNode::Text(s) => {
let escaped = html_escape_text(s);
quote! { ::basecoat_core::Markup::from_static(#escaped) }
}
RsxNode::Block(ts) => {
quote! {
{
let __v = { #ts };
::basecoat_core::Markup::from(::std::format!("{}", __v))
}
}
}
}
}
fn emit_fragment_as_markup(children: &[RsxNode]) -> TokenStream {
let child_pushes = emit_children_into_string(children);
quote! {
{
let mut __s = ::std::string::String::new();
#child_pushes
::basecoat_core::Markup::from(__s)
}
}
}
fn emit_component(name: &str, attrs: &[RsxAttr], children: &[RsxNode], span: Span) -> TokenStream {
let fn_name_str = component_fn_name(name);
let fn_name = format_ident!("{}", fn_name_str, span = span);
let props_name = format_ident!("{}Props", name, span = span);
let typed_fields = known_typed_setters(name);
let mut typed_setter_calls: Vec<TokenStream> = Vec::new();
let mut extra_attrs: Vec<TokenStream> = Vec::new();
for attr in attrs {
let key = &attr.key;
if key == "children" {
continue;
}
if typed_fields.contains(&key.as_str()) {
let setter = format_ident!("{}", key.replace('-', "_"), span = span);
let val_ts = match &attr.value {
RsxAttrValue::Literal(s) => {
let lit = LitStr::new(s, span);
quote! { #lit }
}
RsxAttrValue::Expr(e) => quote! { #e },
RsxAttrValue::None => {
quote! { true }
}
};
typed_setter_calls.push(quote! { .#setter(#val_ts) });
} else {
let key_lit = LitStr::new(key, span);
let val_ts = match &attr.value {
RsxAttrValue::Literal(s) => {
let lit = LitStr::new(s, span);
quote! { #lit }
}
RsxAttrValue::Expr(e) => {
quote! { ::std::format!("{}", #e) }
}
RsxAttrValue::None => {
quote! { "" }
}
};
extra_attrs.push(quote! { __attrs.push(#key_lit, #val_ts); });
}
}
let children_ts = if children.is_empty() {
quote! { ::basecoat_core::Children::from(::std::string::String::new()) }
} else {
let child_pushes = emit_children_into_string(children);
quote! {
::basecoat_core::Children::from({
let mut __s = ::std::string::String::new();
#child_pushes
__s
})
}
};
quote_spanned! { span =>
{
let mut __attrs = ::basecoat_core::AttrMap::new();
#( #extra_attrs )*
::basecoat_components::#fn_name(
::basecoat_core::#props_name::builder()
#( #typed_setter_calls )*
.attrs(__attrs)
.children(#children_ts)
.build()
)
}
}
}
fn emit_element(name: &str, attrs: &[RsxAttr], children: &[RsxNode], span: Span) -> TokenStream {
let is_void = VOID_ELEMENTS.contains(&name);
if is_void && !children.is_empty() {
return syn::Error::new(
span,
format!("void element `<{}>` cannot have children", name),
)
.to_compile_error();
}
let attr_pushes: Vec<TokenStream> = attrs
.iter()
.map(|attr| emit_html_attr(attr, span))
.collect();
let open_tag = LitStr::new(&format!("<{}", name), span);
let name_lit = name.to_string();
if is_void {
quote_spanned! { span =>
{
let mut __s = ::std::string::String::new();
__s.push_str(#open_tag);
#( #attr_pushes )*
__s.push_str("/>");
::basecoat_core::Markup::from(__s)
}
}
} else {
let close_tag = LitStr::new(&format!("</{}>", name_lit), span);
let child_pushes = emit_children_into_string(children);
quote_spanned! { span =>
{
let mut __s = ::std::string::String::new();
__s.push_str(#open_tag);
#( #attr_pushes )*
__s.push('>');
#child_pushes
__s.push_str(#close_tag);
::basecoat_core::Markup::from(__s)
}
}
}
}
fn emit_html_attr(attr: &RsxAttr, span: Span) -> TokenStream {
let key_lit = LitStr::new(&attr.key, span);
match &attr.value {
RsxAttrValue::Literal(s) => {
let escaped = html_escape_attr(s);
let attr_str = format!(" {}=\"{}\"", attr.key, escaped);
let attr_lit = LitStr::new(&attr_str, span);
quote! { __s.push_str(#attr_lit); }
}
RsxAttrValue::Expr(e) => {
quote! {
__s.push(' ');
__s.push_str(#key_lit);
__s.push_str("=\"");
__s.push_str(&::basecoat_macros_rt::escape_attr(
&::std::format!("{}", #e)
));
__s.push('"');
}
}
RsxAttrValue::None => {
let attr_str = format!(" {}", attr.key);
let attr_lit = LitStr::new(&attr_str, span);
quote! { __s.push_str(#attr_lit); }
}
}
}
pub fn emit_children_into_string(children: &[RsxNode]) -> TokenStream {
let mut stmts: Vec<TokenStream> = Vec::new();
for child in children {
stmts.push(emit_child_push(child));
}
quote! { #( #stmts )* }
}
fn emit_child_push(node: &RsxNode) -> TokenStream {
match node {
RsxNode::Text(s) => {
let escaped = html_escape_text(s);
quote! { __s.push_str(#escaped); }
}
RsxNode::Block(ts) => {
quote! {
{
let __v = { #ts };
__s.push_str(&::std::format!("{}", __v));
}
}
}
RsxNode::Fragment(children) => {
let inner = emit_children_into_string(children);
quote! { #inner }
}
RsxNode::Component {
name,
attrs,
children,
span,
} => {
let markup_ts = emit_component(name, attrs, children, *span);
quote! {
{
let __markup = #markup_ts;
__s.push_str(&__markup.0);
}
}
}
RsxNode::Element {
name,
attrs,
children,
span,
} => {
let markup_ts = emit_element(name, attrs, children, *span);
quote! {
{
let __markup = #markup_ts;
__s.push_str(&__markup.0);
}
}
}
}
}
fn html_escape_text(s: &str) -> proc_macro2::TokenStream {
let escaped = escape_text_str(s);
let lit = LitStr::new(&escaped, Span::call_site());
quote! { #lit }
}
fn html_escape_attr(s: &str) -> String {
escape_attr_str(s)
}
fn escape_text_str(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
other => out.push(other),
}
}
out
}
fn escape_attr_str(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}