1use crate::innerlude::*;
10use proc_macro2::Span;
11use proc_macro2_diagnostics::SpanDiagnosticExt;
12use syn::{
13 ext::IdentExt,
14 parse::{Parse, ParseBuffer, ParseStream},
15 spanned::Spanned,
16 token::{self, Brace},
17 Expr, Ident, LitStr, Token,
18};
19
20#[derive(PartialEq, Eq, Clone, Debug, Default)]
35pub struct RsxBlock {
36 pub brace: token::Brace,
37 pub attributes: Vec<Attribute>,
38 pub spreads: Vec<Spread>,
39 pub children: Vec<BodyNode>,
40 pub diagnostics: Diagnostics,
41}
42
43impl Parse for RsxBlock {
44 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
45 let content: ParseBuffer;
46 let brace = syn::braced!(content in input);
47 RsxBlock::parse_inner(&content, brace)
48 }
49}
50
51impl RsxBlock {
52 pub fn parse_children(content: &ParseBuffer) -> syn::Result<Self> {
54 let mut nodes = vec![];
55 let mut diagnostics = Diagnostics::new();
56 while !content.is_empty() {
57 nodes.push(Self::parse_body_node_with_comma_diagnostics(
58 content,
59 &mut diagnostics,
60 )?);
61 }
62 Ok(Self {
63 children: nodes,
64 diagnostics,
65 ..Default::default()
66 })
67 }
68
69 pub fn parse_inner(content: &ParseBuffer, brace: token::Brace) -> syn::Result<Self> {
70 let mut items = vec![];
71 let mut diagnostics = Diagnostics::new();
72
73 let mut after_attributes = false;
76
77 while !content.is_empty() {
84 if content.peek(Token![..]) {
86 let dots = content.parse::<Token![..]>()?;
87
88 if let Ok(extra) = content.parse::<Token![.]>() {
90 diagnostics.push(
91 extra
92 .span()
93 .error("Spread expressions only take two dots - not 3! (..spread)"),
94 );
95 }
96
97 let expr = content.parse::<Expr>()?;
98 let attr = Spread {
99 expr,
100 dots,
101 dyn_idx: DynIdx::default(),
102 comma: content.parse().ok(),
103 };
104
105 if !content.is_empty() && attr.comma.is_none() {
106 diagnostics.push(
107 attr.span()
108 .error("Attributes must be separated by commas")
109 .help("Did you forget a comma?"),
110 );
111 }
112 items.push(RsxItem::Spread(attr));
113 after_attributes = true;
114
115 continue;
116 }
117
118 if (content.peek(LitStr) || content.peek(Ident::peek_any))
120 && content.peek2(Token![:])
121 && !content.peek3(Token![:])
122 {
123 let attr = content.parse::<Attribute>()?;
124
125 if !content.is_empty() && attr.comma.is_none() {
126 diagnostics.push(
127 attr.span()
128 .error("Attributes must be separated by commas")
129 .help("Did you forget a comma?"),
130 );
131 }
132
133 items.push(RsxItem::Attribute(attr));
134
135 continue;
136 }
137
138 if content.peek(LitStr)
140 | content.peek(Token![for])
141 | content.peek(Token![if])
142 | content.peek(Token![match])
143 | content.peek(token::Brace)
144 | (content.peek(Ident::peek_any) && content.peek2(Token![-]))
146 | (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace)))
148 | (content.peek(Ident::peek_any) && (after_attributes || content.peek2(token::Brace) || content.peek2(Token![::])))
150 {
151 items.push(RsxItem::Child(
152 Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
153 ));
154 if !content.is_empty() && content.peek(Token![,]) {
155 let comma = content.parse::<Token![,]>()?;
156 diagnostics.push(
157 comma.span().warning(
158 "Elements and text nodes do not need to be separated by commas.",
159 ),
160 );
161 }
162 after_attributes = true;
163 continue;
164 }
165
166 if Self::peek_lowercase_ident(&content)
170 && !content.peek2(Brace)
171 && !content.peek2(Token![:]) && !content.peek2(Token![-]) && !content.peek2(Token![<]) && !content.peek2(Token![::])
176 {
177 let attribute = content.parse::<Attribute>()?;
178
179 if !content.is_empty() && attribute.comma.is_none() {
180 diagnostics.push(
181 attribute
182 .span()
183 .error("Attributes must be separated by commas")
184 .help("Did you forget a comma?"),
185 );
186 }
187
188 items.push(RsxItem::Attribute(attribute));
189
190 continue;
191 }
192
193 items.push(RsxItem::Child(
195 Self::parse_body_node_with_comma_diagnostics(content, &mut diagnostics)?,
196 ))
197 }
198
199 RsxBlock::validate(&items, &mut diagnostics);
201
202 let mut attributes = vec![];
205 let mut spreads = vec![];
206 let mut children = vec![];
207 for item in items {
208 match item {
209 RsxItem::Attribute(attr) => attributes.push(attr),
210 RsxItem::Spread(spread) => spreads.push(spread),
211 RsxItem::Child(child) => children.push(child),
212 }
213 }
214
215 Ok(Self {
216 attributes,
217 children,
218 spreads,
219 brace,
220 diagnostics,
221 })
222 }
223
224 fn parse_body_node_with_comma_diagnostics(
226 content: &ParseBuffer,
227 _diagnostics: &mut Diagnostics,
228 ) -> syn::Result<BodyNode> {
229 let body_node = content.parse::<BodyNode>()?;
230 if !content.is_empty() && content.peek(Token![,]) {
231 let _comma = content.parse::<Token![,]>()?;
232
233 }
252 Ok(body_node)
253 }
254
255 fn peek_lowercase_ident(stream: &ParseStream) -> bool {
256 let Ok(ident) = stream.fork().call(Ident::parse_any) else {
257 return false;
258 };
259
260 ident
261 .to_string()
262 .chars()
263 .next()
264 .unwrap()
265 .is_ascii_lowercase()
266 }
267
268 fn validate(items: &[RsxItem], diagnostics: &mut Diagnostics) {
279 #[derive(Debug, PartialEq, Eq)]
280 enum ValidationState {
281 Attributes,
282 Spreads,
283 Children,
284 }
285 use ValidationState::*;
286 let mut state = ValidationState::Attributes;
287
288 for item in items.iter() {
289 match item {
290 RsxItem::Attribute(_) => {
291 if state == Children || state == Spreads {
292 diagnostics.push(
293 item.span()
294 .error("Attributes must come before children in an element"),
295 );
296 }
297 state = Attributes;
298 }
299 RsxItem::Spread(_) => {
300 if state == Children {
301 diagnostics.push(
302 item.span()
303 .error("Spreads must come before children in an element"),
304 );
305 }
306 state = Spreads;
307 }
308 RsxItem::Child(_) => {
309 state = Children;
310 }
311 }
312 }
313 }
314}
315
316pub enum RsxItem {
317 Attribute(Attribute),
318 Spread(Spread),
319 Child(BodyNode),
320}
321
322impl RsxItem {
323 pub fn span(&self) -> Span {
324 match self {
325 RsxItem::Attribute(attr) => attr.span(),
326 RsxItem::Spread(spread) => spread.dots.span(),
327 RsxItem::Child(child) => child.span(),
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use quote::quote;
336
337 #[test]
338 fn basic_cases() {
339 let input = quote! {
340 { "Hello, world!" }
341 };
342
343 let block: RsxBlock = syn::parse2(input).unwrap();
344 assert_eq!(block.attributes.len(), 0);
345 assert_eq!(block.children.len(), 1);
346
347 let input = quote! {
348 {
349 key: "value",
350 onclick: move |_| {
351 "Hello, world!"
352 },
353 ..spread,
354 "Hello, world!"
355 }
356 };
357
358 let block: RsxBlock = syn::parse2(input).unwrap();
359 dbg!(block);
360
361 let complex_element = quote! {
362 {
363 key: "value",
364 onclick2: move |_| {
365 "Hello, world!"
366 },
367 thing: if true { "value" },
368 otherthing: if true { "value" } else { "value" },
369 onclick: move |_| {
370 "Hello, world!"
371 },
372 ..spread,
373 ..spread1
374 ..spread2,
375 "Hello, world!"
376 }
377 };
378
379 let _block: RsxBlock = syn::parse2(complex_element).unwrap();
380
381 let complex_component = quote! {
382 {
383 key: "value",
384 onclick2: move |_| {
385 "Hello, world!"
386 },
387 ..spread,
388 "Hello, world!"
389 }
390 };
391
392 let _block: RsxBlock = syn::parse2(complex_component).unwrap();
393 }
394
395 #[test]
397 fn partial_cases() {
398 let with_handler = quote! {
399 {
400 onclick: move |_| {
401 some.
402 }
403 }
404 };
405
406 let _block: RsxBlock = syn::parse2(with_handler).unwrap();
407 }
408
409 #[test]
411 fn hr_score() {
412 let _block = quote! {
413 {
414 a: "value {cool}",
415 b: "{cool} value",
416 b: "{cool} {thing} value",
417 b: "{thing} value",
418 }
419 };
420
421 quote! {
428 div {
430 div { class: "other {abc} {def} {hij}" } div { class: "thing {abc} {def}" } }
434
435 div {
437 h1 {
438 class: "thing {abc}" }
440 h1 {
441 class: "thing {hij}" }
443 }
450
451 Component {
453 class: "thing {abc}",
454 other: "other {abc} {def}",
455 }
456 Component {
457 class: "thing",
458 other: "other",
459 }
460
461 Component {
462 class: "thing {abc}",
463 other: "other",
464 }
465 Component {
466 class: "thing {abc}",
467 other: "other {abc} {def}",
468 }
469 };
470 }
471
472 #[test]
473 fn kitchen_sink_parse() {
474 let input = quote! {
475 {
477 class: "hello",
478 id: "node-{node_id}",
479 ..props,
480
481 "Hello, world!"
483
484 {rsx! { "hi again!" }}
486
487
488 for item in 0..10 {
489 div { "cool-{item}" }
491 }
492
493 Link {
494 to: "/home",
495 class: "link {is_ready}",
496 "Home"
497 }
498
499 if false {
500 div { "hi again!?" }
501 } else if true {
502 div { "its cool?" }
503 } else {
504 div { "not nice !" }
505 }
506 }
507 };
508
509 let _parsed: RsxBlock = syn::parse2(input).unwrap();
510 }
511
512 #[test]
513 fn simple_comp_syntax() {
514 let input = quote! {
515 { class: "inline-block mr-4", icons::icon_14 {} }
516 };
517
518 let _parsed: RsxBlock = syn::parse2(input).unwrap();
519 }
520
521 #[test]
522 fn with_sutter() {
523 let input = quote! {
524 {
525 div {}
526 d
527 div {}
528 }
529 };
530
531 let _parsed: RsxBlock = syn::parse2(input).unwrap();
532 }
533
534 #[test]
535 fn looks_like_prop_but_is_expr() {
536 let input = quote! {
537 {
538 a: "asd".to_string(),
539 c: "asd".to_string(),
541 d: Some("asd".to_string()),
542 e: Some("asd".to_string()),
543 }
544 };
545
546 let _parsed: RsxBlock = syn::parse2(input).unwrap();
547 }
548
549 #[test]
550 fn no_comma_diagnostics() {
551 let input = quote! {
552 { a, ..ComponentProps { a: 1, b: 2, c: 3, children: VNode::empty(), onclick: Default::default() } }
553 };
554
555 let parsed: RsxBlock = syn::parse2(input).unwrap();
556 assert!(parsed.diagnostics.is_empty());
557 }
558 #[test]
559 fn proper_attributes() {
560 let input = quote! {
561 {
562 onclick: action,
563 href,
564 onmounted: onmounted,
565 prevent_default,
566 class,
567 rel,
568 target: tag_target,
569 aria_current,
570 ..attributes,
571 {children}
572 }
573 };
574
575 let parsed: RsxBlock = syn::parse2(input).unwrap();
576 dbg!(parsed.attributes);
577 }
578
579 #[test]
580 fn reserved_attributes() {
581 let input = quote! {
582 {
583 label {
584 for: "blah",
585 }
586 }
587 };
588
589 let parsed: RsxBlock = syn::parse2(input).unwrap();
590 dbg!(parsed.attributes);
591 }
592
593 #[test]
594 fn diagnostics_check() {
595 let input = quote::quote! {
596 {
597 class: "foo bar"
598 "Hello world"
599 }
600 };
601
602 let _parsed: RsxBlock = syn::parse2(input).unwrap();
603 }
604
605 #[test]
606 fn incomplete_components() {
607 let input = quote::quote! {
608 {
609 some::cool::Component
610 }
611 };
612
613 let _parsed: RsxBlock = syn::parse2(input).unwrap();
614 }
615
616 #[test]
617 fn incomplete_root_elements() {
618 use syn::parse::Parser;
619
620 let input = quote::quote! {
621 di
622 };
623
624 let parsed = RsxBlock::parse_children.parse2(input).unwrap();
625 let children = parsed.children;
626
627 assert_eq!(children.len(), 1);
628 if let BodyNode::Element(parsed) = &children[0] {
629 assert_eq!(
630 parsed.name,
631 ElementName::Ident(Ident::new("di", Span::call_site()))
632 );
633 } else {
634 panic!("expected element, got {:?}", children);
635 }
636 assert!(parsed.diagnostics.is_empty());
637 }
638}