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.
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
:
annotated-surround
macro. The context map contains:
$x
= String x
$y
= String y
$bodyContent
= Renderable content
- note that the String output of this has not yet been evaluated.surround
macro. This updates the context map to:
$x
= old $x
= String x
$y
= String y
$bodyContent
= Renderable annotate:$y\n$bodyContent
- note that the String output of this still has not yet been evaluated, it's still template code.surround
, producing the String surround:x
.surround
, which references $bodyContent
.
$bodyContent
produces the String annotate:y
.$bodyContent
, which references $bodyContent
.
$bodyContent
produces the String annotate:y
.$bodyContent
, which references $bodyContent
.
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:
annotated-surround
macro. The context map contains:
$x
= String x
$y
= String y
$bodyContent
= Renderable content
- note that the String output of this has not yet been evaluated.#set
directive, adding a variable to the context map: $annotated-surround-content
= current $bodyContent
= Renderable content
.surround
macro. This updates the context map to:
$x
= old $x
= String x
$y
= String y
$annotated-surround-content
= old $bodyContent
= Renderable content
$bodyContent
= Renderable annotate:$y\n$annotated-surround-content
surround
, producing the String surround:x
.surround
, which references $bodyContent
.
$bodyContent
produces the String annotate:y
.$bodyContent
, which references $annotated-surround-content
.
$annotated-surround-content
produces the String content
.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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With