Skip to content

Commit bc4ab4a

Browse files
eamonnmcmanusEscapeVelocity Team
authored andcommitted
Add support for $bodyContent in macros.
If a macro is invoked not as `#foo()` but as `#@foo() ... #end`, then the variable `$bodyContent` is defined in the macro body to be the `...` content. RELNOTES=Added support for `#@foo() ... #end`, where the `...` is available in the body of the `foo` macro as `$bodyContent`. PiperOrigin-RevId: 499509223
1 parent 18d40b1 commit bc4ab4a

File tree

6 files changed

+129
-17
lines changed

6 files changed

+129
-17
lines changed

src/main/java/com/google/escapevelocity/DirectiveNode.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,15 +223,18 @@ public int getCount() {
223223
static class MacroCallNode extends DirectiveNode {
224224
private final String name;
225225
private final ImmutableList<ExpressionNode> thunks;
226+
private final Node bodyContent;
226227

227228
MacroCallNode(
228229
String resourceName,
229230
int lineNumber,
230231
String name,
231-
ImmutableList<ExpressionNode> argumentNodes) {
232+
ImmutableList<ExpressionNode> argumentNodes,
233+
Node bodyContent) {
232234
super(resourceName, lineNumber);
233235
this.name = name;
234236
this.thunks = argumentNodes;
237+
this.bodyContent = bodyContent;
235238
}
236239

237240
@Override
@@ -250,7 +253,7 @@ void render(EvaluationContext context, StringBuilder output) {
250253
+ ", got "
251254
+ thunks.size());
252255
}
253-
macro.render(context, thunks, output);
256+
macro.render(context, thunks, bodyContent, output);
254257
}
255258
}
256259
}

src/main/java/com/google/escapevelocity/ExpressionNode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ final void render(EvaluationContext context, StringBuilder output) {
4949
}
5050
throw evaluationException("Null value for " + this);
5151
}
52-
if (rendered instanceof Node) { // $x when we earlier did #define ($x) ... #end
52+
if (rendered instanceof Node) {
53+
// A macro's $bodyContent, or $x when we earlier did #define ($x) ... #end
5354
((Node) rendered).render(context, output);
5455
} else {
5556
output.append(rendered);

src/main/java/com/google/escapevelocity/Macro.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,34 +30,49 @@
3030
* means that we need to set each parameter variable to the node in the parse tree that corresponds
3131
* to it, and arrange for that node to be evaluated when the variable is actually referenced.
3232
*
33+
* <p>There are two ways to invoke a macro. {@code #m('foo', 'bar')} sets $x and $y. {@code
34+
* #@m('foo', 'bar') ... #end} sets $x and $y, and also sets $bodyContent to the template text
35+
* {@code ...}.
36+
*
3337
* @author [email protected] (Éamonn McManus)
3438
*/
3539
class Macro {
3640
private final int definitionLineNumber;
3741
private final String name;
3842
private final ImmutableList<String> parameterNames;
39-
private final Node body;
43+
private final Node macroBody;
4044

41-
Macro(int definitionLineNumber, String name, List<String> parameterNames, Node body) {
45+
Macro(int definitionLineNumber, String name, List<String> parameterNames, Node macroBody) {
4246
this.definitionLineNumber = definitionLineNumber;
4347
this.name = name;
4448
this.parameterNames = ImmutableList.copyOf(parameterNames);
45-
this.body = body;
49+
this.macroBody = macroBody;
4650
}
4751

4852
int parameterCount() {
4953
return parameterNames.size();
5054
}
5155

52-
void render(EvaluationContext context, List<ExpressionNode> thunks, StringBuilder output) {
56+
/**
57+
* Renders a call to this macro with the arguments in {@code thunks} and with a possibly-null
58+
* {@code bodyContent}. The {@code bodyContent} is non-null if the macro call looks like
59+
* {@code #@foo(...) ... #end}; the {@code #@} indicates that the text up to the matching
60+
* {@code #end} should be made available as the variable {@code $bodyContent} inside the macro.
61+
*/
62+
void render(
63+
EvaluationContext context,
64+
List<ExpressionNode> thunks,
65+
Node bodyContent,
66+
StringBuilder output) {
5367
try {
5468
Verify.verify(thunks.size() == parameterNames.size(), "Argument mismatch for %s", name);
5569
Map<String, ExpressionNode> parameterThunks = new LinkedHashMap<>();
5670
for (int i = 0; i < parameterNames.size(); i++) {
5771
parameterThunks.put(parameterNames.get(i), thunks.get(i));
5872
}
59-
EvaluationContext newContext = new MacroEvaluationContext(parameterThunks, context);
60-
body.render(newContext, output);
73+
EvaluationContext newContext =
74+
new MacroEvaluationContext(parameterThunks, context, bodyContent);
75+
macroBody.render(newContext, output);
6176
} catch (EvaluationException e) {
6277
EvaluationException newException = new EvaluationException(
6378
"In macro #" + name + " defined on line " + definitionLineNumber + ": " + e.getMessage());
@@ -85,15 +100,22 @@ void render(EvaluationContext context, List<ExpressionNode> thunks, StringBuilde
85100
static class MacroEvaluationContext implements EvaluationContext {
86101
private final Map<String, ExpressionNode> parameterThunks;
87102
private final EvaluationContext originalEvaluationContext;
103+
private final Node bodyContent;
88104

89105
MacroEvaluationContext(
90-
Map<String, ExpressionNode> parameterThunks, EvaluationContext originalEvaluationContext) {
106+
Map<String, ExpressionNode> parameterThunks,
107+
EvaluationContext originalEvaluationContext,
108+
Node bodyContent) {
91109
this.parameterThunks = parameterThunks;
92110
this.originalEvaluationContext = originalEvaluationContext;
111+
this.bodyContent = bodyContent;
93112
}
94113

95114
@Override
96115
public Object getVar(String var) {
116+
if (bodyContent != null && var.equals("bodyContent")) {
117+
return bodyContent;
118+
}
97119
ExpressionNode thunk = parameterThunks.get(var);
98120
if (thunk == null) {
99121
return originalEvaluationContext.getVar(var);
@@ -109,7 +131,9 @@ public Object getVar(String var) {
109131

110132
@Override
111133
public boolean varIsDefined(String var) {
112-
return parameterThunks.containsKey(var) || originalEvaluationContext.varIsDefined(var);
134+
return parameterThunks.containsKey(var)
135+
|| (bodyContent != null && var.equals("bodyContent"))
136+
|| originalEvaluationContext.varIsDefined(var);
113137
}
114138

115139
@Override

src/main/java/com/google/escapevelocity/Parser.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ private Node parseNode() throws IOException {
263263
return parseHashSquare();
264264
case '{':
265265
return parseDirective();
266+
case '@':
267+
return parseMacroCallWithBody();
266268
default:
267269
if (isAsciiLetter(c)) {
268270
return parseDirective();
@@ -385,7 +387,7 @@ private Node parseDirective() throws IOException {
385387
case "macro":
386388
return parseMacroDefinition();
387389
default:
388-
node = parsePossibleMacroCall(directive);
390+
node = parseMacroCall("#", directive);
389391
}
390392
// Velocity skips a newline after any directive. In the case of #if etc, we'll have done this
391393
// when we stopped scanning the body at #end, so in those cases we return directly rather than
@@ -596,8 +598,9 @@ private Node parseMacroDefinition() throws IOException {
596598
* ...
597599
* }</pre>
598600
*/
599-
private Node parsePossibleMacroCall(String directive) throws IOException {
600-
StringBuilder sb = new StringBuilder("#").append(directive);
601+
private Node parseMacroCall(String prefix, String directive) throws IOException {
602+
int startLine = lineNumber();
603+
StringBuilder sb = new StringBuilder(prefix).append(directive);
601604
while (Character.isWhitespace(c)) {
602605
sb.appendCodePoint(c);
603606
next();
@@ -629,8 +632,27 @@ private Node parsePossibleMacroCall(String directive) throws IOException {
629632
next();
630633
}
631634
}
635+
Node bodyContent;
636+
if (prefix.equals("#")) {
637+
bodyContent = null;
638+
} else {
639+
ParseResult parseResult =
640+
skipNewlineAndParseToStop(
641+
END_CLASS, () -> "#@" + directive + " starting on line " + startLine);
642+
bodyContent = Node.cons(resourceName, startLine, parseResult.nodes);
643+
}
632644
return new DirectiveNode.MacroCallNode(
633-
resourceName, lineNumber(), directive, parameterNodes.build());
645+
resourceName, lineNumber(), directive, parameterNodes.build(), bodyContent);
646+
}
647+
648+
private Node parseMacroCallWithBody() throws IOException {
649+
assert c == '@';
650+
next();
651+
if (!isAsciiLetter(c)) {
652+
return parsePlainText("#@");
653+
}
654+
String id = parseId("#@");
655+
return parseMacroCall("#@", id);
634656
}
635657

636658
/**

src/main/java/com/google/escapevelocity/ReferenceNode.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ EvaluationException evaluationExceptionInThis(String message) {
5252

5353
/**
5454
* Evaluates the first part of a complex reference, for example {@code $foo} in {@code $foo.bar}.
55-
* It must not be null, and it must not be the result of a {@code #define}.
55+
* It must not be null, and it must not be a macro's {@code $bodyContent} or the result of a
56+
* {@code #define}.
5657
*/
5758
Object evaluateLhs(ReferenceNode lhs, EvaluationContext context) {
5859
Object lhsValue = lhs.evaluate(context);
5960
if (lhsValue == null) {
6061
throw evaluationExceptionInThis(lhs + " must not be null");
6162
} else if (lhsValue instanceof Node) {
62-
throw evaluationExceptionInThis(lhs + " comes from #define");
63+
throw evaluationExceptionInThis(lhs + " comes from #define or is a macro's $bodyContent");
6364
}
6465
return lhsValue;
6566
}

src/test/java/com/google/escapevelocity/TemplateTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,67 @@ public void macroArgumentMismatch() {
13251325
expectException(template, "Wrong number of arguments to #twoArgs: expected 2, got 1");
13261326
}
13271327

1328+
@Test
1329+
public void macroWithBody() {
1330+
// The #if ($bodyContent) is needed because Velocity treats $bodyContent as undefined if the
1331+
// macro is invoked as #withBody rather than #@withBody. The documented examples use
1332+
// $!bodyContent but that doesn't work if there is no body and Velocity is in strict ref mode.
1333+
String template =
1334+
"#macro(withBody $x)\n"
1335+
+ "$x\n#if ($bodyContent) $bodyContent #end\n"
1336+
+ "#end\n"
1337+
+ "#withBody('foo')\n"
1338+
+ "#@withBody('bar')\n"
1339+
+ "#if (0 == 1) what #else yes #end\n"
1340+
+ "#end ## end of #@withBody";
1341+
compare(template);
1342+
}
1343+
1344+
@Test
1345+
public void nestedMacrosWithBodies() {
1346+
String template =
1347+
"#macro(outer $x)\n"
1348+
+ "$x $!bodyContent $x\n"
1349+
+ "#end\n"
1350+
+ "#macro(inner $x)\n"
1351+
+ "[$x] $!bodyContent [$x]\n"
1352+
+ "#end\n"
1353+
+ "#@outer('foo')\n"
1354+
+ "before inner\n"
1355+
+ "#@inner('bar')\n"
1356+
+ "in inner\n"
1357+
+ "#end ## inner\n"
1358+
+ "after inner\n"
1359+
+ "#end ## outer\n";
1360+
compare(template);
1361+
}
1362+
1363+
@Test
1364+
public void bodyContentTwice() {
1365+
String template =
1366+
"#macro(one)\n"
1367+
+ "[$bodyContent]\n"
1368+
+ "#end\n"
1369+
+ "#macro(two)\n"
1370+
+ "#set($bodyContentCopy = \"$bodyContent\")\n"
1371+
+ "<#@one()$bodyContentCopy#end>\n"
1372+
+ "#end\n"
1373+
+ "#@two()foo#end\n";
1374+
// Velocity doesn't handle this well. It shouldn't be necessary to make $bodyContentCopy; we
1375+
// should just be able to use $bodyContent. But that gets an exception: "Reference $bodyContent
1376+
// evaluated to object org.apache.velocity.runtime.directive.Block$Reference whose toString()
1377+
// method returned null". By evaluating it into $bodyContentCopy we avoid whatever confusion
1378+
// that was. This is probably related to
1379+
// https://issues.apache.org/jira/projects/VELOCITY/issues/VELOCITY-940.
1380+
compare(template);
1381+
}
1382+
1383+
@Test
1384+
public void notMacroCall() {
1385+
compare("#@ foo");
1386+
compare("#@foo no parens");
1387+
}
1388+
13281389
@Test
13291390
public void unclosedBlockQuote() {
13301391
String template = "foo\nbar #[[\nblah\nblah";

0 commit comments

Comments
 (0)