use proc_macro2::Span;
use quote::{ToTokens, quote};
use syn::{
Expr, FieldValue, Ident, Member, Pat, Token, TypePath, braced, parse::Parse,
parse::ParseStream, punctuated::Punctuated, spanned::Spanned, token::Comma,
};
use uuid::Uuid;
use crate::adapter::ParsedAdapter;
pub(crate) enum ParsedElementChild {
Element(ElementOrAdapter),
Expr(Expr),
ControlFlow(Box<ControlFlow>),
}
pub(crate) enum ControlFlow {
If {
cond: Expr,
then_branch: Vec<ParsedElementChild>,
else_branch: Option<Box<ElseBranch>>,
},
For {
pat: Pat,
expr: Expr,
body: Vec<ParsedElementChild>,
},
Match {
expr: Expr,
arms: Vec<MatchArm>,
},
}
pub(crate) enum ElseBranch {
If(Box<ControlFlow>),
Block(Vec<ParsedElementChild>),
}
pub(crate) struct MatchArm {
pat: Pat,
guard: Option<Expr>,
body: Vec<ParsedElementChild>,
}
fn parse_children(input: ParseStream) -> syn::Result<Vec<ParsedElementChild>> {
let mut children = Vec::new();
while !input.is_empty() {
if input.peek(Token![if]) {
children.push(ParsedElementChild::ControlFlow(Box::new(parse_if(input)?)));
} else if input.peek(Token![for]) {
children.push(ParsedElementChild::ControlFlow(Box::new(parse_for(input)?)));
} else if input.peek(Token![match]) {
children.push(ParsedElementChild::ControlFlow(Box::new(parse_match(
input,
)?)));
} else if input.peek(syn::token::Brace) {
children.push(ParsedElementChild::Expr(input.parse()?));
} else {
children.push(ParsedElementChild::Element(input.parse()?));
}
}
Ok(children)
}
fn parse_if(input: ParseStream) -> syn::Result<ControlFlow> {
input.parse::<Token![if]>()?;
let cond = Expr::parse_without_eager_brace(input)?;
let content;
braced!(content in input);
let then_branch = parse_children(&content)?;
let else_branch = if input.peek(Token![else]) {
input.parse::<Token![else]>()?;
if input.peek(Token![if]) {
Some(Box::new(ElseBranch::If(Box::new(parse_if(input)?))))
} else {
let content;
braced!(content in input);
Some(Box::new(ElseBranch::Block(parse_children(&content)?)))
}
} else {
None
};
Ok(ControlFlow::If {
cond,
then_branch,
else_branch,
})
}
fn parse_for(input: ParseStream) -> syn::Result<ControlFlow> {
input.parse::<Token![for]>()?;
let pat = Pat::parse_single(input)?;
input.parse::<Token![in]>()?;
let expr = Expr::parse_without_eager_brace(input)?;
let content;
braced!(content in input);
let body = parse_children(&content)?;
Ok(ControlFlow::For { pat, expr, body })
}
fn parse_match(input: ParseStream) -> syn::Result<ControlFlow> {
input.parse::<Token![match]>()?;
let expr = Expr::parse_without_eager_brace(input)?;
let content;
braced!(content in input);
let mut arms = Vec::new();
while !content.is_empty() {
let pat = Pat::parse_multi(&content)?;
let guard = if content.peek(Token![if]) {
content.parse::<Token![if]>()?;
Some(content.parse::<Expr>()?)
} else {
None
};
content.parse::<Token![=>]>()?;
let body_content;
braced!(body_content in content);
let body = parse_children(&body_content)?;
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
arms.push(MatchArm { pat, guard, body });
}
Ok(ControlFlow::Match { expr, arms })
}
impl ParsedElementChild {
fn to_extend(&self, dest: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
match self {
ParsedElementChild::Element(element) => {
quote!(::ratatui_kit::extend_with_elements(&mut #dest, #element);)
}
ParsedElementChild::Expr(expr) => quote!({
let _child = #expr;
::ratatui_kit::extend_with_elements(&mut #dest, _child);
}),
ParsedElementChild::ControlFlow(cf) => cf.to_extend(dest),
}
}
}
impl ControlFlow {
fn to_extend(&self, dest: &proc_macro2::TokenStream) -> proc_macro2::TokenStream {
match self {
ControlFlow::If {
cond,
then_branch,
else_branch,
} => {
let then_stmts = then_branch.iter().map(|c| c.to_extend(dest));
let else_tokens = match else_branch {
None => quote!(),
Some(b) => match &**b {
ElseBranch::Block(children) => {
let stmts = children.iter().map(|c| c.to_extend(dest));
quote!(else { #(#stmts)* })
}
ElseBranch::If(inner) => {
let inner_tokens = inner.to_extend(dest);
quote!(else #inner_tokens)
}
},
};
quote!(if #cond { #(#then_stmts)* } #else_tokens)
}
ControlFlow::For { pat, expr, body } => {
let stmts = body.iter().map(|c| c.to_extend(dest));
quote!(for #pat in #expr { #(#stmts)* })
}
ControlFlow::Match { expr, arms } => {
let arm_tokens = arms.iter().map(|arm| {
let MatchArm { pat, guard, body } = arm;
let stmts = body.iter().map(|c| c.to_extend(dest));
let guard_tokens = match guard {
Some(g) => quote!(if #g),
None => quote!(),
};
quote!(#pat #guard_tokens => { #(#stmts)* })
});
quote!(match #expr { #(#arm_tokens)* })
}
}
}
}
pub enum PropsItem {
FieldValue(FieldValue),
Rest(Expr),
}
impl Parse for PropsItem {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(Token![..]) {
input.parse::<Token![..]>()?;
let rest_expr: Expr = input.parse()?;
Ok(PropsItem::Rest(rest_expr))
} else {
let field_value: FieldValue = input.parse()?;
Ok(PropsItem::FieldValue(field_value))
}
}
}
impl ToTokens for PropsItem {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
PropsItem::FieldValue(field_value) => {
let mut field_value = field_value.clone();
let expr = &field_value.expr;
field_value.expr = syn::parse2(quote!((#expr).into())).unwrap();
tokens.extend(quote!(#field_value))
}
PropsItem::Rest(expr) => {
tokens.extend(quote!(..#expr));
}
}
}
}
impl PropsItem {
pub fn span(&self) -> Span {
match self {
PropsItem::FieldValue(field_value) => field_value.span(),
PropsItem::Rest(expr) => expr.span(),
}
}
fn as_key_field(&self) -> Option<&FieldValue> {
match self {
PropsItem::FieldValue(fv) if matches!(&fv.member, Member::Named(ident) if ident == "key") => {
Some(fv)
}
_ => None,
}
}
}
pub struct ParsedElementHead {
ty: TypePath,
props: Punctuated<PropsItem, Comma>,
}
impl Parse for ParsedElementHead {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ty: TypePath = input.parse()?;
let props = if input.peek(syn::token::Paren) {
let props_input;
syn::parenthesized!(props_input in input);
Punctuated::parse_terminated(&props_input)?
} else {
Punctuated::new()
};
let rest_position = props
.iter()
.position(|item| matches!(item, PropsItem::Rest(_)));
if let Some(pos) = rest_position
&& pos != props.len() - 1
{
return Err(syn::Error::new(
props[pos].span(),
"the rest property must be the last item",
));
}
Ok(Self { ty, props })
}
}
impl ParsedElementHead {
#[cfg(feature = "router")]
pub fn key_span(&self) -> Option<Span> {
self.props
.iter()
.find_map(PropsItem::as_key_field)
.map(|fv| fv.member.span())
}
pub(crate) fn to_element_expr(
&self,
children: &[ParsedElementChild],
) -> proc_macro2::TokenStream {
let ty = &self.ty;
let decl_key = Uuid::new_v4().as_u128();
let has_rest = self
.props
.iter()
.any(|item| matches!(item, PropsItem::Rest(_)));
let key = self
.props
.iter()
.find_map(PropsItem::as_key_field)
.map(|fv| {
let expr = &fv.expr;
quote!(::ratatui_kit::ElementKey::user((#decl_key, #expr)))
})
.unwrap_or_else(|| quote!(::ratatui_kit::ElementKey::decl(#decl_key)));
let props_assignments = self
.props
.iter()
.filter(|item| item.as_key_field().is_none())
.map(|props_item| quote!(#props_item))
.collect::<Vec<_>>();
let set_children = if !children.is_empty() {
let dest = quote!(_element.props.children);
let stmts = children.iter().map(|child| child.to_extend(&dest));
Some(quote! {
#(#stmts)*
})
} else {
None
};
let has_props_assignments = !props_assignments.is_empty();
let default_rest = if has_rest {
quote! {
#(#props_assignments),*
}
} else {
quote! {
#(#props_assignments,)*
..Default::default()
}
};
let element_binding = if set_children.is_some() {
quote!(let mut _element=::ratatui_kit::Element::<#ty>{
key: #key,
props: _props,
};)
} else {
quote!(let _element=::ratatui_kit::Element::<#ty>{
key: #key,
props: _props,
};)
};
if has_props_assignments {
quote! {
({
type Props<'a>= <#ty as ::ratatui_kit::ElementType>::Props<'a>;
#[allow(clippy::needless_update)]
let _props = Props{
#default_rest
};
#element_binding
#set_children
_element
})
}
} else {
quote! {
({
type Props<'a>= <#ty as ::ratatui_kit::ElementType>::Props<'a>;
let _props = Props::default();
#element_binding
#set_children
_element
})
}
}
}
}
pub struct ParsedElement {
head: ParsedElementHead,
children: Vec<ParsedElementChild>,
}
impl Parse for ParsedElement {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let head = input.parse::<ParsedElementHead>()?;
let children = if input.peek(syn::token::Brace) {
let children_input;
braced!(children_input in input);
parse_children(&children_input)?
} else {
Vec::new()
};
Ok(Self { head, children })
}
}
impl ToTokens for ParsedElement {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(self.head.to_element_expr(&self.children));
}
}
pub enum ElementOrAdapter {
Element(ParsedElement),
Adapter(Box<ParsedAdapter>),
}
impl Parse for ElementOrAdapter {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(Ident) {
let fork = input.fork();
let ident: Ident = fork.parse()?;
let ident = ident.to_string();
if matches!(ident.as_str(), "widget" | "stateful") && fork.peek(syn::token::Paren) {
let adapter: ParsedAdapter = input.parse()?;
return Ok(ElementOrAdapter::Adapter(Box::new(adapter)));
}
}
if input.peek(Token![$]) {
return Err(input.error(
"`$` adapter syntax was removed; use `widget(...)` or `stateful(widget, state)`",
));
}
if input.peek(Token![#]) {
return Err(input.error("`#(expr)` child syntax was removed; use `{ expr }`"));
}
let element: ParsedElement = input.parse()?;
Ok(ElementOrAdapter::Element(element))
}
}
impl ToTokens for ElementOrAdapter {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
match self {
ElementOrAdapter::Element(element) => element.to_tokens(tokens),
ElementOrAdapter::Adapter(adapter) => adapter.to_tokens(tokens),
}
}
}