rscx_macros/
lib.rs

1use std::collections::HashSet;
2
3use proc_macro::TokenStream;
4use proc_macro2_diagnostics::Diagnostic;
5use quote::{quote, quote_spanned, ToTokens};
6use rstml::{
7    node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName},
8    Parser, ParserConfig,
9};
10use syn::punctuated::Punctuated;
11use syn::{parse::Parse, parse_quote, spanned::Spanned, Expr, ExprLit, FnArg, ItemStruct, Token};
12
13#[proc_macro]
14pub fn html(tokens: TokenStream) -> TokenStream {
15    html_inner(tokens, false)
16}
17
18#[proc_macro]
19pub fn html_ide(tokens: TokenStream) -> TokenStream {
20    html_inner(tokens, true)
21}
22
23fn is_empty_element(name: &str) -> bool {
24    // https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
25    match name {
26        "img" | "input" | "meta" | "link" | "hr" | "br" | "source" | "track" | "wbr" | "area"
27        | "base" | "col" | "embed" | "param" => true,
28        _ => false,
29    }
30}
31
32fn empty_elements_set() -> HashSet<&'static str> {
33    [
34        "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
35        "source", "track", "wbr",
36    ]
37    .into_iter()
38    .collect()
39}
40
41fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream {
42    let config = ParserConfig::new()
43        .recover_block(true)
44        .element_close_use_default_wildcard_ident(true)
45        .always_self_closed_elements(empty_elements_set());
46
47    let parser = Parser::new(config);
48    let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
49    process_nodes(ide_helper, &nodes, errors).into()
50}
51
52fn process_nodes<'n>(
53    ide_helper: bool,
54    nodes: &'n Vec<Node>,
55    errors: Vec<Diagnostic>,
56) -> proc_macro2::TokenStream {
57    let WalkNodesOutput {
58        static_format: html_string,
59        values,
60        collected_elements: elements,
61        diagnostics,
62    } = walk_nodes(&nodes);
63    let docs = if ide_helper {
64        generate_tags_docs(elements)
65    } else {
66        vec![]
67    };
68    let errors = errors
69        .into_iter()
70        .map(|e| e.emit_as_expr_tokens())
71        .chain(diagnostics);
72    quote! {
73        {
74            // Make sure that "compile_error!(..);"  can be used in this context.
75            #(#errors;)*
76            // Make sure that "enum x{};" and "let _x = crate::element;"  can be used in this context
77            #(#docs;)*
78            format!(#html_string, #(rscx::FormatWrapper::new(#values)),*)
79        }
80    }
81}
82
83fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
84    // Mark some of elements as type,
85    // and other as elements as fn in crate::docs,
86    // to give an example how to link tag with docs.
87    let elements_as_type: HashSet<&'static str> =
88        vec!["html", "head", "meta", "link", "body", "div"]
89            .into_iter()
90            .collect();
91
92    elements
93        .into_iter()
94        .map(|e| {
95            if elements_as_type.contains(&*e.to_string()) {
96                let element = quote_spanned!(e.span() => enum);
97                quote!({#element X{}})
98            } else {
99                // let _ = crate::docs::element;
100                let element = quote_spanned!(e.span() => element);
101                quote!(let _ = crate::docs::#element)
102            }
103        })
104        .collect()
105}
106
107#[derive(Default)]
108struct WalkNodesOutput<'a> {
109    static_format: String,
110    // Use proc_macro2::TokenStream instead of syn::Expr
111    // to provide more errors to the end user.
112    values: Vec<proc_macro2::TokenStream>,
113    // Additional diagnostic messages.
114    diagnostics: Vec<proc_macro2::TokenStream>,
115    // Collect elements to provide semantic highlight based on element tag.
116    // No differences between open tag and closed tag.
117    // Also multiple tags with same name can be present,
118    // because we need to mark each of them.
119    collected_elements: Vec<&'a NodeName>,
120}
121impl<'a> WalkNodesOutput<'a> {
122    fn extend(&mut self, other: WalkNodesOutput<'a>) {
123        self.static_format.push_str(&other.static_format);
124        self.values.extend(other.values);
125        self.diagnostics.extend(other.diagnostics);
126        self.collected_elements.extend(other.collected_elements);
127    }
128}
129
130fn walk_nodes<'a>(nodes: &'a Vec<Node>) -> WalkNodesOutput<'a> {
131    let mut out = WalkNodesOutput::default();
132
133    for node in nodes {
134        match node {
135            Node::Doctype(doctype) => {
136                let value = &doctype.value.to_token_stream_string();
137                out.static_format.push_str(&format!("<!DOCTYPE {}>", value));
138            }
139            Node::Element(element) => {
140                let name = element.name().to_string();
141
142                if !is_component_tag_name(&name) {
143                    match element.name() {
144                        NodeName::Block(block) => {
145                            out.static_format.push_str("<{}");
146                            out.values.push(block.to_token_stream());
147                        }
148                        _ => {
149                            out.static_format.push_str(&format!("<{}", name));
150                            out.collected_elements.push(&element.open_tag.name);
151                            if let Some(e) = &element.close_tag {
152                                out.collected_elements.push(&e.name)
153                            }
154                        }
155                    }
156
157                    // attributes
158                    for attribute in element.attributes() {
159                        match attribute {
160                            NodeAttribute::Block(block) => {
161                                // If the nodes parent is an attribute we prefix with whitespace
162                                out.static_format.push(' ');
163                                out.static_format.push_str("{}");
164                                out.values.push(block.to_token_stream());
165                            }
166                            NodeAttribute::Attribute(attribute) => {
167                                let (static_format, value) = walk_attribute(attribute);
168                                out.static_format.push_str(&static_format);
169                                if let Some(value) = value {
170                                    out.values.push(value);
171                                }
172                            }
173                        }
174                    }
175                    // Ignore childs of special Empty elements
176                    if is_empty_element(element.open_tag.name.to_string().as_str()) {
177                        out.static_format.push_str(" />");
178                        if !element.children.is_empty() {
179                            let warning = proc_macro2_diagnostics::Diagnostic::spanned(
180                                element.open_tag.name.span(),
181                                proc_macro2_diagnostics::Level::Warning,
182                                "Element is processed as empty, and cannot have any child",
183                            );
184                            out.diagnostics.push(warning.emit_as_expr_tokens())
185                        }
186
187                        continue;
188                    }
189                    out.static_format.push('>');
190
191                    // children
192                    let other_output = walk_nodes(&element.children);
193                    out.extend(other_output);
194
195                    match element.name() {
196                        NodeName::Block(block) => {
197                            out.static_format.push_str("</{}>");
198                            out.values.push(block.to_token_stream());
199                        }
200                        _ => {
201                            out.static_format.push_str(&format!("</{}>", name));
202                        }
203                    }
204                } else {
205                    // custom elements
206                    out.static_format.push_str("{}");
207                    out.values
208                        .push(CustomElement::new(element).to_token_stream());
209                }
210            }
211            Node::Text(text) => {
212                out.static_format.push_str(&text.value_string());
213            }
214            Node::RawText(text) => {
215                out.static_format.push_str(&text.to_string_best());
216            }
217            Node::Fragment(fragment) => {
218                let other_output = walk_nodes(&fragment.children);
219                out.extend(other_output)
220            }
221            Node::Comment(comment) => {
222                out.static_format.push_str("<!-- {} -->");
223                out.values.push(comment.value.to_token_stream());
224            }
225            Node::Block(block) => {
226                let block = block.try_block().unwrap();
227                let stmts = &block.stmts;
228                out.static_format.push_str("{}");
229                out.values.push(quote!(#(#stmts)*));
230            }
231        }
232    }
233
234    out
235}
236
237fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option<proc_macro2::TokenStream>) {
238    let mut static_format = String::new();
239    let mut format_value = None;
240    let key = match attribute.key.to_string().as_str() {
241        "as_" => "as".to_string(),
242        _ => attribute.key.to_string(),
243    };
244    static_format.push_str(&format!(" {}", key));
245
246    match attribute.value() {
247        Some(Expr::Lit(ExprLit {
248            lit: syn::Lit::Str(value),
249            ..
250        })) => {
251            static_format.push_str(&format!(
252                r#"="{}""#,
253                html_escape::encode_unquoted_attribute(&value.value())
254            ));
255        }
256        Some(Expr::Lit(ExprLit {
257            lit: syn::Lit::Bool(value),
258            ..
259        })) => {
260            static_format.push_str(&format!(r#"="{}""#, value.value()));
261        }
262        Some(Expr::Lit(ExprLit {
263            lit: syn::Lit::Int(value),
264            ..
265        })) => {
266            static_format.push_str(&format!(r#"="{}""#, value.token()));
267        }
268        Some(Expr::Lit(ExprLit {
269            lit: syn::Lit::Float(value),
270            ..
271        })) => {
272            static_format.push_str(&format!(r#"="{}""#, value.token()));
273        }
274        Some(value) => {
275            static_format.push_str(r#"="{}""#);
276            format_value = Some(
277                quote! {{
278                    // (#value).escape_attribute()
279                    ::rscx::EscapeAttribute::escape_attribute(&#value)
280                }}
281                .into_token_stream(),
282            );
283        }
284        None => {}
285    }
286
287    (static_format, format_value)
288}
289
290fn is_component_tag_name(name: &str) -> bool {
291    name.starts_with(|c: char| c.is_ascii_uppercase())
292}
293
294struct CustomElement<'e> {
295    e: &'e NodeElement,
296}
297
298impl<'e> CustomElement<'e> {
299    fn new(e: &'e NodeElement) -> Self {
300        CustomElement { e }
301    }
302}
303
304impl<'e> ToTokens for CustomElement<'_> {
305    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
306        let name = self.e.name();
307
308        let mut chain = vec![quote! {
309            ::rscx::props::props_builder(&#name)
310        }];
311
312        let children = &self.e.children;
313        if !children.is_empty() {
314            let c = process_nodes(false, children, vec![]);
315            chain.push(quote! { .children(#c) });
316        }
317
318        chain.push({
319            self.e
320                .attributes()
321                .iter()
322                .map(|a| match a {
323                    NodeAttribute::Block(block) => {
324                        quote! {
325                            .push_attr(
326                                #[allow(unused_braces)]
327                                #block
328                            )
329                        }
330                    }
331                    NodeAttribute::Attribute(attribute) => {
332                        let key = &attribute.key;
333                        let value = attribute.value().unwrap();
334                        quote! { .#key(#value) }
335                    }
336                })
337                .collect::<proc_macro2::TokenStream>()
338        });
339
340        chain.push(quote! { .build() });
341
342        tokens.extend(quote! {
343            #name(#(#chain)*).await
344        });
345    }
346}
347
348#[proc_macro_attribute]
349pub fn props(_attr: TokenStream, input: TokenStream) -> TokenStream {
350    let props = syn::parse_macro_input!(input as PropsStruct);
351    quote! { #props }.to_token_stream().into()
352}
353
354struct PropsStruct {
355    name: syn::Ident,
356    item: ItemStruct,
357}
358
359impl Parse for PropsStruct {
360    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
361        let item = input.parse::<ItemStruct>()?;
362        let name = item.ident.clone();
363
364        Ok(PropsStruct { name, item })
365    }
366}
367
368impl ToTokens for PropsStruct {
369    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
370        let name = &self.name;
371        let item = &self.item;
372
373        let builder_name =
374            syn::Ident::new(&format!("{}Builder", name), proc_macro2::Span::call_site());
375
376        tokens.extend(quote! {
377            #[derive(::rscx::typed_builder::TypedBuilder)]
378            #[builder(doc, crate_module_path=::rscx::typed_builder)]
379            #item
380
381            impl ::rscx::props::Props for #name {
382                type Builder = #builder_name;
383                fn builder() -> Self::Builder {
384                    #name::builder()
385                }
386            }
387        });
388
389        let has_attributes = item
390            .fields
391            .iter()
392            .any(|field| field.ident.as_ref().unwrap().to_string() == "attributes");
393
394        if has_attributes {
395            tokens.extend(quote! {
396                impl #builder_name {
397                    pub fn push_attr<A: std::fmt::Display>(mut self, attr: A) -> Self {
398                        self.props.attributes.push_str(&format!("{} ", attr));
399                        self
400                    }
401                }
402            });
403        }
404    }
405}
406
407#[proc_macro_attribute]
408pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream {
409    let comp = syn::parse_macro_input!(input as ComponentFn);
410    quote! { #comp }.to_token_stream().into()
411}
412
413struct ComponentFn {
414    item: syn::ItemFn,
415}
416
417impl Parse for ComponentFn {
418    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
419        let item = input.parse::<syn::ItemFn>()?;
420        Ok(ComponentFn { item })
421    }
422}
423
424impl ToTokens for ComponentFn {
425    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
426        let item = &self.item;
427        let name = &item.sig.ident;
428
429        let (defs, args) = match item.sig.inputs.len() {
430            0 => {
431                // generate empty props
432                let props_name =
433                    syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
434                (
435                    quote! {
436                        #[props]
437                        pub struct #props_name{}
438                    },
439                    quote! { _props: #props_name },
440                )
441            }
442            // match if there is a single arg of type #nameProps
443            1 if matches!(item.sig.inputs.first().unwrap(), syn::FnArg::Typed(arg) if matches!(arg.ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string() == format!("{}Props", name))) =>
444            {
445                let props = item.sig.inputs.first().unwrap();
446                (quote! {}, props.to_token_stream())
447            }
448            _ => {
449                let field_defs = &item
450                    .sig
451                    .inputs
452                    .clone()
453                    .into_iter()
454                    .map(|i| match i {
455                        FnArg::Receiver(_) => {
456                            panic!("receiver arguments unsupported");
457                        }
458                        FnArg::Typed(mut t) => {
459                            if t.attrs.is_empty() {
460                                t.attrs.push(parse_quote! { #[builder(setter(into))] });
461                            }
462
463                            t
464                        }
465                    })
466                    .collect::<Punctuated<_, Token![,]>>();
467                let field_names = item
468                    .sig
469                    .inputs
470                    .iter()
471                    .map(|i| match i {
472                        FnArg::Receiver(_) => {
473                            panic!("receiver arguments unsupported");
474                        }
475                        FnArg::Typed(t) => &t.pat,
476                    })
477                    .collect::<Punctuated<_, Token![,]>>();
478                let props_name =
479                    syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
480
481                (
482                    quote! {
483                        #[rscx::props]
484                        pub struct #props_name {
485                            #field_defs
486                        }
487                    },
488                    quote! { #props_name { #field_names }: #props_name },
489                )
490            }
491        };
492
493        let body = &item.block;
494        let output = &item.sig.output;
495        let vis = &item.vis;
496
497        tokens.extend(quote! {
498            #defs
499            #[allow(non_snake_case)]
500            #vis async fn #name(#args) #output {
501                #body
502            }
503        });
504    }
505}