Skip to main content

telex_macro/
lib.rs

1//! Procedural macros for Telex.
2//!
3//! The `view!` macro provides JSX-like syntax for building UI trees.
4
5use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use quote::quote;
8use syn::{
9    braced,
10    parse::{Parse, ParseStream},
11    parse_macro_input, Expr, Ident, LitStr, Result, Token,
12};
13
14/// The view! macro for building UI trees with JSX-like syntax.
15#[proc_macro]
16pub fn view(input: TokenStream) -> TokenStream {
17    let node = parse_macro_input!(input as ViewNode);
18    let expanded = node.to_tokens();
19    TokenStream::from(expanded)
20}
21
22/// A node in the view tree (during parsing).
23enum ViewNode {
24    /// An element like <Text>...</Text>
25    Element(ElementNode),
26    /// A string literal "Hello"
27    Text(String),
28    /// An expression in braces {expr}
29    Expr(Expr),
30}
31
32/// A prop like on_press={...} or selected={...}
33struct Prop {
34    name: Ident,
35    value: Expr,
36}
37
38struct ElementNode {
39    tag: String,
40    props: Vec<Prop>,
41    children: Vec<ViewNode>,
42}
43
44impl Parse for ViewNode {
45    fn parse(input: ParseStream) -> Result<Self> {
46        if input.peek(Token![<]) {
47            // Parse element: <Tag prop={val}>...</Tag>
48            input.parse::<Token![<]>()?;
49            let tag: Ident = input.parse()?;
50
51            // Parse props
52            let mut props = Vec::new();
53            while !input.peek(Token![>]) && !input.peek(Token![/]) {
54                let name: Ident = input.parse()?;
55                input.parse::<Token![=]>()?;
56                let content;
57                braced!(content in input);
58                let value: Expr = content.parse()?;
59                props.push(Prop { name, value });
60            }
61
62            // Check for self-closing tag: <Tag />
63            if input.peek(Token![/]) {
64                input.parse::<Token![/]>()?;
65                input.parse::<Token![>]>()?;
66                return Ok(ViewNode::Element(ElementNode {
67                    tag: tag.to_string(),
68                    props,
69                    children: Vec::new(),
70                }));
71            }
72
73            input.parse::<Token![>]>()?;
74
75            let mut children = Vec::new();
76
77            // Parse children until we hit the closing tag
78            while !(input.peek(Token![<]) && input.peek2(Token![/])) {
79                if input.is_empty() {
80                    return Err(syn::Error::new(tag.span(), format!("Unclosed tag: <{}>", tag)));
81                }
82                children.push(input.parse()?);
83            }
84
85            // Parse closing tag: </Tag>
86            input.parse::<Token![<]>()?;
87            input.parse::<Token![/]>()?;
88            let close_tag: Ident = input.parse()?;
89            input.parse::<Token![>]>()?;
90
91            if tag != close_tag {
92                return Err(syn::Error::new(
93                    close_tag.span(),
94                    format!(
95                        "Mismatched tags: expected </{}>, found </{}>",
96                        tag, close_tag
97                    ),
98                ));
99            }
100
101            Ok(ViewNode::Element(ElementNode {
102                tag: tag.to_string(),
103                props,
104                children,
105            }))
106        } else if input.peek(LitStr) {
107            // Parse string literal: "Hello"
108            let lit: LitStr = input.parse()?;
109            Ok(ViewNode::Text(lit.value()))
110        } else if input.peek(syn::token::Brace) {
111            // Parse expression: {expr}
112            let content;
113            braced!(content in input);
114            let expr: Expr = content.parse()?;
115            Ok(ViewNode::Expr(expr))
116        } else {
117            Err(input.error("Expected <Element>, \"string literal\", or {expression}"))
118        }
119    }
120}
121
122impl ViewNode {
123    fn to_tokens(&self) -> TokenStream2 {
124        match self {
125            ViewNode::Text(s) => {
126                quote! { telex::View::text(#s) }
127            }
128            ViewNode::Expr(expr) => {
129                // Convert expression to string for text
130                quote! { telex::View::text(format!("{}", #expr)) }
131            }
132            ViewNode::Element(elem) => elem.to_tokens(),
133        }
134    }
135}
136
137impl ElementNode {
138    fn to_tokens(&self) -> TokenStream2 {
139        match self.tag.as_str() {
140            "Text" => {
141                // <Text>"content"</Text> or <Text>{expr}</Text>
142                if let Some(child) = self.children.first() {
143                    match child {
144                        ViewNode::Text(content) => quote! { telex::View::text(#content) },
145                        ViewNode::Expr(expr) => quote! { telex::View::text(format!("{}", #expr)) },
146                        _ => quote! { telex::View::text("") },
147                    }
148                } else {
149                    quote! { telex::View::text("") }
150                }
151            }
152            "VStack" => {
153                let mut builder_calls = Vec::new();
154
155                // Handle props (spacing)
156                for prop in &self.props {
157                    let name = &prop.name;
158                    let value = &prop.value;
159                    builder_calls.push(quote! { .#name(#value) });
160                }
161
162                // Handle children
163                for child in &self.children {
164                    let tokens = child.to_tokens();
165                    builder_calls.push(quote! { .child(#tokens) });
166                }
167
168                quote! { telex::View::vstack()#(#builder_calls)*.build() }
169            }
170            "HStack" => {
171                let mut builder_calls = Vec::new();
172
173                // Handle props (spacing)
174                for prop in &self.props {
175                    let name = &prop.name;
176                    let value = &prop.value;
177                    builder_calls.push(quote! { .#name(#value) });
178                }
179
180                // Handle children
181                for child in &self.children {
182                    let tokens = child.to_tokens();
183                    builder_calls.push(quote! { .child(#tokens) });
184                }
185
186                quote! { telex::View::hstack()#(#builder_calls)*.build() }
187            }
188            "Box" => {
189                let mut builder_calls = Vec::new();
190
191                // Handle props (border, padding, flex)
192                for prop in &self.props {
193                    let name = &prop.name;
194                    let value = &prop.value;
195                    builder_calls.push(quote! { .#name(#value) });
196                }
197
198                // Handle single child
199                if let Some(child) = self.children.first() {
200                    let tokens = child.to_tokens();
201                    builder_calls.push(quote! { .child(#tokens) });
202                }
203
204                quote! { telex::View::boxed()#(#builder_calls)*.build() }
205            }
206            "Spacer" => {
207                // Spacer with optional flex prop
208                if let Some(prop) = self.props.iter().find(|p| p.name == "flex") {
209                    let value = &prop.value;
210                    quote! { telex::View::spacer_flex(#value) }
211                } else {
212                    quote! { telex::View::spacer() }
213                }
214            }
215            "Button" => {
216                // Parse props and children for Button
217                let mut builder_calls = Vec::new();
218
219                // Handle props
220                for prop in &self.props {
221                    let name = &prop.name;
222                    let value = &prop.value;
223                    builder_calls.push(quote! { .#name(#value) });
224                }
225
226                // Handle label from children
227                if let Some(child) = self.children.first() {
228                    match child {
229                        ViewNode::Text(label) => {
230                            builder_calls.push(quote! { .label(#label) });
231                        }
232                        ViewNode::Expr(expr) => {
233                            builder_calls.push(quote! { .label(format!("{}", #expr)) });
234                        }
235                        _ => {}
236                    }
237                }
238
239                quote! { telex::View::button()#(#builder_calls)*.build() }
240            }
241            "List" => {
242                // Parse props for List: items, selected, on_select
243                let mut builder_calls = Vec::new();
244
245                for prop in &self.props {
246                    let name = &prop.name;
247                    let value = &prop.value;
248                    builder_calls.push(quote! { .#name(#value) });
249                }
250
251                quote! { telex::View::list()#(#builder_calls)*.build() }
252            }
253            "TextInput" => {
254                // Parse props for TextInput: value, placeholder, on_change
255                let mut builder_calls = Vec::new();
256
257                for prop in &self.props {
258                    let name = &prop.name;
259                    let value = &prop.value;
260                    builder_calls.push(quote! { .#name(#value) });
261                }
262
263                quote! { telex::View::text_input()#(#builder_calls)*.build() }
264            }
265            "Checkbox" => {
266                // Parse props and children for Checkbox: checked, on_toggle
267                let mut builder_calls = Vec::new();
268
269                // Handle props
270                for prop in &self.props {
271                    let name = &prop.name;
272                    let value = &prop.value;
273                    builder_calls.push(quote! { .#name(#value) });
274                }
275
276                // Handle label from children
277                if let Some(child) = self.children.first() {
278                    match child {
279                        ViewNode::Text(label) => {
280                            builder_calls.push(quote! { .label(#label) });
281                        }
282                        ViewNode::Expr(expr) => {
283                            builder_calls.push(quote! { .label(format!("{}", #expr)) });
284                        }
285                        _ => {}
286                    }
287                }
288
289                quote! { telex::View::checkbox()#(#builder_calls)*.build() }
290            }
291            "TextArea" => {
292                // Parse props for TextArea: value, placeholder, rows, cursor_line, cursor_col, on_change
293                let mut builder_calls = Vec::new();
294
295                for prop in &self.props {
296                    let name = &prop.name;
297                    let value = &prop.value;
298                    builder_calls.push(quote! { .#name(#value) });
299                }
300
301                quote! { telex::View::text_area()#(#builder_calls)*.build() }
302            }
303            "Modal" => {
304                // Parse props for Modal: visible, title, width, height, on_dismiss
305                let mut builder_calls = Vec::new();
306
307                for prop in &self.props {
308                    let name = &prop.name;
309                    let value = &prop.value;
310                    builder_calls.push(quote! { .#name(#value) });
311                }
312
313                // Handle single child
314                if let Some(child) = self.children.first() {
315                    let tokens = child.to_tokens();
316                    builder_calls.push(quote! { .child(#tokens) });
317                }
318
319                quote! { telex::View::modal()#(#builder_calls)*.build() }
320            }
321            "StyledText" => {
322                // Parse props for styled text: bold, italic, underline, dim, color, bg
323                let mut content = quote! { "" };
324                let mut bold_val = quote! { false };
325                let mut italic_val = quote! { false };
326                let mut underline_val = quote! { false };
327                let mut dim_val = quote! { false };
328                let mut color_call = quote! {};
329                let mut bg_call = quote! {};
330
331                // Handle props
332                for prop in &self.props {
333                    let name_str = prop.name.to_string();
334                    let value = &prop.value;
335
336                    match name_str.as_str() {
337                        "bold" => bold_val = quote! { #value },
338                        "italic" => italic_val = quote! { #value },
339                        "underline" => underline_val = quote! { #value },
340                        "dim" => dim_val = quote! { #value },
341                        "color" => color_call = quote! { .color(#value) },
342                        "bg" => bg_call = quote! { .bg(#value) },
343                        _ => {}
344                    }
345                }
346
347                // Handle text content from children
348                if let Some(child) = self.children.first() {
349                    match child {
350                        ViewNode::Text(text) => {
351                            content = quote! { #text };
352                        }
353                        ViewNode::Expr(expr) => {
354                            content = quote! { format!("{}", #expr) };
355                        }
356                        _ => {}
357                    }
358                }
359
360                // Generate conditional builder chain
361                quote! {
362                    {
363                        let __builder = telex::View::styled_text(#content);
364                        let __builder = if #bold_val { __builder.bold() } else { __builder };
365                        let __builder = if #italic_val { __builder.italic() } else { __builder };
366                        let __builder = if #underline_val { __builder.underline() } else { __builder };
367                        let __builder = if #dim_val { __builder.dim() } else { __builder };
368                        __builder #color_call #bg_call .build()
369                    }
370                }
371            }
372            unknown => {
373                // Provide helpful error with suggestions
374                let known_elements = [
375                    "Text", "StyledText", "VStack", "HStack", "Box", "Spacer",
376                    "Button", "List", "TextInput", "TextArea", "Checkbox", "Modal",
377                ];
378
379                // Find similar element names (simple edit distance check)
380                let suggestion = known_elements.iter()
381                    .filter(|&e| {
382                        let e_lower = e.to_lowercase();
383                        let u_lower = unknown.to_lowercase();
384                        e_lower.starts_with(&u_lower[..1.min(u_lower.len())]) ||
385                        u_lower.starts_with(&e_lower[..1.min(e_lower.len())]) ||
386                        e_lower.contains(&u_lower) ||
387                        u_lower.contains(&e_lower)
388                    })
389                    .next();
390
391                let msg = if let Some(suggested) = suggestion {
392                    format!(
393                        "Unknown element: <{}>. Did you mean <{}>?\n\nAvailable elements: {}",
394                        unknown, suggested, known_elements.join(", ")
395                    )
396                } else {
397                    format!(
398                        "Unknown element: <{}>.\n\nAvailable elements: {}",
399                        unknown, known_elements.join(", ")
400                    )
401                };
402                quote! { compile_error!(#msg) }
403            }
404        }
405    }
406}