Added ?with_args_last(args). Also some cleanup in code related to ?with_args.
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index da9ffc6..36e74b7 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -84,11 +84,13 @@
static final Set<String> CAMEL_CASE_NAMES = new TreeSet<String>();
static final Set<String> SNAKE_CASE_NAMES = new TreeSet<String>();
- static final int NUMBER_OF_BIS = 287;
+ static final int NUMBER_OF_BIS = 289;
static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
static final String BI_NAME_SNAKE_CASE_WITH_ARGS = "with_args";
static final String BI_NAME_CAMEL_CASE_WITH_ARGS = "withArgs";
+ static final String BI_NAME_SNAKE_CASE_WITH_ARGS_LAST = "with_args_last";
+ static final String BI_NAME_CAMEL_CASE_WITH_ARGS_LAST = "withArgsLast";
static {
// Note that you must update NUMBER_OF_BIS if you add new items here!
@@ -300,7 +302,10 @@
putBI("url_path", "urlPath", new BuiltInsForStringsEncoding.urlPathBI());
putBI("values", new BuiltInsForHashes.valuesBI());
putBI("web_safe", "webSafe", BUILT_INS_BY_NAME.get("html")); // deprecated; use ?html instead
- putBI(BI_NAME_SNAKE_CASE_WITH_ARGS, BI_NAME_CAMEL_CASE_WITH_ARGS, new BuiltInsForCallables.with_argsBI());
+ putBI(BI_NAME_SNAKE_CASE_WITH_ARGS, BI_NAME_CAMEL_CASE_WITH_ARGS,
+ new BuiltInsForCallables.with_argsBI());
+ putBI(BI_NAME_SNAKE_CASE_WITH_ARGS_LAST, BI_NAME_CAMEL_CASE_WITH_ARGS_LAST,
+ new BuiltInsForCallables.with_args_lastBI());
putBI("word_list", "wordList", new BuiltInsForStringsBasic.word_listBI());
putBI("xhtml", new BuiltInsForStringsEncoding.xhtmlBI());
putBI("xml", new BuiltInsForStringsEncoding.xmlBI());
diff --git a/src/main/java/freemarker/core/BuiltInsForCallables.java b/src/main/java/freemarker/core/BuiltInsForCallables.java
index acc4245..87d6647 100644
--- a/src/main/java/freemarker/core/BuiltInsForCallables.java
+++ b/src/main/java/freemarker/core/BuiltInsForCallables.java
@@ -40,7 +40,9 @@
class BuiltInsForCallables {
- static class with_argsBI extends BuiltIn {
+ static abstract class AbstractWithArgsBI extends BuiltIn {
+
+ protected abstract boolean isOrderLast();
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel model = target.eval(env);
@@ -73,13 +75,13 @@
Macro.WithArgs withArgs;
if (argTM instanceof TemplateSequenceModel) {
- withArgs = new Macro.WithArgs((TemplateSequenceModel) argTM);
+ withArgs = new Macro.WithArgs((TemplateSequenceModel) argTM, isOrderLast());
} else if (argTM instanceof TemplateHashModelEx) {
if (macroOrFunction.isFunction()) {
throw new _TemplateModelException("When applied on a function, ?", key,
" can't have a hash argument. Use a sequence argument.");
}
- withArgs = new Macro.WithArgs((TemplateHashModelEx) argTM);
+ withArgs = new Macro.WithArgs((TemplateHashModelEx) argTM, isOrderLast());
} else {
throw _MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0, argTM);
}
@@ -110,11 +112,15 @@
List<TemplateModel> newArgs = new ArrayList<TemplateModel>(
withArgsSize + origArgs.size());
+ if (isOrderLast()) {
+ newArgs.addAll(origArgs);
+ }
for (int i = 0; i < withArgsSize; i++) {
newArgs.add(withArgs.get(i));
}
-
- newArgs.addAll(origArgs);
+ if (!isOrderLast()) {
+ newArgs.addAll(origArgs);
+ }
return method.exec(newArgs);
}
@@ -126,12 +132,16 @@
List<String> newArgs = new ArrayList<String>(
withArgsSize + origArgs.size());
+ if (isOrderLast()) {
+ newArgs.addAll(origArgs);
+ }
for (int i = 0; i < withArgsSize; i++) {
TemplateModel argVal = withArgs.get(i);
newArgs.add(argValueToString(argVal));
}
-
- newArgs.addAll(origArgs);
+ if (!isOrderLast()) {
+ newArgs.addAll(origArgs);
+ }
return method.exec(newArgs);
}
@@ -187,33 +197,48 @@
public void execute(Environment env, Map origArgs, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
int withArgsSize = withArgs.size();
+ // This is unnecessarily big if there are overridden arguments, but we care more about
+ // avoiding rehashing.
Map<String, TemplateModel> newArgs = new LinkedHashMap<String, TemplateModel>(
(withArgsSize + origArgs.size()) * 4 / 3, 1f);
TemplateHashModelEx2.KeyValuePairIterator withArgsIter =
TemplateModelUtils.getKeyValuePairIterator(withArgs);
- while (withArgsIter.hasNext()) {
- TemplateHashModelEx2.KeyValuePair spreadArgKVP = withArgsIter.next();
-
- TemplateModel argNameTM = spreadArgKVP.getKey();
- if (!(argNameTM instanceof TemplateScalarModel)) {
- throw new _TemplateModelException(
- "Expected string keys in the spread args hash, but one of the keys was ",
- new _DelayedAOrAn(new _DelayedFTLTypeDescription(argNameTM)), ".");
+ if (isOrderLast()) {
+ newArgs.putAll(origArgs);
+ while (withArgsIter.hasNext()) {
+ TemplateHashModelEx2.KeyValuePair withArgsKVP = withArgsIter.next();
+ String argName = getArgumentName(withArgsKVP);
+ if (!newArgs.containsKey(argName)) {
+ newArgs.put(argName, withArgsKVP.getValue());
+ }
}
- String argName = EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null);
-
- newArgs.put(argName, spreadArgKVP.getValue());
+ } else {
+ while (withArgsIter.hasNext()) {
+ TemplateHashModelEx2.KeyValuePair withArgsKVP = withArgsIter.next();
+ newArgs.put(getArgumentName(withArgsKVP), withArgsKVP.getValue());
+ }
+ newArgs.putAll(origArgs);
}
- newArgs.putAll(origArgs); // TODO Should null replace non-null?
-
directive.execute(env, newArgs, loopVars, body);
}
+
+ private String getArgumentName(TemplateHashModelEx2.KeyValuePair withArgsKVP) throws
+ TemplateModelException {
+ TemplateModel argNameTM = withArgsKVP.getKey();
+ if (!(argNameTM instanceof TemplateScalarModel)) {
+ throw new _TemplateModelException(
+ "Expected string keys in the ?", key, "(...) arguments, " +
+ "but one of the keys was ",
+ new _DelayedAOrAn(new _DelayedFTLTypeDescription(argNameTM)), ".");
+ }
+ return EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null);
+ }
};
} else if (argTM instanceof TemplateSequenceModel) {
throw new _TemplateModelException("When applied on a directive, ?", key,
- " can't have a sequence argument. Use a hash argument.");
+ "(...) can't have a sequence argument. Use a hash argument.");
} else {
throw _MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0, argTM);
}
@@ -223,4 +248,18 @@
}
+ static final class with_argsBI extends AbstractWithArgsBI {
+ @Override
+ protected boolean isOrderLast() {
+ return false;
+ }
+ }
+
+ static final class with_args_lastBI extends AbstractWithArgsBI {
+ @Override
+ protected boolean isOrderLast() {
+ return true;
+ }
+ }
+
}
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 8ca29b1..fcce497 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -904,32 +904,31 @@
int nextPositionalArgToAssignIdx = 0;
// Used for ?with_args(...):
- Macro.WithArgs withArgs = macro.getWithArgs();
- if (withArgs != null) {
- TemplateHashModelEx byNameWithArgs = withArgs.getByName();
- TemplateSequenceModel byPositionWithArgs = withArgs.getByPosition();
+ WithArgsState withArgsState = getWithArgState(macro);
+ if (withArgsState != null) {
+ TemplateHashModelEx byNameWithArgs = withArgsState.byName;
+ TemplateSequenceModel byPositionWithArgs = withArgsState.byPosition;
if (byNameWithArgs != null) {
- new HashMap<String, TemplateModel>(byNameWithArgs.size() * 4 / 3, 1f);
- TemplateHashModelEx2.KeyValuePairIterator namedParamValueOverridesIter =
- TemplateModelUtils.getKeyValuePairIterator(byNameWithArgs);
- while (namedParamValueOverridesIter.hasNext()) {
- TemplateHashModelEx2.KeyValuePair defaultArgHashKVP = namedParamValueOverridesIter.next();
+ TemplateHashModelEx2.KeyValuePairIterator withArgsKVPIter
+ = TemplateModelUtils.getKeyValuePairIterator(byNameWithArgs);
+ while (withArgsKVPIter.hasNext()) {
+ TemplateHashModelEx2.KeyValuePair withArgKVP = withArgsKVPIter.next();
String argName;
{
- TemplateModel argNameTM = defaultArgHashKVP.getKey();
+ TemplateModel argNameTM = withArgKVP.getKey();
if (!(argNameTM instanceof TemplateScalarModel)) {
throw new _TemplateModelException(
- "Expected string keys in the spread args hash, but one of the keys was ",
+ "Expected string keys in the \"with args\" hash, but one of the keys was ",
new _DelayedAOrAn(new _DelayedFTLTypeDescription(argNameTM)), ".");
}
argName = EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null);
}
- TemplateModel argValue = defaultArgHashKVP.getValue();
+ TemplateModel argValue = withArgKVP.getValue();
// What if argValue is null? It still has to occur in the named catch-all parameter, to be similar
- // to <@macroWithCatchAll a=null b=null />, that will also add the keys to the catch-all hash.
+ // to <@macroWithCatchAll a=null b=null />, which will also add the keys to the catch-all hash.
// Similarly, we also still fail if the name is not declared.
final boolean isArgNameDeclared = macro.hasArgNamed(argName);
if (isArgNameDeclared) {
@@ -938,39 +937,67 @@
if (namedCatchAllParamValue == null) {
namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName);
}
- namedCatchAllParamValue.put(argName, argValue);
+ if (!withArgsState.orderLast) {
+ namedCatchAllParamValue.put(argName, argValue);
+ } else {
+ List<NameValuePair> orderLastByNameCatchAll = withArgsState.orderLastByNameCatchAll;
+ if (orderLastByNameCatchAll == null) {
+ orderLastByNameCatchAll = new ArrayList<NameValuePair>();
+ withArgsState.orderLastByNameCatchAll = orderLastByNameCatchAll;
+ }
+ orderLastByNameCatchAll.add(new NameValuePair(argName, argValue));
+ }
} else {
throw newUndeclaredParamNameException(macro, argName);
}
- }
+ } // while (withArgsKVPIter.hasNext())
} else if (byPositionWithArgs != null) {
- String[] argNames = macro.getArgumentNamesInternal();
- final int argsCnt = byPositionWithArgs.size();
- if (argNames.length < argsCnt && catchAllParamName == null) {
- throw newTooManyArgumentsException(macro, argNames, argsCnt);
- }
- for (int i = 0; i < argsCnt; i++) {
- TemplateModel argValue = byPositionWithArgs.get(i);
- try {
- if (nextPositionalArgToAssignIdx < argNames.length) {
- String argName = argNames[nextPositionalArgToAssignIdx++];
- macroCtx.setLocalVar(argName, argValue);
- } else {
- if (positionalCatchAllParamValue == null) {
- positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName);
+ if (!withArgsState.orderLast) { // ?withArgs
+ String[] argNames = macro.getArgumentNamesNoCopy();
+ final int argsCnt = byPositionWithArgs.size();
+ if (argNames.length < argsCnt && catchAllParamName == null) {
+ throw newTooManyArgumentsException(macro, argNames, argsCnt);
+ }
+ for (int argIdx = 0; argIdx < argsCnt; argIdx++) {
+ TemplateModel argValue = byPositionWithArgs.get(argIdx);
+ try {
+ if (nextPositionalArgToAssignIdx < argNames.length) {
+ String argName = argNames[nextPositionalArgToAssignIdx++];
+ macroCtx.setLocalVar(argName, argValue);
+ } else {
+ if (positionalCatchAllParamValue == null) {
+ positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName);
+ }
+ positionalCatchAllParamValue.add(argValue);
}
- positionalCatchAllParamValue.add(argValue);
+ } catch (RuntimeException re) {
+ throw new _MiscTemplateException(re, this);
}
- } catch (RuntimeException re) {
- throw new _MiscTemplateException(re, this);
+ }
+ } else { // ?withArgsLast
+ if (namedArgs != null && !namedArgs.isEmpty() && byPositionWithArgs.size() != 0) {
+ // Unlike with ?withArgs, here we can't know in general which argument byPositionWithArgs[0]
+ // meant to refer to, as the named arguments have already taken some indexes.
+ throw new _MiscTemplateException("Call can't pass parameters by name, as there's " +
+ "\"with args last\" in effect that specifies parameters by position.");
+ }
+ if (catchAllParamName == null) {
+ // To fail before Expression-s for some normal arguments are evaluated:
+ int totalPositionalArgCnt =
+ (positionalArgs != null ? positionalArgs.size() : 0) + byPositionWithArgs.size();
+ if (totalPositionalArgCnt > macro.getArgumentNamesNoCopy().length) {
+ throw newTooManyArgumentsException(macro, macro.getArgumentNamesNoCopy(), totalPositionalArgCnt);
+ }
}
}
}
- }
+ } // if (withArgsState != null)
if (namedArgs != null) {
if (catchAllParamName != null && namedCatchAllParamValue == null && positionalCatchAllParamValue == null) {
- if (namedArgs.isEmpty() && withArgs != null && withArgs.getByPosition() != null) {
+ // If a macro call has no argument (like <@m />), before 2.3.30 we assumed it's a by-name call. But now
+ // if we have ?with_args(args), its argument type decides if the call is by-name or by-position.
+ if (namedArgs.isEmpty() && withArgsState != null && withArgsState.byPosition != null) {
positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName);
} else {
namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName);
@@ -998,14 +1025,14 @@
}
} else if (positionalArgs != null) {
if (catchAllParamName != null && positionalCatchAllParamValue == null && namedCatchAllParamValue == null) {
- if (positionalArgs.isEmpty() && withArgs != null && withArgs.getByName() != null) {
+ if (positionalArgs.isEmpty() && withArgsState != null && withArgsState.byName != null) {
namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName);
} else {
positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName);
}
}
- String[] argNames = macro.getArgumentNamesInternal();
+ String[] argNames = macro.getArgumentNamesNoCopy();
final int argsCnt = positionalArgs.size();
final int argsWithWithArgsCnt = argsCnt + nextPositionalArgToAssignIdx;
if (argNames.length < argsWithWithArgsCnt && positionalCatchAllParamValue == null) {
@@ -1017,18 +1044,72 @@
}
for (int srcPosArgIdx = 0; srcPosArgIdx < argsCnt; srcPosArgIdx++) {
Expression argValueExp = positionalArgs.get(srcPosArgIdx);
- TemplateModel argValue = argValueExp.eval(this);
+ TemplateModel argValue;
try {
- if (nextPositionalArgToAssignIdx < argNames.length) {
- String argName = argNames[nextPositionalArgToAssignIdx++];
- macroCtx.setLocalVar(argName, argValue);
- } else {
- positionalCatchAllParamValue.add(argValue);
- }
- } catch (RuntimeException re) {
- throw new _MiscTemplateException(re, this);
+ argValue = argValueExp.eval(this);
+ } catch (RuntimeException e) {
+ throw new _MiscTemplateException(e, this);
+ }
+ if (nextPositionalArgToAssignIdx < argNames.length) {
+ String argName = argNames[nextPositionalArgToAssignIdx++];
+ macroCtx.setLocalVar(argName, argValue);
+ } else {
+ positionalCatchAllParamValue.add(argValue);
}
}
+ } // else if (positionalArgs != null)
+
+ if (withArgsState != null && withArgsState.orderLast) {
+ if (withArgsState.orderLastByNameCatchAll != null) {
+ for (NameValuePair nameValuePair : withArgsState.orderLastByNameCatchAll) {
+ if (!namedCatchAllParamValue.containsKey(nameValuePair.name)) {
+ namedCatchAllParamValue.put(nameValuePair.name, nameValuePair.value);
+ }
+ }
+ } else if (withArgsState.byPosition != null) {
+ TemplateSequenceModel byPosition = withArgsState.byPosition;
+ int withArgCnt = byPosition.size();
+ String[] argNames = macro.getArgumentNamesNoCopy();
+ for (int withArgIdx = 0; withArgIdx < withArgCnt; withArgIdx++) {
+ TemplateModel withArgValue = byPosition.get(withArgIdx);
+ if (nextPositionalArgToAssignIdx < argNames.length) {
+ String argName = argNames[nextPositionalArgToAssignIdx++];
+ macroCtx.setLocalVar(argName, withArgValue);
+ } else {
+ // It was checked much earlier that we don't have too many arguments, so this must work:
+ positionalCatchAllParamValue.add(withArgValue);
+ }
+ }
+ }
+ }
+ }
+
+ private static WithArgsState getWithArgState(Macro macro) {
+ Macro.WithArgs withArgs = macro.getWithArgs();
+ return withArgs == null ? null : new WithArgsState(withArgs.getByName(), withArgs.getByPosition(),
+ withArgs.isOrderLast());
+ }
+
+ private static final class WithArgsState {
+ private final TemplateHashModelEx byName;
+ private final TemplateSequenceModel byPosition;
+ private final boolean orderLast;
+ private List<NameValuePair> orderLastByNameCatchAll;
+
+ public WithArgsState(TemplateHashModelEx byName, TemplateSequenceModel byPosition, boolean orderLast) {
+ this.byName = byName;
+ this.byPosition = byPosition;
+ this.orderLast = orderLast;
+ }
+ }
+
+ private static final class NameValuePair {
+ private final String name;
+ private final TemplateModel value;
+
+ public NameValuePair(String name, TemplateModel value) {
+ this.name = name;
+ this.value = value;
}
}
@@ -1057,7 +1138,7 @@
return new _MiscTemplateException(this,
(macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()),
" has no parameter with name ", new _DelayedJQuote(argName), ". Valid parameter names are: "
- , new _DelayedJoinWithComma(macro.getArgumentNames()));
+ , new _DelayedJoinWithComma(macro.getArgumentNamesNoCopy()));
}
private _MiscTemplateException newBothNamedAndPositionalCatchAllParamsException(Macro macro) {
diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java
index d1a81d0..4e564ef 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -115,16 +115,24 @@
public String getCatchAll() {
return catchAllParamName;
}
-
+
+ /**
+ * Returns a new copy of the array that stored the names of arguments declared in this macro or function.
+ */
public String[] getArgumentNames() {
return paramNames.clone();
}
- String[] getArgumentNamesInternal() {
+ String[] getArgumentNamesNoCopy() {
return paramNames;
}
- boolean hasArgNamed(String name) {
+ /**
+ * Returns if the macro or function has a parameter called as the argument.
+ *
+ * @since 2.3.30
+ */
+ public boolean hasArgNamed(String name) {
return paramNamesWithDefault.containsKey(name);
}
@@ -480,15 +488,18 @@
static final class WithArgs {
private final TemplateHashModelEx byName;
private final TemplateSequenceModel byPosition;
+ private final boolean orderLast;
- WithArgs(TemplateHashModelEx byName) {
+ WithArgs(TemplateHashModelEx byName, boolean orderLast) {
this.byName = byName;
this.byPosition = null;
+ this.orderLast = orderLast;
}
- WithArgs(TemplateSequenceModel byPosition) {
+ WithArgs(TemplateSequenceModel byPosition, boolean orderLast) {
this.byName = null;
this.byPosition = byPosition;
+ this.orderLast = orderLast;
}
public TemplateHashModelEx getByName() {
@@ -498,6 +509,10 @@
public TemplateSequenceModel getByPosition() {
return byPosition;
}
+
+ public boolean isOrderLast() {
+ return orderLast;
+ }
}
}
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index dc47af5..d9717e1 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -13183,6 +13183,11 @@
<listitem>
<para><link
+ linkend="ref_builtin_with_args_last">with_args_last</link></para>
+ </listitem>
+
+ <listitem>
+ <para><link
linkend="ref_builtin_word_list">word_list</link></para>
</listitem>
@@ -19877,6 +19882,152 @@
there:</para>
<programlisting role="template"><@myMacro?with_args({'a': 1})>...</<emphasis>@myMacro</emphasis>></programlisting>
+
+ <para>Note that as far as the order of arguments is concerned,
+ arguments coming from
+ <literal>with_args(<replaceable>...</replaceable>)</literal> are
+ added before the arguments specified in the call to the returned
+ directive/function/method. In some use cases it's more desirable to
+ add them at the end instead, in which case use the <link
+ linkend="ref_builtin_with_args_last"><literal>with_args_last</literal>
+ built-in</link>.</para>
+ </section>
+
+ <section xml:id="ref_builtin_with_args_last">
+ <title>with_args_last</title>
+
+ <note>
+ <para>This built-in is available since 2.3.30</para>
+ </note>
+
+ <para>Same as <link
+ linkend="ref_builtin_with_args"><literal>with_args</literal></link>,
+ but if the order of the arguments in resulting final argument list
+ may differs (but not the values in it). This only matters if you
+ pass parameters by position (typically, when calling functions or
+ methods), or when there's catch-all argument.</para>
+
+ <para>A typical example with positional arguments is when you want
+ to add the dynamic argument to the end of the parameter list:</para>
+
+ <programlisting role="template"><#function f a b c d>
+ <#return "a=${a}, b=${b}, c=${c}, d=${d}">
+</#function>
+
+<#assign dynamicArgs=[3, 4]>
+
+with_args:
+${f?with_args(dynamicArgs)(1, 2)}
+
+with_args_last:
+${f?with_args_last(dynamicArgs)(1, 2)}</programlisting>
+
+ <programlisting role="output">with_args:
+a=3, b=4, c=1, d=2
+
+with_args_last:
+a=1, b=2, c=3, d=4</programlisting>
+
+ <para>In the case of name arguments, while the primary mean of
+ identifying an argument is the its name, catch-all arguments
+ (<literal>others...</literal> below) still have an order:</para>
+
+ <programlisting role="template"><#macro m a b others...>
+ a=${a}
+ b=${b}
+ others:
+ <#list others as k, v>
+ ${k} = ${v}
+ </#list>
+</#macro>
+
+<#assign dynamicArgs={'e': 5, 'f': 6}>
+
+with_args:
+<@m?with_args(dynamicArgs) a=1 b=2 c=3 d=4 />
+
+with_args_last:
+<@m?with_args_last(dynamicArgs) a=1 b=2 c=3 d=4 /></programlisting>
+
+ <programlisting role="output">with_args:
+ a=1
+ b=2
+ others:
+ e = 5
+ f = 6
+ c = 3
+ d = 4
+
+with_args_last:
+ a=1
+ b=2
+ others:
+ c = 3
+ d = 4
+ e = 5
+ f = 6</programlisting>
+
+ <para>If you specify a named parameter that are not catch-all, so
+ they are declared in the <literal>macro</literal> tag (as
+ <literal>a</literal> and <literal>b</literal> below), then
+ <literal>with_args</literal> and <literal>with_args_last</literal>
+ are no different, since the argument order is specified by the macro
+ definition, not the macro call:</para>
+
+ <programlisting role="template"><#macro m a=0 b=0>
+ <#-- We use .args to demonstrate the ordering of a and b: -->
+ <#list .args as k, v>
+ ${k} = ${v}
+ </#list>
+</#macro>
+
+<#assign dynamicArgs={'b': 1}>
+
+with_args:
+<@m?with_args(dynamicArgs) a=1 />
+
+with_args_last:
+<@m?with_args_last(dynamicArgs) a=1 /></programlisting>
+
+ <programlisting role="output">with_args:
+ a = 1
+ b = 1
+
+with_args_last:
+ a = 1
+ b = 1</programlisting>
+
+ <para>If both the macro or directive call, and the
+ <literal>with_args_last</literal> argument specifies named catch-all
+ argument with the same name (like <literal>b</literal> below), then
+ the placement of those parameters is decide by the macro/directive
+ call:</para>
+
+ <programlisting role="template"><#macro m others...>
+ <#list others as k, v>
+ ${k} = ${v}
+ </#list>
+</#macro>
+
+<#assign dynamicArgs={'b': 0, 'd': 4}>
+
+with_args:
+<@m?with_args(dynamicArgs) a=1 b=2 c=3 />
+
+with_args_last:
+<@m?with_args_last(dynamicArgs) a=1 b=2 c=3 /></programlisting>
+
+ <programlisting role="output">with_args:
+<emphasis> b = 2
+ d = 4
+</emphasis> a = 1
+ c = 3
+
+with_args_last:
+ a = 1
+<emphasis> b = 2
+</emphasis> c = 3
+<emphasis> d = 4</emphasis></programlisting>
</section>
</section>
</chapter>
@@ -28988,8 +29139,10 @@
xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-107">FREEMARKER-107</link>:
Added
<literal>?<replaceable>with_args</replaceable>(dynamicArguments)</literal>
+ and
+ <literal>?<replaceable>with_args_last</replaceable>(dynamicArguments)</literal>
to add parameters dynamically to directive (like macro),
- function and method calls. Actually, this built-in returns
+ function and method calls. Actually, this built-in returns a
directive or macro or function that has different parameter
defaults. <link linkend="ref_builtin_with_args">See more
here...</link></para>
diff --git a/src/test/java/freemarker/core/WithArgsBuiltInTest.java b/src/test/java/freemarker/core/WithArgsBuiltInTest.java
index 3712509..71366e6 100644
--- a/src/test/java/freemarker/core/WithArgsBuiltInTest.java
+++ b/src/test/java/freemarker/core/WithArgsBuiltInTest.java
@@ -20,6 +20,7 @@
package freemarker.core;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
@@ -42,7 +43,7 @@
public class WithArgsBuiltInTest extends TemplateTest {
- private static final String PRINT_O = "o=<#if o?isSequence>[${o?join(', ')}]" +
+ private static final String PRINT_O = "o=<#if o?isSequence>[<#list o as v>${v!'null'}<#sep>, </#list>]" +
"<#else>{<#list o as k,v>${k}=${v!'null'}<#sep>, </#list>}" +
"</#if>";
@@ -179,10 +180,10 @@
@Test
public void testNullsWithMacroWithPositionalWithArgs() throws Exception {
// Null-s in ?withArgs should behave similarly as if they were given directly as argument.
- assertOutput("<@mCAO 1 null null 4 />", "o=[1, 4]"); // [FM3] Should be: 1, null, null, 4
+ assertOutput("<@mCAO 1 null null 4 />", "o=[1, null, null, 4]");
addToDataModel("args", Arrays.asList(1, null, null, 4));
- assertOutput("<@mCAO?withArgs(args) />", "o=[1, 4]"); // [FM3] See above
- assertOutput("<@mCAO?withArgs(args) null 5 6 />", "o=[1, 4, 5, 6]"); // [FM3] See above
+ assertOutput("<@mCAO?withArgs(args) />", "o=[1, null, null, 4]");
+ assertOutput("<@mCAO?withArgs(args) null 5 6 />", "o=[1, null, null, 4, null, 5, 6]");
}
@Test
@@ -216,10 +217,10 @@
@Test
public void testNullsWithFunction() throws Exception {
// Null-s in ?withArgs should behave similarly as if they were given directly as argument.
- assertOutput("${fCAO(1, null, null, 4)}", "o=[1, 4]"); // [FM3] Should be: 1, null, null, 4
+ assertOutput("${fCAO(1, null, null, 4)}", "o=[1, null, null, 4]");
addToDataModel("args", Arrays.asList(1, null, null, 4));
- assertOutput("${fCAO?withArgs(args)()}", "o=[1, 4]"); // [FM3] See above
- assertOutput("${fCAO?withArgs(args)(null, 5, 6)}", "o=[1, 4, 5, 6]"); // [FM3] See above
+ assertOutput("${fCAO?withArgs(args)()}", "o=[1, null, null, 4]");
+ assertOutput("${fCAO?withArgs(args)(null, 5, 6)}", "o=[1, null, null, 4, null, 5, 6]");
}
@Test
@@ -322,6 +323,18 @@
assertOutput("${obj.mNullable?withArgs(args)()}", "null, 2, null");
}
+ @Test
+ public void testMethodWithArgsLast() throws IOException, TemplateException {
+ addToDataModel("obj", new MethodHolder());
+ assertOutput("${obj.m3p?withArgsLast([1, 2, 3])()}", "1, 2, 3");
+ assertOutput("${obj.m3p?withArgsLast([1, 2])(3)}", "3, 1, 2");
+ assertOutput("${obj.m3p?withArgsLast([1])(2, 3)}", "2, 3, 1");
+ assertOutput("${obj.m3p?withArgsLast([])(1, 2, 3)}", "1, 2, 3");
+
+ addToDataModel("args", Arrays.asList(null, 2));
+ assertOutput("${obj.mNullable?withArgsLast(args)(1)}", "1, null, 2");
+ }
+
public static class MethodHolder {
public String m3p(int a, int b, int c) {
return a + ", " + b + ", " + c;
@@ -400,6 +413,126 @@
"{a=null, b=22, c=null, e=6, d=55}{}");
}
+ @Test
+ public void testTemplateDirectiveModelWithArgsLast() throws IOException, TemplateException {
+ addToDataModel("directive", new TestTemplateDirectiveModel());
+
+ Map<String, Integer> args = new LinkedHashMap<String, Integer>();
+ args.put("a", null);
+ args.put("b", 2);
+ args.put("c", 3);
+ args.put("e", 6);
+ args.put("f", 7);
+ args.put("g", null);
+ addToDataModel("args", args);
+
+ assertOutput("<@directive?withArgsLast(args) b=22 c=null d=55 />",
+ "{b=22, c=null, d=55, a=null, e=6, f=7, g=null}{}");
+
+ assertOutput("<@directive?withArgsLast({}) b=22 c=null d=55 />",
+ "{b=22, c=null, d=55}{}");
+
+ assertOutput("<@directive?withArgsLast(args) />",
+ "{a=null, b=2, c=3, e=6, f=7, g=null}{}");
+ }
+
+ @Test
+ public void testMacroWithArgsLastNamed() throws IOException, TemplateException {
+ assertOutput("<@m?withArgsLast({'a': 1, 'b': 2}) />", "a=1; b=2; c=d3");
+ assertOutput("<@m?withArgsLast({'b': 2}) a=1 />", "a=1; b=2; c=d3");
+ assertOutput("<@m?withArgsLast({}) a=1 b=2 />", "a=1; b=2; c=d3");
+
+ assertOutput("<@m?withArgsLast({'a': 1, 'b': 2, 'c': 3}) />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast({'b': 2}) a=1 c=3 />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast({'c': 3}) a=1 b=2 />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast({}) a=1 b=2 c=3 />", "a=1; b=2; c=3");
+
+ assertOutput("<@m?withArgsLast({'b': 2}) 1 />", "a=1; b=2; c=d3");
+ assertOutput("<@m?withArgsLast({'c': 3}) 1 2 />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast({'b': 22, 'c': 3}) 1 2 />", "a=1; b=2; c=3");
+
+ assertOutput("<@mCA?withArgsLast({'a': 1, 'b': 2, 'c': 3, 'd': 4}) />", "a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?withArgsLast({'b': 2, 'c': 3, 'd': 4}) a=1 />", "a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 b=2 />", "a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?withArgsLast({'d': 4}) a=1 b=2 c=3 />", "a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?withArgsLast({}) a=1 b=2 c=3 d=4 />", "a=1; b=2; o={c=3, d=4}");
+
+ assertOutput("<@mCA?withArgsLast({'a': 11}) 1 2 />", "a=1; b=2; o=[]");
+ assertOutput("<@mCA?withArgsLast({'a': 11, 'c': 3}) 1 2 />", "a=1; b=2; o={c=3}");
+ assertErrorContains("<@mCA?withArgsLast({'a': 11, 'c': 3}) 1 2 3 />", "both named and positional", "catch-all");
+ assertOutput("<@mCA?withArgsLast({'a': 11, 'b': 22}) 1 2 3 />", "a=1; b=2; o=[3]");
+
+ assertOutput("<@mCAO?withArgsLast({'a': 1, 'b': 2}) />", "o={a=1, b=2}");
+ assertOutput("<@mCAO?withArgsLast({'b': 2}) a=1 />", "o={a=1, b=2}");
+ assertOutput("<@mCAO?withArgsLast({}) a=1 b=2 />", "o={a=1, b=2}");
+
+ assertOutput("<@mCAO?withArgsLast({}) />", "o={}");
+
+ // Ordering of "real" args win:
+ assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 b=2 />", "a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 d=44 b=2 />", "a=1; b=2; o={d=44, c=3}");
+ }
+
+ @Test
+ public void testMacroWithArgsLastNamedNullArgs() throws IOException, TemplateException {
+ assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 d=null b=2 />", "a=1; b=2; o={d=null, c=3}");
+ Map<String, Integer> cAndDNull = new LinkedHashMap<String, Integer>();
+ cAndDNull.put("c", 3);
+ cAndDNull.put("d", null);
+ addToDataModel("cAndDNull", cAndDNull);
+ assertOutput("<@mCA?withArgsLast(cAndDNull) a=1 b=2 />", "a=1; b=2; o={c=3, d=null}");
+ assertOutput("<@mCA?withArgsLast(cAndDNull) a=1 d=null b=2 />", "a=1; b=2; o={d=null, c=3}");
+ }
+
+ @Test
+ public void testMacroWithArgsLastPositional() throws IOException, TemplateException {
+ assertOutput("<@m?withArgsLast([1, 2, 3]) />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast([2, 3]) 1 />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast([3]) 1 2 />", "a=1; b=2; c=3");
+ assertOutput("<@m?withArgsLast([]) 1 2 3 />", "a=1; b=2; c=3");
+
+ assertOutput("<@m?withArgsLast([]) a=1 b=2 />", "a=1; b=2; c=d3");
+ assertErrorContains("<@m?withArgsLast([3]) a=1 b=2 />", "by name", "by position", "last");
+
+ assertOutput("<@m?withArgsLast([1, 2]) />", "a=1; b=2; c=d3");
+ assertOutput("<@m?withArgsLast([2]) 1 />", "a=1; b=2; c=d3");
+ assertOutput("<@m?withArgsLast([]) 1 2 />", "a=1; b=2; c=d3");
+
+ assertOutput("<@mCA?withArgsLast([1, 2, 3, 4]) />", "a=1; b=2; o=[3, 4]");
+ assertOutput("<@mCA?withArgsLast([2, 3, 4]) 1 />", "a=1; b=2; o=[3, 4]");
+ assertOutput("<@mCA?withArgsLast([3, 4]) 1 2 />", "a=1; b=2; o=[3, 4]");
+ assertOutput("<@mCA?withArgsLast([4]) 1 2 3 />", "a=1; b=2; o=[3, 4]");
+ assertOutput("<@mCA?withArgsLast([]) 1 2 3 4 />", "a=1; b=2; o=[3, 4]");
+
+ assertOutput("<@mCAO?withArgsLast([1, 2, 3, 4]) />", "o=[1, 2, 3, 4]");
+ assertOutput("<@mCAO?withArgsLast([3, 4]) 1 2 />", "o=[1, 2, 3, 4]");
+ assertOutput("<@mCAO?withArgsLast([]) 1 2 3 4 />", "o=[1, 2, 3, 4]");
+
+ assertOutput("<@mCAO?withArgsLast([]) a=1 b=2 />", "o={a=1, b=2}");
+ assertErrorContains("<@mCAO?withArgsLast([3]) a=1 b=2 />", "by name", "by position", "last");
+
+ assertOutput("<@mCAO?withArgsLast([]) />", "o=[]");
+
+ assertErrorContains("<@m?withArgsLast([0, 0, 0, 0]) />", "3", "4", "parameter");
+ assertErrorContains("<@m?withArgsLast([0, 0, 0]) 0 />", "3", "4", "parameter");
+ assertErrorContains("<@m?withArgsLast([]) 0 0 0 0 />", "3", "4", "parameter");
+ }
+
+ @Test
+ public void testMacroWithArgsLastPositionalNullArgs() throws IOException, TemplateException {
+ ArrayList<Object> twoAndNull = new ArrayList<Object>();
+ twoAndNull.add(2);
+ twoAndNull.add(null);
+ addToDataModel("twoAndNull", twoAndNull);
+
+ assertOutput("<@m?withArgsLast(twoAndNull) 1 />", "a=1; b=2; c=d3");
+ assertErrorContains("<@m?withArgsLast([3]) null 2 />", "\"a\"", "null");
+ assertOutput("<@m?withArgsLast([]) 1 2 null />", "a=1; b=2; c=d3");
+
+ assertOutput("<@mCAO?withArgsLast(twoAndNull) 1 />", "o=[1, 2, null]");
+ assertOutput("<@mCAO?withArgsLast([3]) null 2 />", "o=[null, 2, 3]");
+ }
+
private static class TestTemplateDirectiveModel implements TemplateDirectiveModel {
public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws
diff --git a/src/test/java/freemarker/manual/WithArgsLastExamples.java b/src/test/java/freemarker/manual/WithArgsLastExamples.java
new file mode 100644
index 0000000..b59dca8
--- /dev/null
+++ b/src/test/java/freemarker/manual/WithArgsLastExamples.java
@@ -0,0 +1,35 @@
+/*
+ * 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.manual;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import freemarker.template.TemplateException;
+
+public class WithArgsLastExamples extends ExamplesTest {
+
+ @Test
+ public void usingWithArgsSpecialVariable() throws IOException, TemplateException {
+ assertOutputForNamed("WithArgsLastExamples.ftl");
+ }
+
+}
diff --git a/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
new file mode 100644
index 0000000..4494b13
--- /dev/null
+++ b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl
@@ -0,0 +1,25 @@
+<#function f a b c d>
+ <#return "a=${a}, b=${b}, c=${c}, d=${d}">
+</#function>
+
+${f?with_args([2, 3])(1, 2)}
+${f?with_args_last([2, 3])(1, 2)}
+
+<#macro m a b others...>
+ a=${a}
+ b=${b}
+ others:
+ <#list others as k, v>
+ ${k} = ${v}
+ </#list>
+</#macro>
+<@m?with_args({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 />
+<@m?with_args_last({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 />
+
+<#macro m a b others...>
+ <#list .args as k, v>
+ ${k} = ${v}
+ </#list>
+</#macro>
+<@m?with_args({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 />
+<@m?with_args_last({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 />