This is an automated email from the ASF dual-hosted git repository. ddekany pushed a commit to branch 2.3-gae in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit 7e6b0c0302e27ec767056c6930fbd8b88199e96e Author: ddekany <[email protected]> AuthorDate: Wed Oct 23 20:58:46 2019 +0200 Added new special variable, .args. This evaluates to a hash (in macros), or sequence (in functions) that contains all the arguments. This is useful for operations that act on all the arguments uniformly, like for example to pass the arguments to ?spread_args(...). --- src/main/java/freemarker/core/BuiltinVariable.java | 30 ++-- src/main/java/freemarker/core/Macro.java | 160 ++++++++++++++++----- src/main/javacc/FTL.jj | 13 +- src/manual/en_US/book.xml | 101 +++++++++++++ .../freemarker/core/ArgsSpecialVariableTest.java | 160 +++++++++++++++++++++ 5 files changed, 422 insertions(+), 42 deletions(-) diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java index b42dcb3..7436dcb 100644 --- a/src/main/java/freemarker/core/BuiltinVariable.java +++ b/src/main/java/freemarker/core/BuiltinVariable.java @@ -78,8 +78,10 @@ final class BuiltinVariable extends Expression { static final String GET_OPTIONAL_TEMPLATE_CC = "getOptionalTemplate"; static final String CALLER_TEMPLATE_NAME = "caller_template_name"; static final String CALLER_TEMPLATE_NAME_CC = "callerTemplateName"; + static final String ARGS = "args"; static final String[] SPEC_VAR_NAMES = new String[] { // IMPORTANT! Keep this sorted alphabetically! + ARGS, AUTO_ESC_CC, AUTO_ESC, CALLER_TEMPLATE_NAME_CC, @@ -256,21 +258,33 @@ final class BuiltinVariable extends Expression { return GetOptionalTemplateMethod.INSTANCE_CC; } if (name == CALLER_TEMPLATE_NAME || name == CALLER_TEMPLATE_NAME_CC) { - Context ctx = env.getCurrentMacroContext(); - if (ctx == null) { - throw new TemplateException( - "Can't get ." + name + " here, as there's no macro or function (that's " - + "implemented in the template) call in context.", env); - } - TemplateObject callPlace = ctx.callPlace; + TemplateObject callPlace = getRequiredMacroContext(env).callPlace; String name = callPlace != null ? callPlace.getTemplate().getName() : null; return name != null ? new SimpleScalar(name) : TemplateScalarModel.EMPTY_STRING; } - + if (name == ARGS) { + TemplateModel args = getRequiredMacroContext(env).getArgsSpecialVariableValue(); + if (args == null) { + // Should be impossible, as the parser checks this condition already. + throw new _MiscTemplateException(this, "The \"", ARGS, "\" special variable wasn't initialized.", name); + } + return args; + } + throw new _MiscTemplateException(this, "Invalid special variable: ", name); } + private Context getRequiredMacroContext(Environment env) throws TemplateException { + Context ctx = env.getCurrentMacroContext(); + if (ctx == null) { + throw new TemplateException( + "Can't get ." + name + " here, as there's no macro or function (that's " + + "implemented in the template) call in context.", env); + } + return ctx; + } + @Override public String toString() { return "." + name; diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java index 6527e80..b64e9e4 100644 --- a/src/main/java/freemarker/core/Macro.java +++ b/src/main/java/freemarker/core/Macro.java @@ -27,8 +27,11 @@ import java.util.List; import java.util.Map; import freemarker.template.Configuration; +import freemarker.template.SimpleHash; +import freemarker.template.SimpleSequence; import freemarker.template.TemplateException; import freemarker.template.TemplateHashModelEx; +import freemarker.template.TemplateHashModelEx2; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelIterator; @@ -45,7 +48,7 @@ public final class Macro extends TemplateElement implements TemplateModel { static final Macro DO_NOTHING_MACRO = new Macro(".pass", Collections.EMPTY_MAP, - null, false, + null, false, false, TemplateElements.EMPTY); final static int TYPE_MACRO = 0; @@ -55,6 +58,7 @@ public final class Macro extends TemplateElement implements TemplateModel { private final String[] paramNames; private final Map<String, Expression> paramNamesWithDefault; private final SpreadArgs spreadArgs; + private boolean requireArgsSpecialVariable; private final String catchAllParamName; private final boolean function; private final Object namespaceLookupKey; @@ -66,7 +70,7 @@ public final class Macro extends TemplateElement implements TemplateModel { */ Macro(String name, Map<String, Expression> paramNamesWithDefault, - String catchAllParamName, boolean function, + String catchAllParamName, boolean function, boolean requireArgsSpecialVariable, TemplateElements children) { // Attention! Keep this constructor in sync with the other constructor! this.name = name; @@ -74,6 +78,7 @@ public final class Macro extends TemplateElement implements TemplateModel { this.paramNames = paramNamesWithDefault.keySet().toArray(new String[0]); this.catchAllParamName = catchAllParamName; this.spreadArgs = null; + this.requireArgsSpecialVariable = requireArgsSpecialVariable; this.function = function; this.setChildren(children); this.namespaceLookupKey = this; @@ -82,7 +87,7 @@ public final class Macro extends TemplateElement implements TemplateModel { /** * Copy-constructor with replacing {@link #spreadArgs} (with the quirk that the parent of the - * child elements will stay the copied macro). + * child AST elements will stay the copied macro). * * @param spreadArgs Usually {@code null}; used by {@link BuiltInsForCallables.spread_argsBI} to * set arbitrary default value to parameters. Note that the defaults aren't @@ -95,12 +100,17 @@ public final class Macro extends TemplateElement implements TemplateModel { this.paramNames = that.paramNames; this.catchAllParamName = that.catchAllParamName; this.spreadArgs = spreadArgs; // Using the argument value here + this.requireArgsSpecialVariable = that.requireArgsSpecialVariable; this.function = that.function; this.namespaceLookupKey = that.namespaceLookupKey; super.copyFieldsFrom(that); // Attention! Keep this constructor in sync with the other constructor! } + boolean getRequireArgsSpecialVariable() { + return requireArgsSpecialVariable; + } + public String getCatchAll() { return catchAllParamName; } @@ -210,6 +220,7 @@ public final class Macro extends TemplateElement implements TemplateModel { final List<String> nestedContentParameterNames; final LocalContextStack prevLocalContextStack; final Context prevMacroContext; + TemplateModel argsSpecialVariableValue; Context(Environment env, TemplateObject callPlace, @@ -221,41 +232,55 @@ public final class Macro extends TemplateElement implements TemplateModel { this.prevLocalContextStack = env.getLocalContextStack(); this.prevMacroContext = env.getCurrentMacroContext(); } - - + Macro getMacro() { return Macro.this; } - // Set default parameters, check if all the required parameters are defined. + /** + * Set default parameters, check if all the required parameters are defined. Also sets the value of + * {@code .args}, if that was requested. + */ void checkParamsSetAndApplyDefaults(Environment env) throws TemplateException { - boolean resolvedAnArg, hasUnresolvedArg; - Expression firstUnresolvedExpression; - InvalidReferenceException firstReferenceException; - do { - firstUnresolvedExpression = null; - firstReferenceException = null; - resolvedAnArg = hasUnresolvedArg = false; - for (int i = 0; i < paramNames.length; ++i) { - String argName = paramNames[i]; - if (localVars.get(argName) == null) { + boolean resolvedADefaultValue, hasUnresolvedDefaultValue; + Expression firstUnresolvedDefaultValueExpression; + InvalidReferenceException firstInvalidReferenceExceptionForDefaultValue; + + final TemplateModel[] argsSpecVarDraft; + if (Macro.this.requireArgsSpecialVariable) { + argsSpecVarDraft = new TemplateModel[paramNames.length]; + } else { + argsSpecVarDraft = null; + } + do { // Retried if there are unresolved defaults left + firstUnresolvedDefaultValueExpression = null; + firstInvalidReferenceExceptionForDefaultValue = null; + resolvedADefaultValue = hasUnresolvedDefaultValue = false; + for (int paramIndex = 0; paramIndex < paramNames.length; ++paramIndex) { + final String argName = paramNames[paramIndex]; + final TemplateModel argValue = localVars.get(argName); + if (argValue == null) { Expression defaultValueExp = paramNamesWithDefault.get(argName); if (defaultValueExp != null) { try { - TemplateModel tm = defaultValueExp.eval(env); - if (tm == null) { - if (!hasUnresolvedArg) { - firstUnresolvedExpression = defaultValueExp; - hasUnresolvedArg = true; + TemplateModel defaultValue = defaultValueExp.eval(env); + if (defaultValue == null) { + if (!hasUnresolvedDefaultValue) { + firstUnresolvedDefaultValueExpression = defaultValueExp; + hasUnresolvedDefaultValue = true; } } else { - localVars.put(argName, tm); - resolvedAnArg = true; + localVars.put(argName, defaultValue); + resolvedADefaultValue = true; + + if (argsSpecVarDraft != null) { + argsSpecVarDraft[paramIndex] = defaultValue; + } } } catch (InvalidReferenceException e) { - if (!hasUnresolvedArg) { - hasUnresolvedArg = true; - firstReferenceException = e; + if (!hasUnresolvedDefaultValue) { + hasUnresolvedDefaultValue = true; + firstInvalidReferenceExceptionForDefaultValue = e; } } } else if (!env.isClassicCompatible()) { @@ -265,7 +290,7 @@ public final class Macro extends TemplateElement implements TemplateModel { "When calling ", (isFunction() ? "function" : "macro"), " ", new _DelayedJQuote(name), ", required parameter ", new _DelayedJQuote(argName), - " (parameter #", Integer.valueOf(i + 1), ") was ", + " (parameter #", Integer.valueOf(paramIndex + 1), ") was ", (argWasSpecified ? "specified, but had null/missing value." : "not specified.") @@ -281,16 +306,79 @@ public final class Macro extends TemplateElement implements TemplateModel { + "for it, like ", "<#macro macroName paramName=defaultExpr>", ")" } )); } + } else if (argsSpecVarDraft != null) { + // Minor performance problem here: If there are multiple iterations due to default value + // dependencies, this will set many parameters for multiple times. + argsSpecVarDraft[paramIndex] = argValue; } } - } while (resolvedAnArg && hasUnresolvedArg); - if (hasUnresolvedArg) { - if (firstReferenceException != null) { - throw firstReferenceException; + } while (hasUnresolvedDefaultValue && resolvedADefaultValue); + if (hasUnresolvedDefaultValue) { + if (firstInvalidReferenceExceptionForDefaultValue != null) { + throw firstInvalidReferenceExceptionForDefaultValue; } else if (!env.isClassicCompatible()) { - throw InvalidReferenceException.getInstance(firstUnresolvedExpression, env); + throw InvalidReferenceException.getInstance(firstUnresolvedDefaultValueExpression, env); } } + + if (argsSpecVarDraft != null) { + final String catchAllParamName = getMacro().catchAllParamName; + final TemplateModel catchAllArgValue = catchAllParamName != null ? localVars.get(catchAllParamName) : null; + + if (getMacro().isFunction()) { + int lengthWithCatchAlls = argsSpecVarDraft.length; + if (catchAllArgValue != null) { + lengthWithCatchAlls += ((TemplateSequenceModel) catchAllArgValue).size(); + } + + SimpleSequence argsSpecVarValue = new SimpleSequence(lengthWithCatchAlls); + for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) { + argsSpecVarValue.add(argsSpecVarDraft[paramIndex]); + } + if (catchAllParamName != null) { + TemplateSequenceModel catchAllSeq = (TemplateSequenceModel) catchAllArgValue; + int catchAllSize = catchAllSeq.size(); + for (int j = 0; j < catchAllSize; j++) { + argsSpecVarValue.add(catchAllSeq.get(j)); + } + } + assert argsSpecVarValue.size() == lengthWithCatchAlls; + + this.argsSpecialVariableValue = argsSpecVarValue; + } else { // #macro + int lengthWithCatchAlls = argsSpecVarDraft.length; + TemplateHashModelEx2 catchAllHash; + if (catchAllParamName != null) { + if (catchAllArgValue instanceof TemplateSequenceModel) { + throw new _MiscTemplateException("The macro can only by called with named arguments, " + + "because it uses both .", BuiltinVariable.ARGS, " and catch-all parameter."); + } + catchAllHash = (TemplateHashModelEx2) catchAllArgValue; + lengthWithCatchAlls += catchAllHash.size(); + } else { + catchAllHash = null; + } + + SimpleHash argsSpecVarValue = new SimpleHash( + new LinkedHashMap<String, Object>(lengthWithCatchAlls * 4 / 3, 1.0f), + null, 0); + for (int paramIndex = 0; paramIndex < argsSpecVarDraft.length; paramIndex++) { + argsSpecVarValue.put(paramNames[paramIndex], argsSpecVarDraft[paramIndex]); + } + if (catchAllArgValue != null) { + for (TemplateHashModelEx2.KeyValuePairIterator iter = catchAllHash.keyValuePairIterator(); + iter.hasNext(); ) { + TemplateHashModelEx2.KeyValuePair kvp = iter.next(); + argsSpecVarValue.put( + ((TemplateScalarModel) kvp.getKey()).getAsString(), + kvp.getValue()); + } + } + assert argsSpecVarValue.size() == lengthWithCatchAlls; + + this.argsSpecialVariableValue = argsSpecVarValue; + } + } // if (argsSpecVarDraft != null) } public TemplateModel getLocalVariable(String name) throws TemplateModelException { @@ -315,6 +403,14 @@ public final class Macro extends TemplateElement implements TemplateModel { } return result; } + + TemplateModel getArgsSpecialVariableValue() { + return argsSpecialVariableValue; + } + + void setArgsSpecialVariableValue(TemplateModel argsSpecialVariableValue) { + this.argsSpecialVariableValue = argsSpecialVariableValue; + } } @Override diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj index 3880ab4..f69042f 100644 --- a/src/main/javacc/FTL.jj +++ b/src/main/javacc/FTL.jj @@ -92,7 +92,7 @@ public class FMParser { */ private int continuableDirectiveNesting; - private boolean inMacro, inFunction; + private boolean inMacro, inFunction, requireArgsSpecialVariable; private LinkedList escapes = new LinkedList(); private int mixedContentNesting; // for stripText @@ -2118,6 +2118,13 @@ BuiltinVariable BuiltinVariable() : parseTimeValue = new SimpleScalar(outputFormat.getName()); } else if (nameStr.equals(BuiltinVariable.AUTO_ESC) || nameStr.equals(BuiltinVariable.AUTO_ESC_CC)) { parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } else if (nameStr.equals(BuiltinVariable.ARGS)) { + if (!inMacro && !inFunction) { + throw new ParseException("The \"" + BuiltinVariable.ARGS + "\" special variable must be " + + "inside a macro or function in the template source code.", template, name); + } + requireArgsSpecialVariable = true; + parseTimeValue = null; } else { parseTimeValue = null; } @@ -3456,6 +3463,7 @@ Macro Macro() : throw new ParseException("Macro or function definitions can't be nested into each other.", template, start); } if (isFunction) inFunction = true; else inMacro = true; + requireArgsSpecialVariable = false; } nameExp = IdentifierOrStringLiteral() { @@ -3537,7 +3545,8 @@ Macro Macro() : } inMacro = inFunction = false; - Macro result = new Macro(name, paramNamesWithDefault, catchAllParamName, isFunction, children); + Macro result = new Macro( + name, paramNamesWithDefault, catchAllParamName, isFunction, requireArgsSpecialVariable, children); result.setLocation(template, start, end); template.addMacro(result); return result; diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 83e7a50..b680afc 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -24567,6 +24567,98 @@ There was no specific handler for node y <itemizedlist spacing="compact"> <listitem> <para><indexterm> + <primary>args</primary> + </indexterm><literal>args</literal> (since 2.3.30): Used inside + the <link + linkend="ref.directive.macro"><literal>macro</literal></link> and + <link + linkend="ref.directive.function"><literal>function</literal></link> + directives, it returns all arguments of the current invocation of + the macro or function. This allows processing all the arguments in + an uniform way (like pass them to the <link + linkend="ref_builtin_spread_args"><literal>spread_args</literal> + built-in</link>). Further details:</para> + + <itemizedlist> + <listitem> + <para>When used inside a macro, <literal>.args</literal> is + always a hash that maps the parameter names to the parameter + values. That's so even if the macro was called with positional + arguments, as the parameter names are declared by the + <literal>macro</literal> directive. But when used inside a + function, <literal>.args</literal> is always a sequence of the + argument values. That's because functions can only be called + with positional arguments, so the parameter names declared + inside the <literal>function</literal> directive aren't + relevant.</para> + </listitem> + + <listitem> + <para>For arguments that have a default value (as + <literal>b</literal> in <literal><#macro m1 a + b=0></literal>), if the caller of the macro or function has + omitted that argument (as in <literal><@m1 a=1 + /></literal>), <literal>.args</literal> will contain the + default value for that argument (<literal>{'a': 1, 'b': + 0}</literal>).</para> + </listitem> + + <listitem> + <para>If there's a catch-all argument, like + <literal>others</literal> in <literal><#macro m2 a, b, + others...></literal>, then <literal>.args</literal> will + contain the elements in the the catch-all parameter directly, + and won't contain the declared catch-all parameter + (<literal>others</literal>) itself. So for <literal><@m2 a=1 + b=2 c=3 d=4 /></literal> it will be <literal>{'a': 1, 'b': 2, + 'c': 3, 'd': 4}</literal>, and <emphasis>not</emphasis> + <literal>{'a': 1, 'b': 2, 'others': {'c':3, 'd': + 4}}</literal>.</para> + </listitem> + + <listitem> + <para>If a macro has a catch-all argument, and the macro uses + <literal>.args</literal> somewhere, it can only be called with + named arguments (like <literal><@m a=1 b=2 /></literal>), + and not with positional arguments (like <literal><@m 1 2 + /></literal>). That's because for macros + <literal>.args</literal> is always a hash, but if the arguments + are positional, the catch-all arguments won't have names.</para> + </listitem> + + <listitem> + <para><literal>.args</literal> is initialized when the macro or + function is called, and so it doesn't reflect changes made later + on the local variables that correspond to the arguments.</para> + </listitem> + + <listitem> + <para><literal>.args</literal> must occur inside a + <literal>macro</literal> or <literal>function</literal> + directive on the source code level, or else it's a template + parsing error. So you can't use it in a template fragment that + you insert dynamically into a macro or function (like via the + <link linkend="ref.directive.include"><literal>include</literal> + directive</link> or <link + linkend="ref_builtin_eval"><literal>eval</literal> + built-in</link>). That's because the <literal>macro</literal> or + <literal>function</literal> has to know if it contains + <literal>.args</literal> earlier than it's called.</para> + </listitem> + + <listitem> + <para>For macros, the order of elements in the + <literal>.args</literal> hash corresponds to the order as the + parameters were declared in the <literal>macro</literal> + directive, not to the order the caller has used. Except, + catch-all arguments will use the order that the caller + used.</para> + </listitem> + </itemizedlist> + </listitem> + + <listitem> + <para><indexterm> <primary>auto_esc</primary> </indexterm><literal>auto_esc</literal> (since 2.3.24): Boolean value that tells if auto-escaping (based on output format) is on or @@ -28755,6 +28847,15 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> defaults. <link linkend="ref_builtin_spread_args">See more here...</link></para> </listitem> + + <listitem> + <para>Added new <link linkend="ref_specvar">special + variable</link>, <literal>.args</literal>. This evaluates to a + hash (in macros), or sequence (in functions) that contains all + the arguments. This is useful for operations that act on all the + arguments uniformly, like for example to pass the arguments to + <literal>?spread_args(<replaceable>...</replaceable>)</literal>.</para> + </listitem> </itemizedlist> </section> diff --git a/src/test/java/freemarker/core/ArgsSpecialVariableTest.java b/src/test/java/freemarker/core/ArgsSpecialVariableTest.java new file mode 100644 index 0000000..1064744 --- /dev/null +++ b/src/test/java/freemarker/core/ArgsSpecialVariableTest.java @@ -0,0 +1,160 @@ +/* + * 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. + */ + +package freemarker.core; + +import java.io.IOException; + +import org.junit.Test; + +import freemarker.template.TemplateException; +import freemarker.test.TemplateTest; + +public class ArgsSpecialVariableTest extends TemplateTest { + + @Test + public void macroSimpleTest() throws IOException, TemplateException { + String macroDef = "<#macro m a b><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>"; + String expectedOutput = "a=11, b=22"; + assertOutput(macroDef + + "<@m a=11 b=22 />", + expectedOutput); + assertOutput(macroDef + + "<@m 11 22 />", + expectedOutput); + } + + @Test + public void macroWithDefaultsTest() throws IOException, TemplateException { + String macroDef = "<#macro m a b c=3><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>"; + String expectedOutput = "" + + "a=11, b=22, c=33; " + + "a=11, b=22, c=3"; + assertOutput(macroDef + + "<@m a=11 b=22 c=33 />; " + + "<@m a=11 b=22 />", + expectedOutput); + assertOutput(macroDef + + "<@m 11 22 33 />; " + + "<@m 11 22 />", + expectedOutput); + } + + @Test + public void macroWithMultiPassDefaultsTest() throws IOException, TemplateException { + String macroDef = "<#macro m a=c b=c c=b><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>"; + String expectedOutput = "" + + "a=33, b=33, c=33; " + + "a=22, b=22, c=22; " + + "a=11, b=33, c=33; " + + "a=11, b=22, c=22"; + assertOutput(macroDef + + "<@m c=33 />; " + + "<@m b=22 />; " + + "<@m a=11 c=33 />; " + + "<@m a=11 b=22 />", + expectedOutput); + assertOutput(macroDef + + "<@m null, null, 33 />; " + + "<@m null, 22, null />; " + + "<@m 11, null, 33 />; " + + "<@m 11, 22, null />", + expectedOutput); + } + + @Test + public void macroWithCatchAllTest() throws IOException, TemplateException { + assertOutput("" + + "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>" + + "<@m a=11 b=22 c=33 d=44 />; " + + "<@m a=11 b=22 />; " + + "<@m a=11 />; " + + "<@m a=11 c=33 />", + "a=11, b=22, c=33, d=44; " + + "a=11, b=22; " + + "a=11, b=2; " + + "a=11, b=2, c=33"); + + assertErrorContains("" + + "<#macro m a b=2 others...><#list .args as k, v>${k}=${v}<#sep>, </#list></#macro>" + + "<@m 1, 2 />", + ".args", "catch-all"); + } + + @Test + public void functionSimpleTest() throws IOException, TemplateException { + String functionDef = "<#function f a b><#return .args?join(', ')></#function>"; + String expectedOutput = "11, 22"; + assertOutput(functionDef + + "${f(11, 22)}", + expectedOutput); + } + + @Test + public void functionWithDefaultsTest() throws IOException, TemplateException { + String functionDef = "<#function f a b c=3><#return .args?join(', ')></#function>"; + String expectedOutput = "" + + "11, 22, 33; " + + "11, 22, 3"; + assertOutput(functionDef + + "${f(11, 22, 33)}; " + + "${f(11, 22)}", + expectedOutput); + } + + @Test + public void functionWithMultiPassDefaultsTest() throws IOException, TemplateException { + String functionDef = "<#function f a=c b=c c=b><#return .args?join(', ')></#function>"; + assertOutput(functionDef + + "${f(null, null, 33)}; " + + "${f(null, 22)}; " + + "${f(11, null, 33)}; " + + "${f(11, 22)}", + "33, 33, 33; " + + "22, 22, 22; " + + "11, 33, 33; " + + "11, 22, 22"); + assertOutput(functionDef + + "${f(11, 22)}; " + + "${f(11, 22, 33)}", + "11, 22, 22; " + + "11, 22, 33"); + } + + @Test + public void functionWithCatchAllTest() throws IOException, TemplateException { + assertOutput("" + + "<#function f a b=2 others...><#return .args?join(', ')></#function>" + + "${f(11, 22, 33, 44)}; " + + "${f(11, 22)}; " + + "${f(11)}; " + + "${f(11, null, 33)}", + "11, 22, 33, 44; " + + "11, 22; " + + "11, 2; " + + "11, 2, 33"); + } + + @Test + public void usedInWrongContextTest() throws IOException, TemplateException { + assertErrorContains("${.args}", "args", "macro", "function"); + assertErrorContains("<#macro m>${'.args'?eval}</#macro><@m />", "args", "macro", "function"); + } + +}
