dioxus_rsx/
element.rs

1use crate::innerlude::*;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use proc_macro2_diagnostics::SpanDiagnosticExt;
4use quote::{quote, ToTokens, TokenStreamExt};
5use std::fmt::{Display, Formatter};
6use syn::{
7    parse::{Parse, ParseStream},
8    punctuated::Punctuated,
9    spanned::Spanned,
10    token::Brace,
11    Ident, LitStr, Result, Token,
12};
13
14/// Parse the VNode::Element type
15#[derive(PartialEq, Eq, Clone, Debug)]
16pub struct Element {
17    /// div { } -> div
18    pub name: ElementName,
19
20    /// The actual attributes that were parsed
21    pub raw_attributes: Vec<Attribute>,
22
23    /// The attributes after merging - basically the formatted version of the combined attributes
24    /// where possible.
25    ///
26    /// These are the actual attributes that get rendered out
27    pub merged_attributes: Vec<Attribute>,
28
29    /// The `...` spread attributes.
30    pub spreads: Vec<Spread>,
31
32    // /// Elements can have multiple, unlike components which can only have one
33    // pub spreads: Vec<Spread>,
34    /// The children of the element
35    pub children: Vec<BodyNode>,
36
37    /// the brace of the `div { }`
38    pub brace: Option<Brace>,
39
40    /// A list of diagnostics that were generated during parsing. This element might be a valid rsx_block
41    /// but not technically a valid element - these diagnostics tell us what's wrong and then are used
42    /// when rendering
43    pub diagnostics: Diagnostics,
44}
45
46impl Parse for Element {
47    fn parse(stream: ParseStream) -> Result<Self> {
48        let name = stream.parse::<ElementName>()?;
49
50        // We very liberally parse elements - they might not even have a brace!
51        // This is designed such that we can throw a compile error but still give autocomplete
52        // ... partial completions mean we do some weird parsing to get the right completions
53        let mut brace = None;
54        let mut block = RsxBlock::default();
55
56        match stream.peek(Brace) {
57            // If the element is followed by a brace, it is complete. Parse the body
58            true => {
59                block = stream.parse::<RsxBlock>()?;
60                brace = Some(block.brace);
61            }
62
63            // Otherwise, it is incomplete. Add a diagnostic
64            false => block.diagnostics.push(
65                name.span()
66                    .error("Elements must be followed by braces")
67                    .help("Did you forget a brace?"),
68            ),
69        }
70
71        // Make sure these attributes have an el_name set for completions and Template generation
72        for attr in block.attributes.iter_mut() {
73            attr.el_name = Some(name.clone());
74        }
75
76        // Assemble the new element from the contents of the block
77        let mut element = Element {
78            brace,
79            name: name.clone(),
80            raw_attributes: block.attributes,
81            children: block.children,
82            diagnostics: block.diagnostics,
83            spreads: block.spreads.clone(),
84            merged_attributes: Vec::new(),
85        };
86
87        // And then merge the various attributes together
88        // The original raw_attributes are kept for lossless parsing used by hotreload/autofmt
89        element.merge_attributes();
90
91        // And then merge the spreads *after* the attributes are merged. This ensures walking the
92        // merged attributes in path order stops before we hit the spreads, but spreads are still
93        // counted as dynamic attributes
94        for spread in block.spreads.iter() {
95            element.merged_attributes.push(Attribute {
96                name: AttributeName::Spread(spread.dots),
97                colon: None,
98                value: AttributeValue::AttrExpr(PartialExpr::from_expr(&spread.expr)),
99                comma: spread.comma,
100                dyn_idx: spread.dyn_idx.clone(),
101                el_name: Some(name.clone()),
102            });
103        }
104
105        Ok(element)
106    }
107}
108
109impl ToTokens for Element {
110    fn to_tokens(&self, tokens: &mut TokenStream2) {
111        let el = self;
112        let el_name = &el.name;
113
114        let ns = |name| match el_name {
115            ElementName::Ident(i) => quote! { dioxus_elements::#i::#name },
116            ElementName::Custom(_) => quote! { None },
117        };
118
119        let static_attrs = el
120            .merged_attributes
121            .iter()
122            .map(|attr| {
123                // Rendering static attributes requires a bit more work than just a dynamic attrs
124                // Early return for dynamic attributes
125                let Some((name, value)) = attr.as_static_str_literal() else {
126                    let id = attr.dyn_idx.get();
127                    return quote! { dioxus_core::TemplateAttribute::Dynamic { id: #id  } };
128                };
129
130                let ns = match name {
131                    AttributeName::BuiltIn(name) => ns(quote!(#name.1)),
132                    AttributeName::Custom(_) => quote!(None),
133                    AttributeName::Spread(_) => {
134                        unreachable!("spread attributes should not be static")
135                    }
136                };
137
138                let name = match (el_name, name) {
139                    (ElementName::Ident(_), AttributeName::BuiltIn(_)) => {
140                        quote! { dioxus_elements::#el_name::#name.0 }
141                    }
142                    //hmmmm I think we could just totokens this, but the to_string might be inserting quotes
143                    _ => {
144                        let as_string = name.to_string();
145                        quote! { #as_string }
146                    }
147                };
148
149                let value = value.to_static().unwrap();
150
151                quote! {
152                    dioxus_core::TemplateAttribute::Static {
153                        name: #name,
154                        namespace: #ns,
155                        value: #value,
156                    }
157                }
158            })
159            .collect::<Vec<_>>();
160
161        // Render either the child
162        let children = el.children.iter().map(|c| match c {
163            BodyNode::Element(el) => quote! { #el },
164            BodyNode::Text(text) if text.is_static() => {
165                let text = text.input.to_static().unwrap();
166                quote! { dioxus_core::TemplateNode::Text { text: #text } }
167            }
168            BodyNode::Text(text) => {
169                let id = text.dyn_idx.get();
170                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
171            }
172            BodyNode::ForLoop(floop) => {
173                let id = floop.dyn_idx.get();
174                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
175            }
176            BodyNode::RawExpr(exp) => {
177                let id = exp.dyn_idx.get();
178                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
179            }
180            BodyNode::Component(exp) => {
181                let id = exp.dyn_idx.get();
182                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
183            }
184            BodyNode::IfChain(exp) => {
185                let id = exp.dyn_idx.get();
186                quote! { dioxus_core::TemplateNode::Dynamic { id: #id } }
187            }
188        });
189
190        let ns = ns(quote!(NAME_SPACE));
191        let el_name = el_name.tag_name();
192        let diagnostics = &el.diagnostics;
193        let completion_hints = &el.completion_hints();
194
195        // todo: generate less code if there's no diagnostics by not including the curlies
196        tokens.append_all(quote! {
197            {
198                #completion_hints
199
200                #diagnostics
201
202                dioxus_core::TemplateNode::Element {
203                    tag: #el_name,
204                    namespace: #ns,
205                    attrs: &[ #(#static_attrs),* ],
206                    children: &[ #(#children),* ],
207                }
208            }
209        })
210    }
211}
212
213impl Element {
214    pub(crate) fn add_merging_non_string_diagnostic(diagnostics: &mut Diagnostics, span: Span) {
215        diagnostics.push(span.error("Cannot merge non-fmt literals").help(
216            "Only formatted strings can be merged together. If you want to merge literals, you can use a format string.",
217        ));
218    }
219
220    /// Collapses ifmt attributes into a single dynamic attribute using a space or `;` as a delimiter
221    ///
222    /// ```ignore,
223    /// div {
224    ///     class: "abc-def",
225    ///     class: if some_expr { "abc" },
226    /// }
227    /// ```
228    fn merge_attributes(&mut self) {
229        let mut attrs: Vec<&Attribute> = vec![];
230
231        for attr in &self.raw_attributes {
232            if attrs.iter().any(|old_attr| old_attr.name == attr.name) {
233                continue;
234            }
235
236            attrs.push(attr);
237        }
238
239        for attr in attrs {
240            if attr.name.is_likely_key() {
241                continue;
242            }
243
244            // Collect all the attributes with the same name
245            let matching_attrs = self
246                .raw_attributes
247                .iter()
248                .filter(|a| a.name == attr.name)
249                .collect::<Vec<_>>();
250
251            // if there's only one attribute with this name, then we don't need to merge anything
252            if matching_attrs.len() == 1 {
253                self.merged_attributes.push(attr.clone());
254                continue;
255            }
256
257            // If there are multiple attributes with the same name, then we need to merge them
258            // This will be done by creating an ifmt attribute that combines all the segments
259            // We might want to throw a diagnostic of trying to merge things together that might not
260            // make a whole lot of sense - like merging two exprs together
261            let mut out = IfmtInput::new(attr.span());
262
263            for (idx, matching_attr) in matching_attrs.iter().enumerate() {
264                // If this is the first attribute, then we don't need to add a delimiter
265                if idx != 0 {
266                    // FIXME: I don't want to special case anything - but our delimiter is special cased to a space
267                    // We really don't want to special case anything in the macro, but the hope here is that
268                    // multiline strings can be merged with a space
269                    out.push_raw_str(" ".to_string());
270                }
271
272                // Merge raw literals into the output
273                if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &matching_attr.value {
274                    out.push_ifmt(lit.formatted_input.clone());
275                    continue;
276                }
277
278                // Merge `if cond { "abc" } else if ...` into the output
279                if let AttributeValue::IfExpr(value) = &matching_attr.value {
280                    out.push_expr(value.quote_as_string(&mut self.diagnostics));
281                    continue;
282                }
283
284                Self::add_merging_non_string_diagnostic(
285                    &mut self.diagnostics,
286                    matching_attr.span(),
287                );
288            }
289
290            let out_lit = HotLiteral::Fmted(out.into());
291
292            self.merged_attributes.push(Attribute {
293                name: attr.name.clone(),
294                value: AttributeValue::AttrLiteral(out_lit),
295                colon: attr.colon,
296                dyn_idx: attr.dyn_idx.clone(),
297                comma: matching_attrs.last().unwrap().comma,
298                el_name: attr.el_name.clone(),
299            });
300        }
301    }
302
303    pub(crate) fn key(&self) -> Option<&AttributeValue> {
304        self.raw_attributes
305            .iter()
306            .find(|attr| attr.name.is_likely_key())
307            .map(|attr| &attr.value)
308    }
309
310    fn completion_hints(&self) -> TokenStream2 {
311        // If there is already a brace, we don't need any completion hints
312        if self.brace.is_some() {
313            return quote! {};
314        }
315
316        let ElementName::Ident(name) = &self.name else {
317            return quote! {};
318        };
319
320        quote! {
321            {
322                #[allow(dead_code)]
323                #[doc(hidden)]
324                mod __completions {
325                    fn ignore() {
326                        super::dioxus_elements::elements::completions::CompleteWithBraces::#name
327                    }
328                }
329            }
330        }
331    }
332}
333
334#[derive(PartialEq, Eq, Clone, Debug, Hash)]
335pub enum ElementName {
336    Ident(Ident),
337    Custom(LitStr),
338}
339
340impl ToTokens for ElementName {
341    fn to_tokens(&self, tokens: &mut TokenStream2) {
342        match self {
343            ElementName::Ident(i) => tokens.append_all(quote! { #i }),
344            ElementName::Custom(s) => s.to_tokens(tokens),
345        }
346    }
347}
348
349impl Parse for ElementName {
350    fn parse(stream: ParseStream) -> Result<Self> {
351        let raw =
352            Punctuated::<Ident, Token![-]>::parse_separated_nonempty_with(stream, parse_raw_ident)?;
353        if raw.len() == 1 {
354            Ok(ElementName::Ident(raw.into_iter().next().unwrap()))
355        } else {
356            let span = raw.span();
357            let tag = raw
358                .into_iter()
359                .map(|ident| ident.to_string())
360                .collect::<Vec<_>>()
361                .join("-");
362            let tag = LitStr::new(&tag, span);
363            Ok(ElementName::Custom(tag))
364        }
365    }
366}
367
368impl ElementName {
369    pub(crate) fn tag_name(&self) -> TokenStream2 {
370        match self {
371            ElementName::Ident(i) => quote! { dioxus_elements::elements::#i::TAG_NAME },
372            ElementName::Custom(s) => quote! { #s },
373        }
374    }
375
376    pub fn span(&self) -> Span {
377        match self {
378            ElementName::Ident(i) => i.span(),
379            ElementName::Custom(s) => s.span(),
380        }
381    }
382}
383
384impl PartialEq<&str> for ElementName {
385    fn eq(&self, other: &&str) -> bool {
386        match self {
387            ElementName::Ident(i) => i == *other,
388            ElementName::Custom(s) => s.value() == *other,
389        }
390    }
391}
392
393impl Display for ElementName {
394    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
395        match self {
396            ElementName::Ident(i) => write!(f, "{}", i),
397            ElementName::Custom(s) => write!(f, "{}", s.value()),
398        }
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405    use prettier_please::PrettyUnparse;
406
407    #[test]
408    fn parses_name() {
409        let _parsed: ElementName = syn::parse2(quote::quote! { div }).unwrap();
410        let _parsed: ElementName = syn::parse2(quote::quote! { some-cool-element }).unwrap();
411
412        let _parsed: Element = syn::parse2(quote::quote! { div {} }).unwrap();
413        let _parsed: Element = syn::parse2(quote::quote! { some-cool-element {} }).unwrap();
414
415        let parsed: Element = syn::parse2(quote::quote! {
416            some-cool-div {
417                id: "hi",
418                id: "hi {abc}",
419                id: "hi {def}",
420                class: 123,
421                something: bool,
422                data_attr: "data",
423                data_attr: "data2",
424                data_attr: "data3",
425                exp: { some_expr },
426                something: {cool},
427                something: bool,
428                something: 123,
429                onclick: move |_| {
430                    println!("hello world");
431                },
432                "some-attr": "hello world",
433                onclick: move |_| {},
434                class: "hello world",
435                id: "my-id",
436                data_attr: "data",
437                data_attr: "data2",
438                data_attr: "data3",
439                "somte_attr3": "hello world",
440                something: {cool},
441                something: bool,
442                something: 123,
443                onclick: move |_| {
444                    println!("hello world");
445                },
446                ..attrs1,
447                ..attrs2,
448                ..attrs3
449            }
450        })
451        .unwrap();
452
453        dbg!(parsed);
454    }
455
456    #[test]
457    fn parses_variety() {
458        let input = quote::quote! {
459            div {
460                class: "hello world",
461                id: "my-id",
462                data_attr: "data",
463                data_attr: "data2",
464                data_attr: "data3",
465                "somte_attr3": "hello world",
466                something: {cool},
467                something: bool,
468                something: 123,
469                onclick: move |_| {
470                    println!("hello world");
471                },
472                ..attrs,
473                ..attrs2,
474                ..attrs3
475            }
476        };
477
478        let parsed: Element = syn::parse2(input).unwrap();
479        dbg!(parsed);
480    }
481
482    #[test]
483    fn to_tokens_properly() {
484        let input = quote::quote! {
485            div {
486                class: "hello world",
487                class2: "hello {world}",
488                class3: "goodbye {world}",
489                class4: "goodbye world",
490                "something": "cool {blah}",
491                "something2": "cooler",
492                div {
493                    div {
494                        h1 { class: "h1 col" }
495                        h2 { class: "h2 col" }
496                        h3 { class: "h3 col" }
497                        div {}
498                    }
499                }
500            }
501        };
502
503        let parsed: Element = syn::parse2(input).unwrap();
504        println!("{}", parsed.to_token_stream().pretty_unparse());
505    }
506
507    #[test]
508    fn to_tokens_with_diagnostic() {
509        let input = quote::quote! {
510            div {
511                class: "hello world",
512                id: "my-id",
513                ..attrs,
514                div {
515                    ..attrs,
516                    class: "hello world",
517                    id: "my-id",
518                }
519            }
520        };
521
522        let parsed: Element = syn::parse2(input).unwrap();
523        println!("{}", parsed.to_token_stream().pretty_unparse());
524    }
525
526    #[test]
527    fn merges_attributes() {
528        let input = quote::quote! {
529            div {
530                class: "hello world",
531                class: if count > 3 { "abc {def}" },
532                class: if count < 50 { "small" } else { "big" }
533            }
534        };
535
536        let parsed: Element = syn::parse2(input).unwrap();
537        assert_eq!(parsed.diagnostics.len(), 0);
538        assert_eq!(parsed.merged_attributes.len(), 1);
539        assert_eq!(
540            parsed.merged_attributes[0].name.to_string(),
541            "class".to_string()
542        );
543
544        let attr = &parsed.merged_attributes[0].value;
545
546        println!("{}", attr.to_token_stream().pretty_unparse());
547
548        let _attr = match attr {
549            AttributeValue::AttrLiteral(lit) => lit,
550            _ => panic!("expected literal"),
551        };
552    }
553
554    /// There are a number of cases where merging attributes doesn't make sense
555    /// - merging two expressions together
556    /// - merging two literals together
557    /// - merging a literal and an expression together
558    ///
559    /// etc
560    ///
561    /// We really only want to merge formatted things together
562    ///
563    /// IE
564    /// class: "hello world ",
565    /// class: if some_expr { "abc" }
566    ///
567    /// Some open questions - should the delimiter be explicit?
568    #[test]
569    fn merging_weird_fails() {
570        let input = quote::quote! {
571            div {
572                class: "hello world",
573                class: if some_expr { 123 },
574
575                style: "color: red;",
576                style: "color: blue;",
577
578                width: "1px",
579                width: 1,
580                width: false,
581                contenteditable: true,
582            }
583        };
584
585        let parsed: Element = syn::parse2(input).unwrap();
586
587        assert_eq!(parsed.merged_attributes.len(), 4);
588        assert_eq!(parsed.diagnostics.len(), 3);
589
590        // style should not generate a diagnostic
591        assert!(!parsed
592            .diagnostics
593            .diagnostics
594            .into_iter()
595            .any(|f| f.emit_as_item_tokens().to_string().contains("style")));
596    }
597
598    #[test]
599    fn diagnostics() {
600        let input = quote::quote! {
601            p {
602                class: "foo bar"
603                "Hello world"
604            }
605        };
606
607        let _parsed: Element = syn::parse2(input).unwrap();
608    }
609
610    #[test]
611    fn parses_raw_elements() {
612        let input = quote::quote! {
613            use {
614                "hello"
615            }
616        };
617
618        let _parsed: Element = syn::parse2(input).unwrap();
619    }
620}