@@ -419,23 +419,35 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
419419 ) ;
420420 field_counter += 1 ;
421421
422- // Check for field: {..expr} — splice a Vec<Id> into the field
422+ // Check for field: {..expr}.chain or field: {expr}.chain — splice a Vec<Id> into the field
423423 if peek_is_group ( tokens, Delimiter :: Brace ) {
424424 let group_clone = tokens. clone ( ) . next ( ) . unwrap ( ) ;
425425 if let TokenTree :: Group ( g) = & group_clone {
426426 let mut inner_check = g. stream ( ) . into_iter ( ) ;
427427 let is_splice = matches ! ( inner_check. next( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' )
428428 && matches ! ( inner_check. next( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) ;
429- if is_splice {
429+ // Determine if a chain (.map(..)) follows the `{}` group.
430+ let mut after = tokens. clone ( ) ;
431+ after. next ( ) ; // skip the brace group
432+ let has_chain = matches ! ( after. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) ;
433+
434+ if is_splice || has_chain {
430435 let group = expect_group ( tokens, Delimiter :: Brace ) ?;
431- let mut inner = group. stream ( ) . into_iter ( ) . peekable ( ) ;
432- inner. next ( ) ; // consume first .
433- inner. next ( ) ; // consume second .
434- let expr: proc_macro2:: TokenStream = inner. collect ( ) ;
436+ let base: TokenStream = if is_splice {
437+ let mut inner = group. stream ( ) . into_iter ( ) . peekable ( ) ;
438+ inner. next ( ) ; // consume first .
439+ inner. next ( ) ; // consume second .
440+ let expr: TokenStream = inner. collect ( ) ;
441+ quote ! {
442+ ( #expr) . into_iter( ) . map( :: std:: convert:: Into :: <usize >:: into)
443+ }
444+ } else {
445+ let expr = group. stream ( ) ;
446+ quote ! { ( #expr) . into_iter( ) }
447+ } ;
448+ let chained = parse_chain_suffix ( tokens, ctx, base) ?;
435449 stmts. push ( quote ! {
436- let #temp: Vec <usize > = ( #expr) . into_iter( )
437- . map( :: std:: convert:: Into :: <usize >:: into)
438- . collect( ) ;
450+ let #temp: Vec <usize > = #chained. collect( ) ;
439451 } ) ;
440452 // An empty splice means the field is absent — skip it
441453 // entirely rather than emitting an empty named field.
@@ -472,6 +484,58 @@ fn parse_direct_node_inner(tokens: &mut Tokens, ctx: &Ident) -> Result<TokenStre
472484 } )
473485}
474486
487+ /// Parse a chain of `.method(args)` suffixes after a `{expr}` or `{..expr}`
488+ /// placeholder in tree templates. Currently supports:
489+ ///
490+ /// ```text
491+ /// .map(param -> template) -- iterator map: produces Vec<usize>
492+ /// ```
493+ ///
494+ /// The chain may be empty (returns `base` unchanged). Multiple chained calls
495+ /// are supported, e.g. `.map(p -> ...).map(q -> ...)`.
496+ ///
497+ /// Each call expects the receiver to be an iterator. The `base` argument
498+ /// should therefore already be an iterator (use `.into_iter()` on it before
499+ /// calling this function).
500+ fn parse_chain_suffix (
501+ tokens : & mut Tokens ,
502+ ctx : & Ident ,
503+ base : TokenStream ,
504+ ) -> Result < TokenStream > {
505+ let mut current = base;
506+ while matches ! ( tokens. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) {
507+ tokens. next ( ) ; // consume .
508+ let method = expect_ident ( tokens, "expected method name after `.`" ) ?;
509+ let method_str = method. to_string ( ) ;
510+ let args_group = expect_group ( tokens, Delimiter :: Parenthesis ) ?;
511+ match method_str. as_str ( ) {
512+ "map" => {
513+ let mut inner = args_group. stream ( ) . into_iter ( ) . peekable ( ) ;
514+ let param = expect_ident ( & mut inner, "expected lambda parameter name" ) ?;
515+ expect_punct ( & mut inner, '-' , "expected `->` after lambda parameter" ) ?;
516+ expect_punct ( & mut inner, '>' , "expected `->` after lambda parameter" ) ?;
517+ let body = parse_direct_node ( & mut inner, ctx) ?;
518+ if let Some ( tok) = inner. next ( ) {
519+ return Err ( syn:: Error :: new_spanned (
520+ tok,
521+ "unexpected token after lambda body" ,
522+ ) ) ;
523+ }
524+ current = quote ! {
525+ #current. map( |#param| #body)
526+ } ;
527+ }
528+ _ => {
529+ return Err ( syn:: Error :: new_spanned (
530+ method,
531+ format ! ( "unknown builtin method `.{method_str}()`" ) ,
532+ ) ) ;
533+ }
534+ }
535+ }
536+ Ok ( current)
537+ }
538+
475539/// Parse the top-level list of a `trees!` template.
476540/// Each item is a node template or `{expr}` splice.
477541fn parse_direct_list ( tokens : & mut Tokens , ctx : & Ident ) -> Result < Vec < TokenStream > > {
@@ -492,18 +556,27 @@ fn parse_direct_list(tokens: &mut Tokens, ctx: &Ident) -> Result<Vec<TokenStream
492556 continue ;
493557 }
494558
495- // {expr} or {..expr} — single node or splice
559+ // {expr} or {..expr} (with optional .chain) — single node or splice
496560 if peek_is_group ( tokens, Delimiter :: Brace ) {
497561 let group = expect_group ( tokens, Delimiter :: Brace ) ?;
562+ let has_chain = matches ! ( tokens. peek( ) , Some ( TokenTree :: Punct ( p) ) if p. as_char( ) == '.' ) ;
498563 let mut inner = group. stream ( ) . into_iter ( ) . peekable ( ) ;
499- if peek_is_dotdot ( & inner) {
500- inner. next ( ) ; // consume first .
501- inner. next ( ) ; // consume second .
502- let expr: TokenStream = inner. collect ( ) ;
503- items. push ( quote ! {
504- __nodes. extend(
564+ let is_splice = peek_is_dotdot ( & inner) ;
565+ if is_splice || has_chain {
566+ let base: TokenStream = if is_splice {
567+ inner. next ( ) ; // consume first .
568+ inner. next ( ) ; // consume second .
569+ let expr: TokenStream = inner. collect ( ) ;
570+ quote ! {
505571 ( #expr) . into_iter( ) . map( :: std:: convert:: Into :: <usize >:: into)
506- ) ;
572+ }
573+ } else {
574+ let expr = group. stream ( ) ;
575+ quote ! { ( #expr) . into_iter( ) }
576+ } ;
577+ let chained = parse_chain_suffix ( tokens, ctx, base) ?;
578+ items. push ( quote ! {
579+ __nodes. extend( #chained) ;
507580 } ) ;
508581 } else {
509582 let expr = group. stream ( ) ;
0 commit comments