rstml_control_flow/
escape.rs

1//!
2//! This module contain implementation of flow-controll based on rstml custom
3//! nodes.
4//!
5//! This is usefull when one need to define optional or repetetive html(*ml)
6//! elements. It contain implementation of If, Match and For constructions that
7//! is very simmilar to rust. The main difference in implementation is that body
8//! of construction (part inside curly brackets) is expected to be an html(*ml),
9//! but condition is expected to be in rust syntax.
10
11use quote::{ToTokens, TokenStreamExt};
12use rstml::{
13    node::{CustomNode, Node as RNode},
14    recoverable::{ParseRecoverable, RecoverableContext},
15    visitor::Visitor,
16};
17use syn::{
18    braced,
19    parse::{Parse, ParseStream},
20    token::Brace,
21    Expr, Pat, Token,
22};
23
24use crate::{Either, TryIntoOrCloneRef};
25
26#[cfg(not(feature = "extendable"))]
27type CustomNodeType = EscapeCode;
28
29#[cfg(feature = "extendable")]
30type CustomNodeType = crate::ExtendableCustomNode;
31
32type Node = RNode<CustomNodeType>;
33
34#[derive(Clone, Debug, syn_derive::ToTokens)]
35pub struct Block {
36    #[syn(braced)]
37    pub brace_token: Brace,
38    #[syn(in = brace_token)]
39    #[to_tokens(|tokens, val| tokens.append_all(val))]
40    pub body: Vec<Node>,
41}
42
43impl ParseRecoverable for Block {
44    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
45        // we use this closure, because `braced!`
46        // has private api and force it's usage inside methods that return Result
47        let inner_parser = |parser: &mut RecoverableContext, input: ParseStream| {
48            let content;
49            let brace_token = braced!(content in input);
50            let mut body = vec![];
51            while !content.is_empty() {
52                let Some(val) = parser.parse_recoverable(&content) else {
53                    return Ok(None);
54                };
55                body.push(val);
56            }
57            Ok(Some(Block { brace_token, body }))
58        };
59        parser.parse_mixed_fn(input, inner_parser)?
60    }
61}
62
63#[derive(Clone, Debug, syn_derive::ToTokens)]
64pub struct ElseIf {
65    pub else_token: Token![else],
66    pub if_token: Token![if],
67    pub condition: Expr,
68    pub then_branch: Block,
69}
70
71impl ParseRecoverable for ElseIf {
72    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
73        Some(ElseIf {
74            else_token: parser.parse_simple(input)?,
75            if_token: parser.parse_simple(input)?,
76            condition: parser.parse_mixed_fn(input, |_, input| {
77                input.call(Expr::parse_without_eager_brace)
78            })?,
79            then_branch: parser.parse_recoverable(input)?,
80        })
81    }
82}
83
84#[derive(Clone, Debug, syn_derive::ToTokens)]
85pub struct Else {
86    pub else_token: Token![else],
87    pub then_branch: Block,
88}
89impl ParseRecoverable for Else {
90    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
91        Some(Else {
92            else_token: parser.parse_simple(input)?,
93            then_branch: parser.parse_recoverable(input)?,
94        })
95    }
96}
97
98/// If construction:
99/// Condition is any valid rust syntax, while body is expected yo be *ml
100/// element.
101///
102/// Example:
103/// `@if x > 2 { <div></div> }`
104/// `@if let Some(x) = foo { <div></div> }`
105///
106/// As in rust can contain arbitrary amount of `else if .. {..}` constructs and
107/// one `else {..}`.
108#[derive(Clone, Debug, syn_derive::ToTokens)]
109pub struct IfExpr {
110    pub keyword: Token![if],
111    pub condition: Expr,
112    pub then_branch: Block,
113    #[to_tokens(TokenStreamExt::append_all)]
114    pub else_ifs: Vec<ElseIf>,
115    pub else_branch: Option<Else>,
116}
117
118impl ParseRecoverable for IfExpr {
119    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
120        let keyword = parser.parse_simple(input)?;
121
122        let condition = parser.parse_mixed_fn(input, |_, input| {
123            input.call(Expr::parse_without_eager_brace)
124        })?;
125
126        let then_branch = parser.parse_recoverable(input)?;
127        let mut else_ifs = vec![];
128
129        while input.peek(Token![else]) && input.peek2(Token![if]) {
130            else_ifs.push(parser.parse_recoverable(input)?)
131        }
132        let mut else_branch = None;
133        if input.peek(Token![else]) {
134            else_branch = Some(parser.parse_recoverable(input)?)
135        }
136        Some(IfExpr {
137            keyword,
138            condition,
139            then_branch,
140            else_ifs,
141            else_branch,
142        })
143    }
144}
145
146#[derive(Clone, Debug, syn_derive::ToTokens)]
147pub struct ForExpr {
148    pub keyword: Token![for],
149    pub pat: Pat,
150    pub token_in: Token![in],
151    pub expr: Expr,
152    pub block: Block,
153}
154
155impl ParseRecoverable for ForExpr {
156    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
157        let keyword = parser.parse_simple(input)?;
158        let pat = parser.parse_mixed_fn(input, |_parse, input| {
159            Pat::parse_multi_with_leading_vert(input)
160        })?;
161        let token_in = parser.parse_simple(input)?;
162        let expr = parser.parse_mixed_fn(input, |_, input| {
163            input.call(Expr::parse_without_eager_brace)
164        })?;
165        let block = parser.parse_recoverable(input)?;
166        Some(ForExpr {
167            keyword,
168            pat,
169            token_in,
170            expr,
171            block,
172        })
173    }
174}
175
176#[derive(Clone, Debug, syn_derive::ToTokens)]
177pub struct Arm {
178    pub pat: Pat,
179    // pub guard: Option<(If, Box<Expr>)>,
180    pub fat_arrow_token: Token![=>],
181    pub body: Block,
182    pub comma: Option<Token![,]>,
183}
184
185impl ParseRecoverable for Arm {
186    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
187        Some(Arm {
188            pat: parser
189                .parse_mixed_fn(input, |_, input| Pat::parse_multi_with_leading_vert(input))?,
190            fat_arrow_token: parser.parse_simple(input)?,
191            body: parser.parse_recoverable(input)?,
192            comma: parser.parse_simple(input)?,
193        })
194    }
195}
196
197// match foo {
198//     1|3 => {},
199//     x if x > 10 => {},
200//     | x => {}
201// }
202
203#[derive(Clone, Debug, syn_derive::ToTokens)]
204pub struct MatchExpr {
205    pub keyword: Token![match],
206    pub expr: Expr,
207    #[syn(braced)]
208    pub brace_token: Brace,
209    #[syn(in = brace_token)]
210    #[to_tokens(TokenStreamExt::append_all)]
211    pub arms: Vec<Arm>,
212}
213
214impl ParseRecoverable for MatchExpr {
215    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
216        // we use this closure, because `braced!`
217        // has private api and force it's usage inside methods that return Result
218        let inner_parser = |parser: &mut RecoverableContext, input: ParseStream| {
219            let Some(keyword) = parser.parse_simple(input) else {
220                return Ok(None);
221            };
222            let content;
223            let expr = Expr::parse_without_eager_brace(input)?;
224            let brace_token = braced!(content in input);
225            let mut arms = Vec::new();
226            while !content.is_empty() {
227                let Some(val) = parser.parse_recoverable(&content) else {
228                    return Ok(None);
229                };
230                arms.push(val);
231            }
232            Ok(Some(MatchExpr {
233                keyword,
234                expr,
235                brace_token,
236                arms,
237            }))
238        };
239        parser.parse_mixed_fn(input, inner_parser)?
240    }
241}
242
243// Minimal version of syn::Expr, that uses custom `Block` with `Node` array
244// instead of `syn::Block` that contain valid rust code.
245
246#[derive(Clone, Debug, syn_derive::ToTokens)]
247pub enum EscapedExpr {
248    If(IfExpr),
249    For(ForExpr),
250    Match(MatchExpr),
251}
252
253impl ParseRecoverable for EscapedExpr {
254    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
255        let res = if input.peek(Token![if]) {
256            EscapedExpr::If(parser.parse_recoverable(input)?)
257        } else if input.peek(Token![for]) {
258            EscapedExpr::For(parser.parse_recoverable(input)?)
259        } else if input.peek(Token![match]) {
260            EscapedExpr::Match(parser.parse_recoverable(input)?)
261        } else {
262            return None;
263        };
264
265        Some(res)
266    }
267}
268#[cfg(feature = "extendable")]
269impl TryIntoOrCloneRef<EscapeCode> for crate::ExtendableCustomNode {
270    fn try_into_or_clone_ref(self) -> Either<EscapeCode, Self> {
271        if let Some(val) = self.try_downcast_ref::<EscapeCode>() {
272            Either::A(val.clone())
273        } else {
274            Either::B(self.clone())
275        }
276    }
277    fn new_from_value(value: EscapeCode) -> Self {
278        Self::from_value(value)
279    }
280}
281
282#[derive(Clone, Debug, syn_derive::ToTokens)]
283pub struct EscapeCode<T: ToTokens = Token![@]> {
284    pub escape_token: T,
285    pub expression: EscapedExpr,
286}
287
288impl<T: ToTokens + Parse> ParseRecoverable for EscapeCode<T> {
289    fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self> {
290        let escape_token = parser.parse_simple(input)?;
291        let expression = parser.parse_recoverable(input)?;
292
293        Some(Self {
294            escape_token,
295            expression,
296        })
297    }
298}
299
300impl<T> CustomNode for EscapeCode<T>
301where
302    T: Parse + ToTokens,
303{
304    fn peek_element(input: syn::parse::ParseStream) -> bool {
305        if input.parse::<T>().is_err() {
306            return false;
307        }
308        input.peek(Token![if]) || input.peek(Token![for]) || input.peek(Token![match])
309    }
310}
311
312pub mod visitor_impl {
313    use rstml::visitor::{CustomNodeWalker, RustCode};
314
315    use super::*;
316
317    pub struct EscapeCodeWalker;
318    impl CustomNodeWalker for EscapeCodeWalker {
319        type Custom = CustomNodeType;
320        fn walk_custom_node_fields<VisitorImpl>(
321            visitor: &mut VisitorImpl,
322            node: &mut Self::Custom,
323        ) -> bool
324        where
325            VisitorImpl: Visitor<CustomNodeType>,
326        {
327            EscapeCode::visit_custom_children(visitor, node)
328        }
329    }
330
331    impl Block {
332        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
333            visitor: &mut V,
334            block: &mut Self,
335        ) -> bool {
336            block.body.iter_mut().all(|val| visitor.visit_node(val))
337        }
338    }
339
340    impl ElseIf {
341        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
342            visitor: &mut V,
343            expr: &mut Self,
344        ) -> bool {
345            visitor.visit_rust_code(RustCode::Expr(&mut expr.condition));
346            Block::visit_custom_children(visitor, &mut expr.then_branch)
347        }
348    }
349
350    impl Else {
351        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
352            visitor: &mut V,
353            expr: &mut Self,
354        ) -> bool {
355            Block::visit_custom_children(visitor, &mut expr.then_branch)
356        }
357    }
358
359    impl IfExpr {
360        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
361            visitor: &mut V,
362            expr: &mut Self,
363        ) -> bool {
364            visitor.visit_rust_code(RustCode::Expr(&mut expr.condition));
365            Block::visit_custom_children(visitor, &mut expr.then_branch)
366                || expr
367                    .else_ifs
368                    .iter_mut()
369                    .all(|val| ElseIf::visit_custom_children(visitor, val))
370                || expr
371                    .else_branch
372                    .as_mut()
373                    .map(|val| Else::visit_custom_children(visitor, val))
374                    .unwrap_or(true)
375        }
376    }
377    impl ForExpr {
378        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
379            visitor: &mut V,
380            expr: &mut Self,
381        ) -> bool {
382            visitor.visit_rust_code(RustCode::Pat(&mut expr.pat));
383            visitor.visit_rust_code(RustCode::Expr(&mut expr.expr));
384            Block::visit_custom_children(visitor, &mut expr.block)
385        }
386    }
387    impl Arm {
388        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
389            visitor: &mut V,
390            expr: &mut Self,
391        ) -> bool {
392            visitor.visit_rust_code(RustCode::Pat(&mut expr.pat));
393            Block::visit_custom_children(visitor, &mut expr.body)
394        }
395    }
396    impl MatchExpr {
397        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
398            visitor: &mut V,
399            expr: &mut Self,
400        ) -> bool {
401            visitor.visit_rust_code(RustCode::Expr(&mut expr.expr));
402
403            expr.arms
404                .iter_mut()
405                .all(|val| Arm::visit_custom_children(visitor, val))
406        }
407    }
408    impl EscapedExpr {
409        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
410            visitor: &mut V,
411            expr: &mut Self,
412        ) -> bool {
413            match expr {
414                EscapedExpr::If(expr) => IfExpr::visit_custom_children(visitor, expr),
415                EscapedExpr::For(expr) => ForExpr::visit_custom_children(visitor, expr),
416                EscapedExpr::Match(expr) => MatchExpr::visit_custom_children(visitor, expr),
417            }
418        }
419    }
420    impl EscapeCode {
421        pub fn visit_custom_children<V: Visitor<CustomNodeType>>(
422            visitor: &mut V,
423            node: &mut CustomNodeType,
424        ) -> bool {
425            let Either::A(mut this): Either<EscapeCode, _> = node.clone().try_into_or_clone_ref()
426            else {
427                return true;
428            };
429            let result = EscapedExpr::visit_custom_children(visitor, &mut this.expression);
430            *node = TryIntoOrCloneRef::new_from_value(this);
431            result
432        }
433    }
434}
435
436#[cfg(test)]
437#[cfg(not(feature = "extendable"))]
438mod test_typed {
439    use quote::quote;
440    use rstml::{node::Node, recoverable::Recoverable, Parser, ParserConfig};
441    use syn::{parse_quote, Token};
442
443    use super::{EscapeCode, EscapedExpr};
444
445    type MyCustomNode = EscapeCode<Token![@]>;
446    type MyNode = Node<MyCustomNode>;
447    #[test]
448    fn if_complex_expression() {
449        let actual: Recoverable<MyNode> = parse_quote! {
450            @if just && an || expression {
451                <div/>
452            }
453        };
454        let Node::Custom(actual) = actual.inner() else {
455            panic!()
456        };
457
458        let EscapedExpr::If(expr) = actual.expression else {
459            panic!()
460        };
461
462        assert_eq!(expr.condition, parse_quote!(just && an || expression));
463    }
464
465    #[test]
466    fn if_let_expr() {
467        let actual: Recoverable<MyNode> = parse_quote! {
468            @if let (foo) = Bar {
469                <div/>
470            }
471        };
472        let Node::Custom(actual) = actual.inner() else {
473            panic!()
474        };
475
476        let EscapedExpr::If(expr) = actual.expression else {
477            panic!()
478        };
479
480        assert_eq!(expr.condition, parse_quote!(let (foo) = Bar));
481    }
482
483    #[test]
484    fn if_simple() {
485        let actual: Recoverable<MyNode> = parse_quote! {
486            @if foo > bar {
487                <a regular="html" component/>
488                <div>
489                </div>
490            }
491        };
492
493        let Node::Custom(actual) = actual.inner() else {
494            panic!()
495        };
496
497        let EscapedExpr::If(expr) = &actual.expression else {
498            panic!()
499        };
500
501        assert_eq!(expr.condition, parse_quote!(foo > bar));
502    }
503
504    #[test]
505    fn if_else_if() {
506        let actual: Recoverable<MyNode> = parse_quote! {
507            @if foo > bar {
508                <first/>
509            } else if foo == 2 {
510                <second/>
511            } else if foo == 3 {
512                <third/>
513            } else {
514                <default/>
515            }
516        };
517
518        let Node::Custom(actual) = actual.inner() else {
519            panic!()
520        };
521
522        let EscapedExpr::If(expr) = &actual.expression else {
523            panic!()
524        };
525
526        assert_eq!(expr.condition, parse_quote!(foo > bar));
527        assert_eq!(expr.else_ifs[0].condition, parse_quote!(foo == 2));
528        assert_eq!(expr.else_ifs[1].condition, parse_quote!(foo == 3));
529        assert!(expr.else_branch.is_some());
530    }
531
532    #[test]
533    fn for_simple() {
534        let actual: Recoverable<MyNode> = parse_quote! {
535            @for x in foo {
536                <div>
537                {x}
538                </div>
539            }
540        };
541
542        let Node::Custom(actual) = actual.inner() else {
543            panic!()
544        };
545
546        let EscapedExpr::For(expr) = &actual.expression else {
547            panic!()
548        };
549
550        assert_eq!(expr.pat, parse_quote!(x));
551        assert_eq!(expr.expr, parse_quote!(foo));
552    }
553
554    #[test]
555    fn for_binding() {
556        let actual: Recoverable<MyNode> = parse_quote! {
557            @for (ref x, f) in foo {
558                <div>
559                {x}
560                </div>
561            }
562        };
563
564        let Node::Custom(actual) = actual.inner() else {
565            panic!()
566        };
567
568        let EscapedExpr::For(expr) = &actual.expression else {
569            panic!()
570        };
571
572        assert_eq!(expr.pat, parse_quote!((ref x, f)));
573        assert_eq!(expr.expr, parse_quote!(foo));
574    }
575
576    #[test]
577    fn match_simple() {
578        let actual: Recoverable<MyNode> = parse_quote! {
579            @match foo {
580                x => {<x/>}
581                _ => {<default/>}
582            }
583        };
584
585        let Node::Custom(actual) = actual.inner() else {
586            panic!()
587        };
588
589        let EscapedExpr::Match(expr) = &actual.expression else {
590            panic!()
591        };
592
593        assert_eq!(expr.expr, parse_quote!(foo));
594        assert_eq!(expr.arms[0].pat, parse_quote!(x));
595        assert_eq!(expr.arms[1].pat, parse_quote!(_));
596    }
597
598    #[test]
599    fn match_complex_exprs() {
600        let actual: Recoverable<MyNode> = parse_quote! {
601            @match foo > 2 * 2 {
602                Some(x) => {<x/>}
603                y|z => {<default/>}
604            }
605        };
606
607        let Node::Custom(actual) = actual.inner() else {
608            panic!()
609        };
610
611        let EscapedExpr::Match(expr) = &actual.expression else {
612            panic!()
613        };
614
615        assert_eq!(expr.expr, parse_quote!(foo > 2 * 2));
616        assert_eq!(expr.arms[0].pat, parse_quote!(Some(x)));
617        assert_eq!(expr.arms[1].pat, parse_quote!(y | z));
618    }
619
620    #[test]
621    fn check_if_inside_if() {
622        let actual: Recoverable<MyNode> = parse_quote! {
623            @if just && an || expression {
624                @if foo > bar {
625                    <div/>
626                }
627            }
628        };
629        let Node::Custom(actual) = actual.inner() else {
630            panic!()
631        };
632
633        let EscapedExpr::If(expr) = actual.expression else {
634            panic!()
635        };
636
637        assert_eq!(expr.condition, parse_quote!(just && an || expression));
638        let node = expr.then_branch.body.iter().next().unwrap();
639        let Node::Custom(actual) = node else { panic!() };
640        let EscapedExpr::If(expr) = &actual.expression else {
641            panic!()
642        };
643        assert_eq!(expr.condition, parse_quote!(foo > bar));
644    }
645
646    #[test]
647    fn for_inside_if() {
648        let actual: Recoverable<MyNode> = parse_quote! {
649            @if just && an || expression {
650                @for x in foo {
651                    <div/>
652                }
653            }
654        };
655        let Node::Custom(actual) = actual.inner() else {
656            panic!()
657        };
658
659        let EscapedExpr::If(expr) = actual.expression else {
660            panic!()
661        };
662
663        assert_eq!(expr.condition, parse_quote!(just && an || expression));
664        let node = expr.then_branch.body.iter().next().unwrap();
665        let Node::Custom(actual) = node else { panic!() };
666        let EscapedExpr::For(expr) = &actual.expression else {
667            panic!()
668        };
669        assert_eq!(expr.pat, parse_quote!(x));
670        assert_eq!(expr.expr, parse_quote!(foo));
671    }
672
673    #[test]
674    fn custom_node_using_config() {
675        let actual = Parser::new(
676            ParserConfig::new()
677                .element_close_use_default_wildcard_ident(false)
678                .custom_node::<MyCustomNode>(),
679        )
680        .parse_simple(quote! {
681            @if just && an || expression {
682                <a regular="html" component/>
683                <div>
684                </div>
685            }
686        })
687        .unwrap();
688        let Node::Custom(actual) = &actual[0] else {
689            panic!()
690        };
691
692        let EscapedExpr::If(expr) = &actual.expression else {
693            panic!()
694        };
695
696        assert_eq!(expr.condition, parse_quote!(just && an || expression));
697    }
698}
699
700#[cfg(test)]
701mod test_universal {
702    use proc_macro2::TokenStream;
703    use quote::quote;
704    use rstml::{
705        recoverable::Recoverable, visitor::visit_nodes_with_custom, ParserConfig, ParsingResult,
706    };
707    use syn::{parse_quote, visit_mut::VisitMut};
708
709    use super::*;
710    use crate::escape::visitor_impl::EscapeCodeWalker;
711
712    // #[cfg(feature="extendable")]
713    #[cfg(not(feature = "extendable"))]
714    fn parse_universal(input: TokenStream) -> ParsingResult<Vec<Node>> {
715        use rstml::Parser;
716
717        let actual =
718            Parser::new(ParserConfig::new().custom_node::<EscapeCode>()).parse_recoverable(input);
719
720        return actual;
721    }
722    #[cfg(feature = "extendable")]
723    fn parse_universal(input: TokenStream) -> ParsingResult<Vec<Node>> {
724        use crate::ExtendableCustomNode;
725
726        let result =
727            ExtendableCustomNode::parse2_with_config::<(EscapeCode,)>(ParserConfig::new(), input);
728        return result;
729    }
730
731    // For extendable custom node it is safe to use only after parsing.
732    fn reparse_concrete<A: ToTokens>(val: A) -> EscapeCode {
733        let recoverable: Recoverable<_> = parse_quote!(#val);
734        recoverable.inner()
735    }
736
737    #[test]
738    fn if_node_reparsable() {
739        let tokens = quote! {
740            @if just && an || expression {
741                <div/>
742            }
743        };
744
745        let actual = &parse_universal(tokens).into_result().unwrap()[0];
746
747        let Node::Custom(actual) = actual else {
748            panic!()
749        };
750        let actual = reparse_concrete(actual);
751
752        let EscapedExpr::If(expr) = actual.expression else {
753            panic!()
754        };
755
756        assert_eq!(expr.condition, parse_quote!(just && an || expression));
757    }
758    #[test]
759    fn if_node_visitor_rstml() {
760        let tokens = quote! {
761            @if just && an || expression {
762                <div/>
763            }
764        };
765
766        let mut actual = parse_universal(tokens).into_result().unwrap();
767
768        struct ElementVisitor {
769            elements: Vec<String>,
770        }
771        impl<C: std::fmt::Debug> Visitor<C> for ElementVisitor {
772            fn visit_element(&mut self, node: &mut rstml::node::NodeElement<C>) -> bool {
773                self.elements.push(node.open_tag.name.to_string());
774                true
775            }
776        }
777        impl VisitMut for ElementVisitor {}
778
779        let visitor = ElementVisitor {
780            elements: Vec::new(),
781        };
782        let elements =
783            visit_nodes_with_custom::<_, _, EscapeCodeWalker>(&mut actual, visitor).elements;
784
785        assert_eq!(&elements, &["div"]);
786    }
787
788    #[test]
789    fn if_node_visitor_syn_expr() {
790        let tokens = quote! {
791            @if just && an || expression {
792                <div/>
793            }
794        };
795
796        let mut actual = parse_universal(tokens).into_result().unwrap();
797
798        struct ElementVisitor {
799            exprs: Vec<String>,
800        }
801        impl<C: std::fmt::Debug> Visitor<C> for ElementVisitor {}
802        impl VisitMut for ElementVisitor {
803            fn visit_expr_mut(&mut self, exprs: &mut syn::Expr) {
804                self.exprs.push(exprs.to_token_stream().to_string());
805            }
806        }
807
808        let visitor = ElementVisitor { exprs: Vec::new() };
809        let elements =
810            visit_nodes_with_custom::<_, _, EscapeCodeWalker>(&mut actual, visitor).exprs;
811
812        assert_eq!(&elements, &["just && an || expression"]);
813    }
814}