use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{braced, token, Expr, FnArg, Ident, ItemFn, LitStr, Pat, Path, Token, Type};
#[proc_macro]
pub fn rsx(input: TokenStream) -> TokenStream {
let nodes = syn::parse_macro_input!(input as Nodes);
let elements: Vec<TokenStream2> = nodes.0.iter().map(Node::to_element).collect();
let tokens = if elements.len() == 1 {
elements.into_iter().next().unwrap_or_default()
} else {
quote! { ::ferroui_view::Element::Fragment(::std::vec![#(#elements),*]) }
};
tokens.into()
}
struct Nodes(Vec<Node>);
impl Parse for Nodes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut nodes = Vec::new();
while !input.is_empty() {
nodes.push(input.parse()?);
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(Nodes(nodes))
}
}
enum Node {
Element(ElementNode),
Text(LitStr),
Expr(Expr, Span),
If(Box<Expr>, Vec<Node>, Option<Vec<Node>>, Span),
For(Box<Pat>, Box<Expr>, Vec<Node>, Span),
}
struct ElementNode {
path: Path,
attributes: Vec<Attribute>,
children: Vec<Node>,
}
enum Attribute {
Value {
owner: Option<Ident>,
name: Ident,
value: Expr,
},
Call { name: Ident, arguments: syn::punctuated::Punctuated<Expr, Token![,]> },
}
fn parse_children(input: ParseStream) -> syn::Result<Vec<Node>> {
let content;
braced!(content in input);
Ok(content.parse::<Nodes>()?.0)
}
impl Parse for Node {
fn parse(input: ParseStream) -> syn::Result<Self> {
if input.peek(LitStr) {
return Ok(Node::Text(input.parse()?));
}
if input.peek(token::Brace) {
let span = input.span();
let content;
braced!(content in input);
return Ok(Node::Expr(content.parse()?, span));
}
if input.peek(Token![if]) {
let span = input.span();
input.parse::<Token![if]>()?;
let condition = Expr::parse_without_eager_brace(input)?;
let then = parse_children(input)?;
let otherwise = if input.peek(Token![else]) {
input.parse::<Token![else]>()?;
if input.peek(Token![if]) {
Some(vec![input.parse::<Node>()?])
} else {
Some(parse_children(input)?)
}
} else {
None
};
return Ok(Node::If(Box::new(condition), then, otherwise, span));
}
if input.peek(Token![for]) {
let span = input.span();
input.parse::<Token![for]>()?;
let pattern = Pat::parse_single(input)?;
input.parse::<Token![in]>()?;
let items = Expr::parse_without_eager_brace(input)?;
let body = parse_children(input)?;
return Ok(Node::For(Box::new(pattern), Box::new(items), body, span));
}
let path: Path = input.call(Path::parse_mod_style)?;
let content;
braced!(content in input);
let mut attributes = Vec::new();
let mut children = Vec::new();
while !content.is_empty() {
if content.peek(Ident::peek_any) && content.peek2(token::Paren) {
let name = content.call(Ident::parse_any)?;
let arguments;
syn::parenthesized!(arguments in content);
let arguments = arguments.parse_terminated(Expr::parse, Token![,])?;
attributes.push(Attribute::Call { name, arguments });
} else if content.peek(Ident::peek_any) && (content.peek2(Token![,]) || is_last_token(&content)) {
let name = content.call(Ident::parse_any)?;
let value: Expr = syn::parse_quote!(#name);
attributes.push(Attribute::Value { owner: None, name, value });
} else if is_attribute(&content) {
let owner = if content.peek2(Token![.]) {
let owner = content.call(Ident::parse_any)?;
content.parse::<Token![.]>()?;
Some(owner)
} else {
None
};
let name = content.call(Ident::parse_any)?;
content.parse::<Token![:]>()?;
let value: Expr = content.parse()?;
attributes.push(Attribute::Value { owner, name, value });
} else {
children.push(content.parse()?);
}
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
Ok(Node::Element(ElementNode { path, attributes, children }))
}
}
fn is_last_token(input: ParseStream) -> bool {
let fork = input.fork();
fork.parse::<proc_macro2::TokenTree>().is_ok() && fork.is_empty()
}
fn is_attribute(input: ParseStream) -> bool {
let fork = input.fork();
if fork.call(Ident::parse_any).is_err() {
return false;
}
if fork.peek(Token![.]) {
let _ = fork.parse::<Token![.]>();
if fork.call(Ident::parse_any).is_err() {
return false;
}
}
fork.peek(Token![:]) && !fork.peek(Token![::])
}
fn snake_case(name: &str) -> String {
let mut out = String::new();
let chars: Vec<char> = name.chars().collect();
for (index, &char) in chars.iter().enumerate() {
if char.is_uppercase() {
let previous_lower = index > 0 && (chars[index - 1].is_lowercase() || chars[index - 1].is_ascii_digit());
let next_lower = chars.get(index + 1).is_some_and(|next| next.is_lowercase());
let previous_upper = index > 0 && chars[index - 1].is_uppercase();
if index > 0 && (previous_lower || (previous_upper && next_lower)) {
out.push('_');
}
out.extend(char.to_lowercase());
} else {
out.push(char);
}
}
out
}
impl Node {
fn to_child(&self) -> TokenStream2 {
match self {
Node::Element(element) => element.to_builder(),
Node::Text(text) => text.to_token_stream(),
Node::Expr(expr, span) => quote_spanned! {*span=> #expr },
Node::If(..) | Node::For(..) => self.to_element(),
}
}
fn to_element(&self) -> TokenStream2 {
match self {
Node::Element(element) => {
let builder = element.to_builder();
quote! { ::ferroui_view::IntoElement::into_element(#builder) }
}
Node::Text(text) => quote! { ::ferroui_view::IntoChild::into_child(#text) },
Node::Expr(expr, span) => quote_spanned! {*span=> ::ferroui_view::IntoChild::into_child(#expr) },
Node::If(condition, then, otherwise, span) => {
let then = fragment(then);
let otherwise = otherwise.as_ref().map_or_else(|| quote! { ::ferroui_view::Element::Empty }, |nodes| fragment(nodes));
quote_spanned! {*span=> if #condition { #then } else { #otherwise } }
}
Node::For(pattern, items, body, span) => {
let body = fragment(body);
quote_spanned! {*span=>
::ferroui_view::Element::Fragment(
::std::iter::IntoIterator::into_iter(#items).map(|#pattern| #body).collect::<::std::vec::Vec<_>>()
)
}
}
}
}
}
fn fragment(nodes: &[Node]) -> TokenStream2 {
let elements: Vec<TokenStream2> = nodes.iter().map(Node::to_element).collect();
match elements.len() {
0 => quote! { ::ferroui_view::Element::Empty },
1 => elements.into_iter().next().unwrap_or_default(),
_ => quote! { ::ferroui_view::Element::Fragment(::std::vec![#(#elements),*]) },
}
}
impl ElementNode {
fn to_builder(&self) -> TokenStream2 {
let path = &self.path;
let span = self.path.span();
let mut tokens = quote_spanned! {span=> <#path as ::ferroui_view::Builder>::builder() };
for attribute in &self.attributes {
match attribute {
Attribute::Value { owner, name, value } => {
let method = match owner {
Some(owner) => {
format_ident!("{}_{}", snake_case(&owner.unraw().to_string()), name.unraw(), span = name.span())
}
None => name.clone(),
};
tokens = quote_spanned! {method.span()=> #tokens.#method(#value) };
}
Attribute::Call { name, arguments } => {
tokens = quote_spanned! {name.span()=> #tokens.#name(#arguments) };
}
}
}
for child in &self.children {
let child = child.to_child();
tokens = quote! { #tokens.child(#child) };
}
tokens
}
}
#[proc_macro_attribute]
pub fn component(attribute: TokenStream, item: TokenStream) -> TokenStream {
if !attribute.is_empty() {
return syn::Error::new(Span::call_site(), "#[component] takes no arguments").to_compile_error().into();
}
let function = syn::parse_macro_input!(item as ItemFn);
match expand_component(function) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
struct Param {
name: Ident,
ty: Type,
default: Option<TokenStream2>,
attrs: Vec<syn::Attribute>,
}
fn is_option(ty: &Type) -> bool {
matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Option"))
}
fn expand_component(mut function: ItemFn) -> syn::Result<TokenStream2> {
let name = function.sig.ident.clone();
let name_text = name.to_string();
if !name_text.chars().next().is_some_and(char::is_uppercase) {
return Err(syn::Error::new(name.span(), "a component is named in upper camel case (`TodoList`)"));
}
let builder_function = Ident::new(&snake_case(&name_text), name.span());
let builder_type = format_ident!("{}Builder", name);
let visibility = function.vis.clone();
let mut params = Vec::new();
for input in function.sig.inputs.iter_mut() {
let FnArg::Typed(typed) = input else {
return Err(syn::Error::new(input.span(), "a component is a free function"));
};
let Pat::Ident(pattern) = &*typed.pat else {
return Err(syn::Error::new(typed.pat.span(), "a parameter of a component is a name"));
};
let mut default = None;
let mut kept = Vec::new();
for attr in typed.attrs.drain(..) {
if attr.path().is_ident("prop") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
if meta.input.peek(Token![=]) {
let value: Expr = meta.value()?.parse()?;
default = Some(value.to_token_stream());
} else {
default = Some(quote! { ::std::default::Default::default() });
}
Ok(())
} else {
Err(meta.error("expected `default` or `default = ...`"))
}
})?;
} else {
kept.push(attr);
}
}
typed.attrs = kept.clone();
let ty = (*typed.ty).clone();
if default.is_none() && is_option(&ty) {
default = Some(quote! { ::std::option::Option::None });
}
params.push(Param { name: pattern.ident.clone(), ty, default, attrs: kept });
}
let fields = params.iter().map(|param| {
let (name, ty) = (¶m.name, ¶m.ty);
quote! { #name: ::std::option::Option<#ty> }
});
let setters = params.iter().map(|param| {
let (name, ty) = (¶m.name, ¶m.ty);
let docs = param.attrs.iter().filter(|attr| attr.path().is_ident("doc"));
if name == "children" {
quote! {
#(#docs)*
pub fn children(mut self, children: impl ::ferroui_view::IntoChildren) -> Self {
let mut items = match self.children.take() {
::std::option::Option::Some(::ferroui_view::Element::Fragment(items)) => items,
_ => ::std::vec::Vec::new(),
};
items.extend(::ferroui_view::IntoChildren::into_children(children));
self.children = ::std::option::Option::Some(::ferroui_view::Element::Fragment(items));
self
}
pub fn child(mut self, child: impl ::ferroui_view::IntoChild) -> Self {
let mut items = match self.children.take() {
::std::option::Option::Some(::ferroui_view::Element::Fragment(items)) => items,
_ => ::std::vec::Vec::new(),
};
items.push(::ferroui_view::IntoChild::into_child(child));
self.children = ::std::option::Option::Some(::ferroui_view::Element::Fragment(items));
self
}
}
} else {
quote! {
#(#docs)*
pub fn #name(mut self, value: impl ::ferroui_view::IntoValue<#ty>) -> Self {
self.#name = ::std::option::Option::Some(::ferroui_view::IntoValue::into_value(value));
self
}
}
}
});
let arguments = params.iter().map(|param| {
let name = ¶m.name;
let message = format!("`{}` is a required property of `{}`", name, name_text);
match ¶m.default {
Some(default) => quote! { self.#name.unwrap_or_else(|| #default) },
None if name == "children" => quote! { self.#name.unwrap_or_default() },
None => quote! { self.#name.unwrap_or_else(|| panic!(#message)) },
}
});
let field_names: Vec<&Ident> = params.iter().map(|param| ¶m.name).collect();
let doc = format!("The builder of the component [`{name_text}`].");
let start_doc = format!("Starts an element of the component [`{name_text}`].");
let type_doc = format!("The component [`{name_text}`] as the name of an element of `rsx!`.");
function.attrs.push(syn::parse_quote!(#[allow(non_snake_case)]));
Ok(quote! {
#function
#[doc = #doc]
#[allow(non_camel_case_types)]
#visibility struct #builder_type {
#(#fields,)*
key: ::std::option::Option<::ferroui_view::Key>,
}
#[doc = #type_doc]
#[allow(non_camel_case_types)]
#visibility struct #name {}
impl ::ferroui_view::Builder for #name {
type Output = #builder_type;
fn builder() -> #builder_type {
#builder_function()
}
}
#[doc = #start_doc]
#visibility fn #builder_function() -> #builder_type {
#builder_type { #(#field_names: ::std::option::Option::None,)* key: ::std::option::Option::None }
}
impl #builder_type {
#(#setters)*
pub fn key(mut self, key: impl ::std::convert::Into<::ferroui_view::Key>) -> Self {
self.key = ::std::option::Option::Some(key.into());
self
}
}
impl ::ferroui_view::IntoElement for #builder_type {
fn into_element(self) -> ::ferroui_view::Element {
let element = #name(#(#arguments),*);
match self.key {
::std::option::Option::Some(key) => ::ferroui_view::element::with_key(element, key),
::std::option::Option::None => element,
}
}
}
})
}