Author: nbubna
Date: Sun Jan 25 17:24:29 2009
New Revision: 737539
URL: http://svn.apache.org/viewvc?rev=737539&view=rev
Log:
VELOCITY-686 bring parity between block macro bodies and #define'd references
Added:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Block.java
(with props)
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/context/ProxyVMContext.java
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/BlockMacro.java
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Define.java
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/RuntimeMacro.java
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java
velocity/engine/trunk/src/java/org/apache/velocity/runtime/parser/node/Node.java
velocity/engine/trunk/src/test/org/apache/velocity/test/BlockMacroTestCase.java
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/context/ProxyVMContext.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/context/ProxyVMContext.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/context/ProxyVMContext.java
(original)
+++
velocity/engine/trunk/src/java/org/apache/velocity/context/ProxyVMContext.java
Sun Jan 25 17:24:29 2009
@@ -28,6 +28,7 @@
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.parser.ParserTreeConstants;
import org.apache.velocity.runtime.parser.node.ASTReference;
@@ -107,6 +108,24 @@
}
/**
+ * Used to put Velocity macro bodyContext arguments into this context.
+ *
+ * @param context rendering context
+ * @param macroArgumentName name of the macro argument that we received
+ * @param literalMacroArgumentName ".literal.$"+macroArgumentName
+ * @param argumentValue actual value of the macro body
+ *
+ * @throws MethodInvocationException
+ */
+ public void addVMProxyArg(InternalContextAdapter context,
+ String macroArgumentName,
+ String literalMacroArgumentName,
+ Renderable argumentValue) throws
MethodInvocationException
+ {
+ localcontext.put(macroArgumentName, argumentValue);
+ }
+
+ /**
* AST nodes that are considered constants can be directly
* saved into the context. Dynamic values are stored in
* another argument hashmap.
@@ -124,7 +143,6 @@
case ParserTreeConstants.JJTMAP:
case ParserTreeConstants.JJTSTRINGLITERAL:
case ParserTreeConstants.JJTTEXT:
- case ParserTreeConstants.JJTBLOCK:
return (false);
default:
return (true);
@@ -220,28 +238,6 @@
return obj;
}
}
- else if (type == ParserTreeConstants.JJTBLOCK)
- {
- // this happens for #...@somemacro($arg1 $arg2) bodyAST #end
calls
- try
- {
- // astNode is actually BlockMacro.BlockMacroContainer
which contains a Writer internally although
- // we seem to pass null here
- astNode.render(innerContext, null);
- // return an empty string because the Node already
rendered all content
- return "";
- }
- catch (RuntimeException e)
- {
- throw e;
- }
- catch (Exception e)
- {
- String msg = "ProxyVMContext.get() : error rendering
reference";
- rsvc.getLog().error(msg, e);
- throw new VelocityException(msg, e);
- }
- }
else if (type == ParserTreeConstants.JJTTEXT)
{
// this really shouldn't happen. text is just a throwaway arg
for #foreach()
Added:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Block.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Block.java?rev=737539&view=auto
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Block.java
(added)
+++
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Block.java
Sun Jan 25 17:24:29 2009
@@ -0,0 +1,173 @@
+package org.apache.velocity.runtime.directive;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.io.Writer;
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.commons.lang.text.StrBuilder;
+import org.apache.velocity.context.InternalContextAdapter;
+import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.log.Log;
+import org.apache.velocity.runtime.parser.node.Node;
+
+/**
+ * Directive that puts an unrendered AST block in the context
+ * under the specified key, postponing rendering until the
+ * reference is used and rendered.
+ *
+ * @author Andrew Tetlaw
+ * @author Nathan Bubna
+ * @author <a href="mailto:[email protected]">Jarkko Viinamaki</a>
+ * @since 1.7
+ * @version $Id: Block.java 686842 2008-08-18 18:29:31Z nbubna $
+ */
+public abstract class Block extends Directive
+{
+ protected Node block;
+ protected Log log;
+ protected int maxDepth;
+ protected String definingTemplate;
+ protected String key;
+
+ /**
+ * Return type of this directive.
+ */
+ public int getType()
+ {
+ return BLOCK;
+ }
+
+ /**
+ * simple init - get the key
+ */
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node
node)
+ throws TemplateInitException
+ {
+ super.init(rs, context, node);
+
+ log = rs.getLog();
+
+ /**
+ * No checking is done. We just grab the last child node and assume
+ * that it's the block!
+ */
+ block = node.jjtGetChild(node.jjtGetNumChildren() - 1);
+
+ /**
+ * keep tabs on the template this came from
+ */
+ definingTemplate = context.getCurrentTemplateName();
+ }
+
+ /**
+ * Creates a string identifying the source and location of the block
+ * definition, and the current template being rendered if that is
+ * different.
+ */
+ protected String id(InternalContextAdapter context)
+ {
+ StrBuilder str = new StrBuilder(100)
+ .append("block $").append(key)
+ .append(" (defined in ").append(definingTemplate)
+ .append(" [line ").append(getLine())
+ .append(", column ").append(getColumn()).append("])");
+
+ if (!context.getCurrentTemplateName().equals(definingTemplate))
+ {
+ str.append(" used in ").append(context.getCurrentTemplateName());
+ }
+ return str.toString();
+ }
+
+ /**
+ * actual class placed in the context, holds the context
+ * being used for the render, as well as the parent (which already holds
+ * everything else we need).
+ */
+ public static class Reference implements Renderable
+ {
+ private InternalContextAdapter context;
+ private Block parent;
+ private int depth;
+
+ public Reference(InternalContextAdapter context, Block parent)
+ {
+ this.context = context;
+ this.parent = parent;
+ }
+
+ /**
+ * Render the AST of this block into the writer using the context.
+ */
+ public boolean render(InternalContextAdapter context, Writer writer)
+ {
+ try
+ {
+ depth++;
+ if (depth > parent.maxDepth)
+ {
+ /* this is only a debug message, as recursion can
+ * happen in quasi-innocent situations and is relatively
+ * harmless due to how we handle it here.
+ * this is more to help anyone nuts enough to intentionally
+ * use recursive block definitions and having problems
+ * pulling it off properly.
+ */
+ parent.log.debug("Max recursion depth reached for
"+parent.id(context));
+ depth--;
+ return false;
+ }
+ else
+ {
+ parent.block.render(context, writer);
+ depth--;
+ return true;
+ }
+ }
+ catch (IOException e)
+ {
+ String msg = "Failed to render "+parent.id(context)+" to
writer";
+ parent.log.error(msg, e);
+ throw new RuntimeException(msg, e);
+ }
+ catch (VelocityException ve)
+ {
+ String msg = "Failed to render "+parent.id(context)+" due to
"+ve;
+ parent.log.error(msg, ve);
+ throw ve;
+ }
+ }
+
+ public String toString()
+ {
+ Writer writer = new StringWriter();
+ if (render(context, writer))
+ {
+ return writer.toString();
+ }
+ return null;
+ }
+ }
+}
Propchange:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Block.java
------------------------------------------------------------------------------
svn:executable = *
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/BlockMacro.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/BlockMacro.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/BlockMacro.java
(original)
+++
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/BlockMacro.java
Sun Jan 25 17:24:29 2009
@@ -21,16 +21,12 @@
import java.io.IOException;
import java.io.Writer;
-
import org.apache.velocity.context.InternalContextAdapter;
-import org.apache.velocity.exception.MethodInvocationException;
-import org.apache.velocity.exception.ParseErrorException;
-import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.Renderable;
+import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
-import org.apache.velocity.runtime.parser.ParserTreeConstants;
import org.apache.velocity.runtime.parser.node.Node;
-import org.apache.velocity.runtime.parser.node.SimpleNode;
/**
* BlockMacro directive is used to invoke Velocity macros with normal
parameters and a macro body.
@@ -58,36 +54,23 @@
* bodyContent reference name is configurable (see velocity.properties).
*
* @author <a href="mailto:[email protected]">Jarkko Viinamaki</a>
- * @since 1.6.2
+ * @since 1.7
* @version $Id$
*/
-public class BlockMacro extends Directive
+public class BlockMacro extends Block
{
private String name;
private RuntimeMacro macro;
- private Node macroBody;
public BlockMacro(String name)
{
this.name = name;
}
- /**
- * Return name of this directive.
- * @return The name of this directive.
- */
+ // This is required, but not actually used.
public String getName()
{
- return "blockmacro";
- }
-
- /**
- * Return type of this directive.
- * @return The type of this directive.
- */
- public int getType()
- {
- return BLOCK;
+ throw new UnsupportedOperationException("BlockMacro is not actually a
named macro.");
}
/**
@@ -98,13 +81,16 @@
* @param node
* @throws TemplateInitException
*/
- public void init(RuntimeServices rs, InternalContextAdapter context,
- Node node)
+ public void init(RuntimeServices rs, InternalContextAdapter context, Node
node)
throws TemplateInitException
{
super.init(rs, context, node);
- macroBody = node.jjtGetChild(node.jjtGetNumChildren() - 1);
+ // get name of the reference that refers to AST block passed to block
macro call
+ key = rsvc.getString(RuntimeConstants.VM_BODY_REFERENCE,
"bodyContent");
+
+ // use the macro max depth for bodyContent max depth as well
+ maxDepth = rs.getInt(RuntimeConstants.VM_MAX_DEPTH);
macro = new RuntimeMacro(name);
macro.setLocation(getLine(), getColumn(), getTemplateName());
@@ -123,35 +109,7 @@
public boolean render(InternalContextAdapter context, Writer writer, Node
node)
throws IOException
{
- return macro.render(context,
- writer,
- node,
- new BlockMacroContainer(macroBody,
ParserTreeConstants.JJTBLOCK, writer));
+ return macro.render(context, writer, node, new Reference(context,
this));
}
- /**
- * BlockMacro body is wrapped in this container.
- *
- * With this we can have reference to the output Writer in
ProxyVMContext.get call.
- * This approach lets us write directly to the output instead of using
costly StringWriter.
- */
- public static class BlockMacroContainer extends SimpleNode
- {
- private Node node;
- private Writer w;
-
- public BlockMacroContainer(Node node, int type, Writer writer)
- {
- super(type);
- this.node = node;
- this.w = writer;
- }
-
- public boolean render( InternalContextAdapter context, Writer writer)
- throws IOException, MethodInvocationException,
ParseErrorException, ResourceNotFoundException
- {
- return node.render(context, writer != null ? writer : w);
- }
- }
}
-
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Define.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Define.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Define.java
(original)
+++
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/Define.java
Sun Jan 25 17:24:29 2009
@@ -20,17 +20,10 @@
*/
import java.io.Writer;
-import java.io.IOException;
-import java.io.StringWriter;
-
-import org.apache.commons.lang.text.StrBuilder;
import org.apache.velocity.context.InternalContextAdapter;
import org.apache.velocity.exception.TemplateInitException;
-import org.apache.velocity.exception.VelocityException;
-import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
-import org.apache.velocity.runtime.log.Log;
import org.apache.velocity.runtime.parser.node.Node;
/**
@@ -42,14 +35,8 @@
* @author Nathan Bubna
* @version $Id: Define.java 686842 2008-08-18 18:29:31Z nbubna $
*/
-public class Define extends Directive
+public class Define extends Block
{
- private String key;
- private Node block;
- private Log log;
- private int maxDepth;
- private String definingTemplate;
-
/**
* Return name of this directive.
*/
@@ -59,14 +46,6 @@
}
/**
- * Return type of this directive.
- */
- public int getType()
- {
- return BLOCK;
- }
-
- /**
* simple init - get the key
*/
public void init(RuntimeServices rs, InternalContextAdapter context, Node
node)
@@ -74,14 +53,6 @@
{
super.init(rs, context, node);
- log = rs.getLog();
-
- /*
- * default max depth of two is used because intentional recursion is
- * unlikely and discouraged, so make unintentional ones end fast
- */
- maxDepth = rs.getInt(RuntimeConstants.DEFINE_DIRECTIVE_MAXDEPTH, 2);
-
/*
* first token is the name of the block. We don't even check the
format,
* just assume it looks like this: $block_name. Should we check if it
has
@@ -89,16 +60,11 @@
*/
key = node.jjtGetChild(0).getFirstToken().image.substring(1);
- /**
- * No checking is done. We just grab the second child node and assume
- * that it's the block!
- */
- block = node.jjtGetChild(1);
-
- /**
- * keep tabs on the template this came from
+ /*
+ * default max depth of two is used because intentional recursion is
+ * unlikely and discouraged, so make unintentional ones end fast
*/
- definingTemplate = context.getCurrentTemplateName();
+ maxDepth = rs.getInt(RuntimeConstants.DEFINE_DIRECTIVE_MAXDEPTH, 2);
}
/**
@@ -107,104 +73,11 @@
*/
public boolean render(InternalContextAdapter context, Writer writer, Node
node)
{
- /* put a Block instance into the context,
+ /* put a Block.Reference instance into the context,
* using the user-defined key, for later inline rendering.
*/
- context.put(key, new Block(context, this));
+ context.put(key, new Reference(context, this));
return true;
}
- /**
- * Creates a string identifying the source and location of the block
- * definition, and the current template being rendered if that is
- * different.
- */
- protected String id(InternalContextAdapter context)
- {
- StrBuilder str = new StrBuilder(100)
- .append("block $").append(key)
- .append(" (defined in ").append(definingTemplate)
- .append(" [line ").append(getLine())
- .append(", column ").append(getColumn()).append("])");
-
- if (!context.getCurrentTemplateName().equals(definingTemplate))
- {
- str.append(" used in ").append(context.getCurrentTemplateName());
- }
-
- return str.toString();
- }
-
- /**
- * actual class placed in the context, holds the context and writer
- * being used for the render, as well as the parent (which already holds
- * everything else we need).
- */
- public static class Block implements Renderable
- {
- private InternalContextAdapter context;
- private Define parent;
- private int depth;
-
- public Block(InternalContextAdapter context, Define parent)
- {
- this.context = context;
- this.parent = parent;
- }
-
- /**
- *
- */
- public boolean render(InternalContextAdapter context, Writer writer)
- {
- try
- {
- depth++;
- if (depth > parent.maxDepth)
- {
- /* this is only a debug message, as recursion can
- * happen in quasi-innocent situations and is relatively
- * harmless due to how we handle it here.
- * this is more to help anyone nuts enough to intentionally
- * use recursive block definitions and having problems
- * pulling it off properly.
- */
- parent.log.debug("Max recursion depth reached for
"+parent.id(context));
- depth--;
- return false;
- }
- else
- {
- parent.block.render(context, writer);
- depth--;
- return true;
- }
- }
- catch (IOException e)
- {
- String msg = "Failed to render "+parent.id(context)+" to
writer";
- parent.log.error(msg, e);
- throw new RuntimeException(msg, e);
- }
- catch (VelocityException ve)
- {
- String msg = "Failed to render "+parent.id(context)+" due to
"+ve;
- parent.log.error(msg, ve);
- throw ve;
- }
- }
-
- public String toString()
- {
- Writer stringwriter = new StringWriter();
- if(render(context,stringwriter))
- {
- return stringwriter.toString();
- }
- else
- {
- return null;
- }
- }
- }
}
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/RuntimeMacro.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/RuntimeMacro.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/RuntimeMacro.java
(original)
+++
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/RuntimeMacro.java
Sun Jan 25 17:24:29 2009
@@ -30,6 +30,7 @@
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.log.Log;
@@ -229,7 +230,7 @@
* @throws MethodInvocationException
*/
public boolean render(InternalContextAdapter context, Writer writer,
- Node node, Node body)
+ Node node, Renderable body)
throws IOException, ResourceNotFoundException,
ParseErrorException, MethodInvocationException
{
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java
(original)
+++
velocity/engine/trunk/src/java/org/apache/velocity/runtime/directive/VelocimacroProxy.java
Sun Jan 25 17:24:29 2009
@@ -29,6 +29,7 @@
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.TemplateInitException;
import org.apache.velocity.exception.VelocityException;
+import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.log.Log;
@@ -145,7 +146,8 @@
* @throws MethodInvocationException
* @throws MacroOverflowException
*/
- public boolean render(InternalContextAdapter context, Writer writer, Node
node, Node body)
+ public boolean render(InternalContextAdapter context, Writer writer,
+ Node node, Renderable body)
throws IOException, MethodInvocationException,
MacroOverflowException
{
// wrap the current context and add the macro arguments
Modified:
velocity/engine/trunk/src/java/org/apache/velocity/runtime/parser/node/Node.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/java/org/apache/velocity/runtime/parser/node/Node.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/java/org/apache/velocity/runtime/parser/node/Node.java
(original)
+++
velocity/engine/trunk/src/java/org/apache/velocity/runtime/parser/node/Node.java
Sun Jan 25 17:24:29 2009
@@ -27,6 +27,7 @@
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
+import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.parser.Token;
/**
@@ -37,7 +38,7 @@
* @version $Id$
*/
-public interface Node
+public interface Node extends Renderable
{
/** This method is called after the node has been made the current
* node. It indicates that child nodes can now be added to it. */
Modified:
velocity/engine/trunk/src/test/org/apache/velocity/test/BlockMacroTestCase.java
URL:
http://svn.apache.org/viewvc/velocity/engine/trunk/src/test/org/apache/velocity/test/BlockMacroTestCase.java?rev=737539&r1=737538&r2=737539&view=diff
==============================================================================
---
velocity/engine/trunk/src/test/org/apache/velocity/test/BlockMacroTestCase.java
(original)
+++
velocity/engine/trunk/src/test/org/apache/velocity/test/BlockMacroTestCase.java
Sun Jan 25 17:24:29 2009
@@ -19,18 +19,7 @@
* under the License.
*/
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-import junit.framework.TestCase;
-
-import org.apache.velocity.VelocityContext;
-import org.apache.velocity.app.Velocity;
import org.apache.velocity.runtime.RuntimeConstants;
-import org.apache.velocity.test.misc.TestLogChute;
-import org.apache.velocity.test.provider.ForeachMethodCallHelper;
/**
* This class tests the BlockMacro functionality.
@@ -102,5 +91,28 @@
engine.setProperty(RuntimeConstants.VM_ARGUMENTS_STRICT, Boolean.TRUE);
assertEvalEquals(" ", "#macro(foo)#end #...@foo() junk #end");
}
+
+ public void testVelocity686() throws Exception
+ {
+ String template = "#macro(foo)#set( $x = $bodyContent )#end"+
+ "#...@foo()b#end a $x ";
+ assertEvalEquals(" a b ", template);
+ }
+
+ public void testNestedBlockMacro()
+ {
+ String template = "#macro(foo)foo:$bodyContent#end"+
+ "#macro(bar)bar:$bodyContent#end"+
+ "#...@foo()foo,#...@bar()bar#end#end";
+ assertEvalEquals("foo:foo,bar:bar", template);
+ }
+
+ public void testRecursiveBlockMacro()
+ {
+ engine.setProperty(RuntimeConstants.VM_MAX_DEPTH, 3);
+ String template = "#macro(foo)start:$bodyContent#end"+
+ "#...@foo()call:$bodyContent#end";
+ assertEvalEquals("start:call:call:call:$bodyContent", template);
+ }
}