nxml_rs_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{quote, ToTokens};
4use syn::{
5    braced,
6    parse::{Parse, ParseStream},
7    parse_macro_input,
8    token::Brace,
9    Expr, Ident, LitStr, Result, Token,
10};
11
12enum RefDeref {
13    Ref(Token![&]),
14    Deref(Token![*]),
15    Mut(Token![mut]),
16}
17
18struct RefsDerefs {
19    refs: Vec<RefDeref>,
20}
21
22impl Parse for RefsDerefs {
23    fn parse(input: ParseStream) -> Result<Self> {
24        let mut refs = Vec::new();
25        while !input.is_empty() {
26            if let Some(r) = input.parse()? {
27                refs.push(RefDeref::Ref(r));
28            } else if let Some(r) = input.parse()? {
29                refs.push(RefDeref::Deref(r));
30            } else if let Some(r) = input.parse()? {
31                refs.push(RefDeref::Mut(r));
32            } else {
33                break;
34            }
35        }
36        Ok(RefsDerefs { refs })
37    }
38}
39
40impl ToTokens for RefsDerefs {
41    fn to_tokens(&self, tokens: &mut TokenStream2) {
42        for ref_deref in &self.refs {
43            match ref_deref {
44                RefDeref::Ref(r) => r.to_tokens(tokens),
45                RefDeref::Deref(d) => d.to_tokens(tokens),
46                RefDeref::Mut(m) => m.to_tokens(tokens),
47            }
48        }
49    }
50}
51
52enum NxmlAttr {
53    Literal(Ident, LitStr),
54    Expr(Ident, Expr),
55    Shortcut(RefsDerefs, Ident),
56}
57
58impl Parse for NxmlAttr {
59    fn parse(input: ParseStream) -> Result<Self> {
60        let Some(ident) = input.parse()? else {
61            // capture the return Err from the macro
62            let content = (|| {
63                let content;
64                braced!(content in input);
65                Ok(content)
66            })()
67            .map_err(|_| input.error("expected attribute name or identifier in curly braces"))?;
68
69            return Ok(NxmlAttr::Shortcut(content.parse()?, content.parse()?));
70        };
71
72        input.parse::<Token![=]>()?;
73
74        if let Some(lit) = input.parse()? {
75            return Ok(NxmlAttr::Literal(ident, lit));
76        }
77
78        // and again..
79        let content = (|| {
80            let content;
81            braced!(content in input);
82            Ok(content)
83        })()
84        .map_err(|_| input.error("expected a string literal or an expression in curly braces"))?;
85        Ok(NxmlAttr::Expr(ident, content.parse()?))
86    }
87}
88
89enum TextPart {
90    Static(String),
91    Expr(Expr),
92}
93
94enum NxmlFinish {
95    SelfClosing,
96    Closing {
97        text_content: Vec<TextPart>,
98        children: Vec<NxmlInput>,
99        name: Ident,
100    },
101}
102
103impl Parse for NxmlFinish {
104    fn parse(input: ParseStream) -> Result<Self> {
105        if input.peek(Token![/]) && input.peek2(Token![>]) {
106            input.parse::<Token![/]>()?;
107            input.parse::<Token![>]>()?;
108            return Ok(NxmlFinish::SelfClosing);
109        }
110
111        input.parse::<Token![>]>()?;
112
113        let mut children = Vec::new();
114        let mut text_content = Vec::new();
115
116        while !(input.peek(Token![<]) && input.peek2(Token![/])) {
117            if let Some(lit) = input.parse::<Option<LitStr>>()? {
118                text_content.push(TextPart::Static(lit.value()));
119                continue;
120            }
121            if let Some(ident) = input.parse::<Option<Ident>>()? {
122                text_content.push(TextPart::Static(ident.to_string()));
123                continue;
124            }
125            if input.peek(Brace) {
126                let content;
127                braced!(content in input);
128                text_content.push(TextPart::Expr(content.parse()?));
129                continue;
130            }
131            if input.peek(Token![<]) {
132                children.push(input.parse()?);
133                continue;
134            }
135            return Err(input.error(
136                "expected a string literal, an expression in curly braces or a child element",
137            ));
138        }
139
140        input.parse::<Token![<]>()?;
141        input.parse::<Token![/]>()?;
142
143        let name = input.parse()?;
144
145        input.parse::<Token![>]>()?;
146
147        Ok(Self::Closing {
148            text_content,
149            children,
150            name,
151        })
152    }
153}
154
155struct NxmlInput {
156    name: Ident,
157    attrs: Vec<NxmlAttr>,
158    finish: NxmlFinish,
159}
160
161impl Parse for NxmlInput {
162    fn parse(input: ParseStream) -> Result<Self> {
163        input.parse::<Token![<]>()?;
164
165        let name = input.parse()?;
166
167        Ok(NxmlInput {
168            attrs: {
169                let mut attrs = Vec::new();
170                while !(input.peek(Token![>]) || input.peek(Token![/]) && input.peek2(Token![>])) {
171                    attrs.push(input.parse()?);
172                }
173                attrs
174            },
175            finish: {
176                let finish = input.parse()?;
177                if let NxmlFinish::Closing { name: end_name, .. } = &finish {
178                    // the intellijRulezz thing does not seem to have an appreciable effect
179                    // (trying to bait rust-analyzer to autocomplete the closing tag)
180                    if *end_name != name && end_name != "intellijRulezz" {
181                        let /* mut */ err = syn::Error::new_spanned(
182                            end_name,
183                            "expected closing tag to match opening tag",
184                        );
185                        // this creates a huge mess for some reason
186                        // seems like rust isn't happy with multiple compile_error!s emitted from a
187                        // macro
188                        // err.combine(syn::Error::new_spanned(name, "opening tag here"));
189                        return Err(err);
190                    }
191                }
192                finish
193            },
194            name,
195        })
196    }
197}
198
199fn codegen(input: &NxmlInput, element: TokenStream2) -> TokenStream2 {
200    let name = &input.name;
201
202    let (text_parts, children, end_name) = match &input.finish {
203        NxmlFinish::Closing {
204            text_content,
205            children,
206            name,
207        } => (&text_content[..], &children[..], name),
208        _ => (&[][..], &[][..], name),
209    };
210
211    let mut static_text = String::new();
212    let mut text_exprs = Vec::new();
213    for part in text_parts {
214        if !static_text.is_empty() {
215            static_text.push(' ');
216        }
217        match part {
218            TextPart::Static(str) => static_text.push_str(str),
219            TextPart::Expr(expr) => {
220                static_text.push_str("{}");
221                text_exprs.push(expr);
222            }
223        }
224    }
225
226    let attrs = input.attrs.iter().map(|attr| match attr {
227        NxmlAttr::Literal(ident, value) => quote!(.with_attr(stringify!(#ident), #value)),
228        NxmlAttr::Expr(ident, expr) => quote!(.with_attr(stringify!(#ident), #expr)),
229        NxmlAttr::Shortcut(r, ident) => quote!(.with_attr(stringify!(#ident), #r #ident)),
230    });
231
232    let text_content = if text_exprs.is_empty() {
233        if !static_text.is_empty() {
234            quote!(.with_text(#static_text))
235        } else {
236            quote!()
237        }
238    } else {
239        quote!(.with_text(format!(#static_text, #(#text_exprs),*)))
240    };
241
242    let children = children.iter().map(|child| {
243        let tokens = codegen(child, element.clone());
244        quote!(.with_child(#tokens))
245    });
246
247    quote!({
248        #[allow(non_camel_case_types)]
249        struct #name;
250        // can use rust-analyzer rename action to change the tag in sync, and
251        // goto reference to jump between them
252        // (name and end_name are always same, but we (and RA) care about spans)
253        let _: #name = #end_name;
254
255        ::nxml_rs::#element::new(stringify!(#name))
256            #text_content
257            #(#attrs)*
258            #(#children)*
259    })
260}
261
262/// Creates an [`Element`](struct.Element.html) from an XML-like syntax.
263///
264/// # Example
265/// ```rust
266/// # use nxml_rs::*;
267/// # let outside_var = 42;
268/// # let shortcut_name = "minä";
269/// # let element =
270/// nxml! {
271///     <Entity>
272///         <SomeComponent name="comp" value={outside_var} {shortcut_name} />
273///         <BareTextIsMeh>
274///             bare words "(idents only)" or
275///             "string literals or"
276///             {"exprs"}
277///             "are format!'ed into a single string"
278///             "(when an expr occurs the zerocopy breaks and we have a Cow::Owned)"
279///         </BareTextIsMeh>
280///     </Entity>
281/// };
282///
283/// # assert_eq!(element.to_string(), "<Entity><SomeComponent name=\"comp\" value=\"42\" shortcut_name=\"minä\"/><BareTextIsMeh>bare words (idents only) or string literals or exprs are format!'ed into a single string (when an expr occurs the zerocopy breaks and we have a Cow::Owned)</BareTextIsMeh></Entity>");
284/// ```
285#[proc_macro]
286pub fn nxml(input: TokenStream) -> TokenStream {
287    let input = parse_macro_input!(input as NxmlInput);
288    codegen(&input, quote!(Element)).into()
289}
290
291/// Creates an [`ElementRef`](struct.ElementRef.html) from an
292/// XML-like syntax.
293///
294/// # Examples
295///
296/// With no expressions, the result is `ElementRef<'static>`:
297/// ```rust
298/// # use nxml_rs::*;
299/// # fn assert_static(_: ElementRef<'static>) {}
300/// assert_static(nxml_ref!(<Entity prop="static" />));
301/// ```
302///
303/// The lifetime is narrowed down to the shortest one of given expressions:
304/// ```compile_fail
305/// # use nxml_rs::*;
306/// # fn assert_static(_: ElementRef<'static>) {}
307/// let prop = String::from("value");
308///
309/// let element = nxml_ref!(<Entity {&prop} />); // borrowed value does not live long enough..
310///
311/// assert_static(element); // ..argument requires that `prop` is borrowed for `'static`
312/// ```
313///
314/// And, unlike [`nxml!`](macro.nxml.html), the expressions must be `&str`:
315/// ```compile_fail
316/// # use nxml_rs::*;
317/// let prop = 42;
318/// nxml_ref!(<Entity {prop} />); // expected `&str`, found integer
319#[proc_macro]
320pub fn nxml_ref(input: TokenStream) -> TokenStream {
321    let input = parse_macro_input!(input as NxmlInput);
322    codegen(&input, quote!(ElementRef)).into()
323}
324
325struct NxmlMultiInput {
326    children: Vec<NxmlInput>,
327}
328
329impl Parse for NxmlMultiInput {
330    fn parse(input: ParseStream) -> Result<Self> {
331        let mut children = Vec::new();
332        while !(input.peek(Token![<]) && input.peek2(Token![/]) || input.is_empty()) {
333            children.push(input.parse()?);
334        }
335        Ok(NxmlMultiInput { children })
336    }
337}
338
339/// Creates a list of [`Element`](struct.Element.html) from an
340/// XML-like syntax.
341///
342/// This is equivalent to calling [`nxml!`](macro.nxml.html) multiple times
343/// inside of a `vec!` macro (or doing `nxml!(<root>...</root>).children`).
344/// # Example
345/// ```rust
346/// # use nxml_rs::*;
347/// let elements = nxmls!(<a/><b/><c/>);
348///
349/// assert_eq!(elements.len(), 3);
350/// ```
351#[proc_macro]
352pub fn nxmls(input: TokenStream) -> TokenStream {
353    let input = parse_macro_input!(input as NxmlMultiInput);
354    let items = input
355        .children
356        .iter()
357        .map(|child| codegen(child, quote!(Element)));
358    quote!(vec![#(#items),*]).into()
359}
360
361/// Creates a list of [`ElementRef`](struct.Element.html) from an
362/// XML-like syntax.
363///
364/// This is equivalent to calling [`nxml_ref!`](macro.nxml_ref.html) multiple
365/// times inside of a `vec!` macro (or doing
366/// `nxml_refs!(<root>...</root>).children`).
367/// # Example
368/// ```rust
369/// # use nxml_rs::*;
370/// let elements = nxml_refs!(<a/><b/><c/>);
371///
372/// assert_eq!(elements.len(), 3);
373/// ```
374#[proc_macro]
375pub fn nxml_refs(input: TokenStream) -> TokenStream {
376    let input = parse_macro_input!(input as NxmlMultiInput);
377    let items = input
378        .children
379        .iter()
380        .map(|child| codegen(child, quote!(ElementRef)));
381    quote!(vec![#(#items),*]).into()
382}