Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Velocity: Is it possible to nest macros that use #@ and $bodyContent?

Tags:

scope

velocity

I have a macro that looks essentially like this:

#macro( surround $x )
  surround:$x
    $bodyContent
  /surround:$x
#end

Invocation #@surround("A")bunch o' stuff#end produces "surround:A bunch o' stuff /surround:A" as expected. Invocation #@surround("A")#@surround("B")more stuff#end#end produces surround:A surround:B more stuff /surround:B /surround:A which is exactly what I want.

But now I want to build upwards with another macro

#macro( annotated-surround $x $y )
  #@surround( $x )
    annotate:$y
    $bodyContent
  #end
#end

The intended expansion of #annotated-surround( "C" "note" ) stuff #end is surround:C annotate:note stuff /surround:C

...but this doesn't work; I get the dreaded semi-infinite expansion of the annotated-surround body content.

I have read the answer at Closure in Velocity template macros and still don't quite know whether what I want to do is possible.

I'm willing to do arbitrarily tricky things within the definitions of #surround and #annotated-surround, but I don't want the users of those macros to see any complexity. The whole idea is to simplify their lives.

As long as I have your ear: Setting macro.provide.scope.control=true is supposed to "a local namespace in macros". What does this mean? Is the provided namespace independent of the default context, but with a single such space shared among all invocations of all macros? Or is a separate context provided for each macro invocation, even recursively? It has to be the latter because of $macro.parent, right?

And yet another question. Consider the following macro:

#macro( recursive $x )
  #if($x == 0)
    zero
  #else
    $x before . . . 
    #set($xMinusOne = $x - 1)
    #recursive($xMinusOne)
    . . . $x after
  #end
#end

#recursive( 4 ) yields:

4 before . . . 3 before . . . 2 before . . . 1 before . . . zero . . . 0 after . . . 0 after . . . 0 after . . . 4 after

Now I understand all those occurrences of "0": there's only one global $x, so assigning to it on the recursive calls smashes it and it doesn't get restored. But where on earth does that final "4" come from? For that matter, how is it that my first "surround" macro works to arbitrary depth; how come its final $x doesn't get smashed in inner calls?

Sorry to be so prolix, but I have been unable to find clear documentation in this matter.

like image 622
Larry Denenberg Avatar asked Nov 12 '22 20:11

Larry Denenberg


1 Answers

The problem is the combination of global variables, a name collision, and lazy rendering.

Let's walk through the rendering process for #@annotated-surround( "x" "y" )content#end:

  1. Rendering enters the annotated-surround macro. The context map contains:
    1. $x = String x
    2. $y = String y
    3. $bodyContent = Renderable content - note that the String output of this has not yet been evaluated.
  2. Rendering of the first line enters the surround macro. This updates the context map to:
    1. new $x = old $x = String x
    2. $y = String y
    3. $bodyContent = Renderable annotate:$y\n$bodyContent - note that the String output of this still has not yet been evaluated, it's still template code.
  3. Rendering outputs the first line of surround, producing the String surround:x.
  4. Rendering begins evaluating the second line of surround, which references $bodyContent.
    1. Rendering the first line of $bodyContent produces the String annotate:y.
    2. Rendering begins evaluating the second line of $bodyContent, which references $bodyContent.
      1. Rendering the first line of $bodyContent produces the String annotate:y.
      2. Rendering begins evaluating the second line of $bodyContent, which references $bodyContent.
        1. etc.

The solution is to remove part of the problem's combination. Global variables and lazy rendering are fundamental parts of how Velocity works, so you can't touch those. That leaves the name collision. What you need is for each macro's $bodyContent to be referred to with a different name. This is easily achieved by assigning it to new variables with unique names in each macro before invoking any other macros, and using the new variable in any invoked macro's body, like this:

#macro( surround $x )
  surround:$x
    $bodyContent
  /surround:$x
#end

#macro( annotated-surround $x $y )
  #set( $annotated-surround-content = $bodyContent )
  #@surround( $x )
    annotate:$y
    $annotated-surround-content
  #end
#end

Rendering of this version goes like this:

  1. Rendering enters the annotated-surround macro. The context map contains:
    1. $x = String x
    2. $y = String y
    3. $bodyContent = Renderable content - note that the String output of this has not yet been evaluated.
  2. Rendering of the first line executes the #set directive, adding a variable to the context map: $annotated-surround-content = current $bodyContent = Renderable content.
  3. Rendering of the second line enters the surround macro. This updates the context map to:
    1. new $x = old $x = String x
    2. $y = String y
    3. $annotated-surround-content = old $bodyContent = Renderable content
    4. $bodyContent = Renderable annotate:$y\n$annotated-surround-content
  4. Rendering outputs the first line of surround, producing the String surround:x.
  5. Rendering begins evaluating the second line of surround, which references $bodyContent.
    1. Rendering the first line of $bodyContent produces the String annotate:y.
    2. Rendering begins evaluating the second line of $bodyContent, which references $annotated-surround-content.
      1. Rendering $annotated-surround-content produces the String content.
  6. Rendering outputs the third line of surround, producing the String /surround:x.

The final rendered output is surround:x annotate:y content /surround:x. This approach can be generalized by applying such substitutions to all occurrences of $bodyContent that are inside the content of another macro call, each time using a variable name derived from the macro's name to ensure uniqueness. It won't work for recursive macros without something extra to distinguish each nested invocation, however.

Regarding the scope setting, all that does is add a $macro object to the context, which is unique to each macro invocation and can be used as a map. If you set $macro.myVar to something different in each of two nested macro calls, the outer macro's value for it will be unchanged when the inner one finishes. This does not help with the $bodyContent issue, however, because any reference to $macro inside a macro's $bodyContent will be resolved to the innermost macro when it's rendered.

Regarding the final 4 from #recursive( 4 ), that comes from a combination of macro arguments having local scope and being passed by name. For all but the outermost invocation of #recursive, the argument $x is a reference to the global context variable $xMinusOne - when they render the after line, the use of $x is actually resolved to looking up the current value of $xMinusOne in the global context. For the outermost invocation it is instead the constant value 4, and the arguments of the inner invocations go out of scope when they finish, so when the outermost one gets to the final line it's back to being 4.

like image 107
Douglas Avatar answered Jan 04 '23 02:01

Douglas