1use proc_macro::TokenStream;
10use proc_macro2::{Span, TokenStream as TokenStream2};
11use quote::{format_ident, quote, quote_spanned, ToTokens};
12use syn::ext::IdentExt;
13use syn::parse::{Parse, ParseStream};
14use syn::spanned::Spanned;
15use syn::{braced, token, Expr, FnArg, Ident, ItemFn, LitStr, Pat, Path, Token, Type};
16
17#[proc_macro]
49pub fn rsx(input: TokenStream) -> TokenStream {
50 let nodes = syn::parse_macro_input!(input as Nodes);
51 let elements: Vec<TokenStream2> = nodes.0.iter().map(Node::to_element).collect();
52 let tokens = if elements.len() == 1 {
53 elements.into_iter().next().unwrap_or_default()
54 } else {
55 quote! { ::ferroui_view::Element::Fragment(::std::vec![#(#elements),*]) }
56 };
57 tokens.into()
58}
59
60struct Nodes(Vec<Node>);
61
62impl Parse for Nodes {
63 fn parse(input: ParseStream) -> syn::Result<Self> {
64 let mut nodes = Vec::new();
65 while !input.is_empty() {
66 nodes.push(input.parse()?);
67 if input.peek(Token![,]) {
68 input.parse::<Token![,]>()?;
69 }
70 }
71 Ok(Nodes(nodes))
72 }
73}
74
75enum Node {
76 Element(ElementNode),
77 Text(LitStr),
78 Expr(Expr, Span),
79 If(Box<Expr>, Vec<Node>, Option<Vec<Node>>, Span),
80 For(Box<Pat>, Box<Expr>, Vec<Node>, Span),
81}
82
83struct ElementNode {
84 path: Path,
85 attributes: Vec<Attribute>,
86 children: Vec<Node>,
87}
88
89enum Attribute {
90 Value {
92 owner: Option<Ident>,
94 name: Ident,
95 value: Expr,
96 },
97 Call { name: Ident, arguments: syn::punctuated::Punctuated<Expr, Token![,]> },
99}
100
101fn parse_children(input: ParseStream) -> syn::Result<Vec<Node>> {
102 let content;
103 braced!(content in input);
104 Ok(content.parse::<Nodes>()?.0)
105}
106
107impl Parse for Node {
108 fn parse(input: ParseStream) -> syn::Result<Self> {
109 if input.peek(LitStr) {
110 return Ok(Node::Text(input.parse()?));
111 }
112 if input.peek(token::Brace) {
113 let span = input.span();
114 let content;
115 braced!(content in input);
116 return Ok(Node::Expr(content.parse()?, span));
117 }
118 if input.peek(Token![if]) {
119 let span = input.span();
120 input.parse::<Token![if]>()?;
121 let condition = Expr::parse_without_eager_brace(input)?;
122 let then = parse_children(input)?;
123 let otherwise = if input.peek(Token![else]) {
124 input.parse::<Token![else]>()?;
125 if input.peek(Token![if]) {
126 Some(vec![input.parse::<Node>()?])
127 } else {
128 Some(parse_children(input)?)
129 }
130 } else {
131 None
132 };
133 return Ok(Node::If(Box::new(condition), then, otherwise, span));
134 }
135 if input.peek(Token![for]) {
136 let span = input.span();
137 input.parse::<Token![for]>()?;
138 let pattern = Pat::parse_single(input)?;
139 input.parse::<Token![in]>()?;
140 let items = Expr::parse_without_eager_brace(input)?;
141 let body = parse_children(input)?;
142 return Ok(Node::For(Box::new(pattern), Box::new(items), body, span));
143 }
144 let path: Path = input.call(Path::parse_mod_style)?;
145 let content;
146 braced!(content in input);
147 let mut attributes = Vec::new();
148 let mut children = Vec::new();
149 while !content.is_empty() {
150 if content.peek(Ident::peek_any) && content.peek2(token::Paren) {
151 let name = content.call(Ident::parse_any)?;
152 let arguments;
153 syn::parenthesized!(arguments in content);
154 let arguments = arguments.parse_terminated(Expr::parse, Token![,])?;
155 attributes.push(Attribute::Call { name, arguments });
156 } else if content.peek(Ident::peek_any) && (content.peek2(Token![,]) || is_last_token(&content)) {
157 let name = content.call(Ident::parse_any)?;
160 let value: Expr = syn::parse_quote!(#name);
161 attributes.push(Attribute::Value { owner: None, name, value });
162 } else if is_attribute(&content) {
163 let owner = if content.peek2(Token![.]) {
164 let owner = content.call(Ident::parse_any)?;
165 content.parse::<Token![.]>()?;
166 Some(owner)
167 } else {
168 None
169 };
170 let name = content.call(Ident::parse_any)?;
171 content.parse::<Token![:]>()?;
172 let value: Expr = content.parse()?;
173 attributes.push(Attribute::Value { owner, name, value });
174 } else {
175 children.push(content.parse()?);
176 }
177 if content.peek(Token![,]) {
178 content.parse::<Token![,]>()?;
179 }
180 }
181 Ok(Node::Element(ElementNode { path, attributes, children }))
182 }
183}
184
185fn is_last_token(input: ParseStream) -> bool {
187 let fork = input.fork();
188 fork.parse::<proc_macro2::TokenTree>().is_ok() && fork.is_empty()
189}
190
191fn is_attribute(input: ParseStream) -> bool {
194 let fork = input.fork();
195 if fork.call(Ident::parse_any).is_err() {
196 return false;
197 }
198 if fork.peek(Token![.]) {
199 let _ = fork.parse::<Token![.]>();
200 if fork.call(Ident::parse_any).is_err() {
201 return false;
202 }
203 }
204 fork.peek(Token![:]) && !fork.peek(Token![::])
205}
206
207fn snake_case(name: &str) -> String {
209 let mut out = String::new();
210 let chars: Vec<char> = name.chars().collect();
211 for (index, &char) in chars.iter().enumerate() {
212 if char.is_uppercase() {
213 let previous_lower = index > 0 && (chars[index - 1].is_lowercase() || chars[index - 1].is_ascii_digit());
214 let next_lower = chars.get(index + 1).is_some_and(|next| next.is_lowercase());
215 let previous_upper = index > 0 && chars[index - 1].is_uppercase();
216 if index > 0 && (previous_lower || (previous_upper && next_lower)) {
217 out.push('_');
218 }
219 out.extend(char.to_lowercase());
220 } else {
221 out.push(char);
222 }
223 }
224 out
225}
226
227impl Node {
228 fn to_child(&self) -> TokenStream2 {
230 match self {
231 Node::Element(element) => element.to_builder(),
232 Node::Text(text) => text.to_token_stream(),
233 Node::Expr(expr, span) => quote_spanned! {*span=> #expr },
234 Node::If(..) | Node::For(..) => self.to_element(),
235 }
236 }
237
238 fn to_element(&self) -> TokenStream2 {
240 match self {
241 Node::Element(element) => {
242 let builder = element.to_builder();
243 quote! { ::ferroui_view::IntoElement::into_element(#builder) }
244 }
245 Node::Text(text) => quote! { ::ferroui_view::IntoChild::into_child(#text) },
246 Node::Expr(expr, span) => quote_spanned! {*span=> ::ferroui_view::IntoChild::into_child(#expr) },
247 Node::If(condition, then, otherwise, span) => {
248 let then = fragment(then);
249 let otherwise = otherwise.as_ref().map_or_else(|| quote! { ::ferroui_view::Element::Empty }, |nodes| fragment(nodes));
250 quote_spanned! {*span=> if #condition { #then } else { #otherwise } }
251 }
252 Node::For(pattern, items, body, span) => {
253 let body = fragment(body);
254 quote_spanned! {*span=>
255 ::ferroui_view::Element::Fragment(
256 ::std::iter::IntoIterator::into_iter(#items).map(|#pattern| #body).collect::<::std::vec::Vec<_>>()
257 )
258 }
259 }
260 }
261 }
262}
263
264fn fragment(nodes: &[Node]) -> TokenStream2 {
265 let elements: Vec<TokenStream2> = nodes.iter().map(Node::to_element).collect();
266 match elements.len() {
267 0 => quote! { ::ferroui_view::Element::Empty },
268 1 => elements.into_iter().next().unwrap_or_default(),
269 _ => quote! { ::ferroui_view::Element::Fragment(::std::vec![#(#elements),*]) },
270 }
271}
272
273impl ElementNode {
274 fn to_builder(&self) -> TokenStream2 {
275 let path = &self.path;
276 let span = self.path.span();
277 let mut tokens = quote_spanned! {span=> <#path as ::ferroui_view::Builder>::builder() };
278 for attribute in &self.attributes {
279 match attribute {
280 Attribute::Value { owner, name, value } => {
281 let method = match owner {
282 Some(owner) => {
283 format_ident!("{}_{}", snake_case(&owner.unraw().to_string()), name.unraw(), span = name.span())
284 }
285 None => name.clone(),
286 };
287 tokens = quote_spanned! {method.span()=> #tokens.#method(#value) };
288 }
289 Attribute::Call { name, arguments } => {
290 tokens = quote_spanned! {name.span()=> #tokens.#name(#arguments) };
291 }
292 }
293 }
294 for child in &self.children {
295 let child = child.to_child();
296 tokens = quote! { #tokens.child(#child) };
297 }
298 tokens
299 }
300}
301
302#[proc_macro_attribute]
325pub fn component(attribute: TokenStream, item: TokenStream) -> TokenStream {
326 if !attribute.is_empty() {
327 return syn::Error::new(Span::call_site(), "#[component] takes no arguments").to_compile_error().into();
328 }
329 let function = syn::parse_macro_input!(item as ItemFn);
330 match expand_component(function) {
331 Ok(tokens) => tokens.into(),
332 Err(error) => error.to_compile_error().into(),
333 }
334}
335
336struct Param {
337 name: Ident,
338 ty: Type,
339 default: Option<TokenStream2>,
340 attrs: Vec<syn::Attribute>,
341}
342
343fn is_option(ty: &Type) -> bool {
344 matches!(ty, Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Option"))
345}
346
347fn expand_component(mut function: ItemFn) -> syn::Result<TokenStream2> {
348 let name = function.sig.ident.clone();
349 let name_text = name.to_string();
350 if !name_text.chars().next().is_some_and(char::is_uppercase) {
351 return Err(syn::Error::new(name.span(), "a component is named in upper camel case (`TodoList`)"));
352 }
353 let builder_function = Ident::new(&snake_case(&name_text), name.span());
354 let builder_type = format_ident!("{}Builder", name);
355 let visibility = function.vis.clone();
356
357 let mut params = Vec::new();
358 for input in function.sig.inputs.iter_mut() {
359 let FnArg::Typed(typed) = input else {
360 return Err(syn::Error::new(input.span(), "a component is a free function"));
361 };
362 let Pat::Ident(pattern) = &*typed.pat else {
363 return Err(syn::Error::new(typed.pat.span(), "a parameter of a component is a name"));
364 };
365 let mut default = None;
366 let mut kept = Vec::new();
367 for attr in typed.attrs.drain(..) {
368 if attr.path().is_ident("prop") {
369 attr.parse_nested_meta(|meta| {
370 if meta.path.is_ident("default") {
371 if meta.input.peek(Token![=]) {
372 let value: Expr = meta.value()?.parse()?;
373 default = Some(value.to_token_stream());
374 } else {
375 default = Some(quote! { ::std::default::Default::default() });
376 }
377 Ok(())
378 } else {
379 Err(meta.error("expected `default` or `default = ...`"))
380 }
381 })?;
382 } else {
383 kept.push(attr);
384 }
385 }
386 typed.attrs = kept.clone();
387 let ty = (*typed.ty).clone();
388 if default.is_none() && is_option(&ty) {
389 default = Some(quote! { ::std::option::Option::None });
390 }
391 params.push(Param { name: pattern.ident.clone(), ty, default, attrs: kept });
392 }
393
394 let fields = params.iter().map(|param| {
395 let (name, ty) = (¶m.name, ¶m.ty);
396 quote! { #name: ::std::option::Option<#ty> }
397 });
398 let setters = params.iter().map(|param| {
399 let (name, ty) = (¶m.name, ¶m.ty);
400 let docs = param.attrs.iter().filter(|attr| attr.path().is_ident("doc"));
401 if name == "children" {
402 quote! {
403 #(#docs)*
404 pub fn children(mut self, children: impl ::ferroui_view::IntoChildren) -> Self {
405 let mut items = match self.children.take() {
406 ::std::option::Option::Some(::ferroui_view::Element::Fragment(items)) => items,
407 _ => ::std::vec::Vec::new(),
408 };
409 items.extend(::ferroui_view::IntoChildren::into_children(children));
410 self.children = ::std::option::Option::Some(::ferroui_view::Element::Fragment(items));
411 self
412 }
413
414 pub fn child(mut self, child: impl ::ferroui_view::IntoChild) -> Self {
416 let mut items = match self.children.take() {
417 ::std::option::Option::Some(::ferroui_view::Element::Fragment(items)) => items,
418 _ => ::std::vec::Vec::new(),
419 };
420 items.push(::ferroui_view::IntoChild::into_child(child));
421 self.children = ::std::option::Option::Some(::ferroui_view::Element::Fragment(items));
422 self
423 }
424 }
425 } else {
426 quote! {
427 #(#docs)*
428 pub fn #name(mut self, value: impl ::ferroui_view::IntoValue<#ty>) -> Self {
429 self.#name = ::std::option::Option::Some(::ferroui_view::IntoValue::into_value(value));
430 self
431 }
432 }
433 }
434 });
435 let arguments = params.iter().map(|param| {
436 let name = ¶m.name;
437 let message = format!("`{}` is a required property of `{}`", name, name_text);
438 match ¶m.default {
439 Some(default) => quote! { self.#name.unwrap_or_else(|| #default) },
440 None if name == "children" => quote! { self.#name.unwrap_or_default() },
441 None => quote! { self.#name.unwrap_or_else(|| panic!(#message)) },
442 }
443 });
444 let field_names: Vec<&Ident> = params.iter().map(|param| ¶m.name).collect();
445 let doc = format!("The builder of the component [`{name_text}`].");
446 let start_doc = format!("Starts an element of the component [`{name_text}`].");
447 let type_doc = format!("The component [`{name_text}`] as the name of an element of `rsx!`.");
448
449 function.attrs.push(syn::parse_quote!(#[allow(non_snake_case)]));
450 Ok(quote! {
451 #function
452
453 #[doc = #doc]
454 #[allow(non_camel_case_types)]
455 #visibility struct #builder_type {
456 #(#fields,)*
457 key: ::std::option::Option<::ferroui_view::Key>,
458 }
459
460 #[doc = #type_doc]
461 #[allow(non_camel_case_types)]
462 #visibility struct #name {}
463
464 impl ::ferroui_view::Builder for #name {
465 type Output = #builder_type;
466 fn builder() -> #builder_type {
467 #builder_function()
468 }
469 }
470
471 #[doc = #start_doc]
472 #visibility fn #builder_function() -> #builder_type {
473 #builder_type { #(#field_names: ::std::option::Option::None,)* key: ::std::option::Option::None }
474 }
475
476 impl #builder_type {
477 #(#setters)*
478
479 pub fn key(mut self, key: impl ::std::convert::Into<::ferroui_view::Key>) -> Self {
481 self.key = ::std::option::Option::Some(key.into());
482 self
483 }
484 }
485
486 impl ::ferroui_view::IntoElement for #builder_type {
487 fn into_element(self) -> ::ferroui_view::Element {
488 let element = #name(#(#arguments),*);
489 match self.key {
490 ::std::option::Option::Some(key) => ::ferroui_view::element::with_key(element, key),
491 ::std::option::Option::None => element,
492 }
493 }
494 }
495 })
496}