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#[derive(PartialEq, Eq, Clone, Debug)]
16pub struct Element {
17 pub name: ElementName,
19
20 pub raw_attributes: Vec<Attribute>,
22
23 pub merged_attributes: Vec<Attribute>,
28
29 pub spreads: Vec<Spread>,
31
32 pub children: Vec<BodyNode>,
36
37 pub brace: Option<Brace>,
39
40 pub diagnostics: Diagnostics,
44}
45
46impl Parse for Element {
47 fn parse(stream: ParseStream) -> Result<Self> {
48 let name = stream.parse::<ElementName>()?;
49
50 let mut brace = None;
54 let mut block = RsxBlock::default();
55
56 match stream.peek(Brace) {
57 true => {
59 block = stream.parse::<RsxBlock>()?;
60 brace = Some(block.brace);
61 }
62
63 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 for attr in block.attributes.iter_mut() {
73 attr.el_name = Some(name.clone());
74 }
75
76 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 element.merge_attributes();
90
91 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 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 _ => {
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 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 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 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 let matching_attrs = self
246 .raw_attributes
247 .iter()
248 .filter(|a| a.name == attr.name)
249 .collect::<Vec<_>>();
250
251 if matching_attrs.len() == 1 {
253 self.merged_attributes.push(attr.clone());
254 continue;
255 }
256
257 let mut out = IfmtInput::new(attr.span());
262
263 for (idx, matching_attr) in matching_attrs.iter().enumerate() {
264 if idx != 0 {
266 out.push_raw_str(" ".to_string());
270 }
271
272 if let AttributeValue::AttrLiteral(HotLiteral::Fmted(lit)) = &matching_attr.value {
274 out.push_ifmt(lit.formatted_input.clone());
275 continue;
276 }
277
278 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 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 #[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 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}