1use 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#[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
22enum ViewNode {
24 Element(ElementNode),
26 Text(String),
28 Expr(Expr),
30}
31
32struct 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 input.parse::<Token![<]>()?;
49 let tag: Ident = input.parse()?;
50
51 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 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 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 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 let lit: LitStr = input.parse()?;
109 Ok(ViewNode::Text(lit.value()))
110 } else if input.peek(syn::token::Brace) {
111 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 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 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 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 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 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 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 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 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 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 let mut builder_calls = Vec::new();
218
219 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 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 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 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 let mut builder_calls = Vec::new();
268
269 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 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 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 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 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 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 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 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 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 let known_elements = [
375 "Text", "StyledText", "VStack", "HStack", "Box", "Spacer",
376 "Button", "List", "TextInput", "TextArea", "Checkbox", "Modal",
377 ];
378
379 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}