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 merge_trivial_attributes() {
528        let input = quote::quote! {
529            div {
530                class: "foo",
531                class: "bar",
532            }
533        };
534
535        let parsed: Element = syn::parse2(input).unwrap();
536        assert_eq!(parsed.diagnostics.len(), 0);
537        assert_eq!(parsed.merged_attributes.len(), 1);
538        assert_eq!(
539            parsed.merged_attributes[0].name.to_string(),
540            "class".to_string()
541        );
542
543        let attr = &parsed.merged_attributes[0].value;
544
545        assert_eq!(
546            attr.to_token_stream().pretty_unparse().as_str(),
547            "\"foo bar\""
548        );
549
550        if let AttributeValue::AttrLiteral(_) = attr {
551        } else {
552            panic!("expected literal")
553        }
554    }
555
556    #[test]
557    fn merge_formatted_attributes() {
558        let input = quote::quote! {
559            div {
560                class: "foo",
561                class: "{bar}",
562            }
563        };
564
565        let parsed: Element = syn::parse2(input).unwrap();
566        assert_eq!(parsed.diagnostics.len(), 0);
567        assert_eq!(parsed.merged_attributes.len(), 1);
568        assert_eq!(
569            parsed.merged_attributes[0].name.to_string(),
570            "class".to_string()
571        );
572
573        let attr = &parsed.merged_attributes[0].value;
574
575        assert_eq!(
576            attr.to_token_stream().pretty_unparse().as_str(),
577            "::std::format!(\"foo {0:}\", bar)"
578        );
579
580        if let AttributeValue::AttrLiteral(_) = attr {
581        } else {
582            panic!("expected literal")
583        }
584    }
585
586    #[test]
587    fn merge_conditional_attributes() {
588        let input = quote::quote! {
589            div {
590                class: "foo",
591                class: if true { "bar" },
592                class: if false { "baz" } else { "qux" }
593            }
594        };
595
596        let parsed: Element = syn::parse2(input).unwrap();
597        assert_eq!(parsed.diagnostics.len(), 0);
598        assert_eq!(parsed.merged_attributes.len(), 1);
599        assert_eq!(
600            parsed.merged_attributes[0].name.to_string(),
601            "class".to_string()
602        );
603
604        let attr = &parsed.merged_attributes[0].value;
605
606        assert_eq!(
607            attr.to_token_stream().pretty_unparse().as_str(),
608            "::std::format!(\n    \
609                \"foo {0:} {1:}\",\n    \
610                { if true { \"bar\".to_string() } else { ::std::string::String::new() } },\n    \
611                { if false { \"baz\".to_string() } else { \"qux\".to_string() } },\n\
612            )"
613        );
614
615        if let AttributeValue::AttrLiteral(_) = attr {
616        } else {
617            panic!("expected literal")
618        }
619    }
620
621    #[test]
622    fn merge_all_attributes() {
623        let input = quote::quote! {
624            div {
625                class: "foo",
626                class: "{bar}",
627                class: if true { "baz" },
628                class: if false { "{qux}" } else { "quux" }
629            }
630        };
631
632        let parsed: Element = syn::parse2(input).unwrap();
633        assert_eq!(parsed.diagnostics.len(), 0);
634        assert_eq!(parsed.merged_attributes.len(), 1);
635        assert_eq!(
636            parsed.merged_attributes[0].name.to_string(),
637            "class".to_string()
638        );
639
640        let attr = &parsed.merged_attributes[0].value;
641
642        if cfg!(debug_assertions) {
643            assert_eq!(
644                attr.to_token_stream().pretty_unparse().as_str(),
645                "::std::format!(\n    \
646                    \"foo {0:} {1:} {2:}\",\n    \
647                    bar,\n    \
648                    { if true { \"baz\".to_string() } else { ::std::string::String::new() } },\n    \
649                    { if false { ::std::format!(\"{qux}\").to_string() } else { \"quux\".to_string() } },\n\
650                )"
651            );
652        } else {
653            assert_eq!(
654                attr.to_token_stream().pretty_unparse().as_str(),
655                "::std::format!(\n    \
656                    \"foo {0:} {1:} {2:}\",\n    \
657                    bar,\n    \
658                    { if true { \"baz\".to_string() } else { ::std::string::String::new() } },\n    \
659                    { if false { (qux).to_string().to_string() } else { \"quux\".to_string() } },\n\
660                )"
661            );
662        }
663
664        if let AttributeValue::AttrLiteral(_) = attr {
665        } else {
666            panic!("expected literal")
667        }
668    }
669
670    /// There are a number of cases where merging attributes doesn't make sense
671    /// - merging two expressions together
672    /// - merging two literals together
673    /// - merging a literal and an expression together
674    ///
675    /// etc
676    ///
677    /// We really only want to merge formatted things together
678    ///
679    /// IE
680    /// class: "hello world ",
681    /// class: if some_expr { "abc" }
682    ///
683    /// Some open questions - should the delimiter be explicit?
684    #[test]
685    fn merging_weird_fails() {
686        let input = quote::quote! {
687            div {
688                class: "hello world",
689                class: if some_expr { 123 },
690
691                style: "color: red;",
692                style: "color: blue;",
693
694                width: "1px",
695                width: 1,
696                width: false,
697                contenteditable: true,
698            }
699        };
700
701        let parsed: Element = syn::parse2(input).unwrap();
702
703        assert_eq!(parsed.merged_attributes.len(), 4);
704        assert_eq!(parsed.diagnostics.len(), 3);
705
706        // style should not generate a diagnostic
707        assert!(!parsed
708            .diagnostics
709            .diagnostics
710            .into_iter()
711            .any(|f| f.emit_as_item_tokens().to_string().contains("style")));
712    }
713
714    #[test]
715    fn diagnostics() {
716        let input = quote::quote! {
717            p {
718                class: "foo bar"
719                "Hello world"
720            }
721        };
722
723        let _parsed: Element = syn::parse2(input).unwrap();
724    }
725
726    #[test]
727    fn parses_raw_elements() {
728        let input = quote::quote! {
729            use {
730                "hello"
731            }
732        };
733
734        let _parsed: Element = syn::parse2(input).unwrap();
735    }
736}