FREEMARKER-83: Added new special variable, macro_caller_template_name, which returns the name (path) of the template from which the current macro was called. It's mostly useful if you want to resolve paths relative to the caller template.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/4a5eec42 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/4a5eec42 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/4a5eec42 Branch: refs/heads/2.3 Commit: 4a5eec42dcfed0058dcef03c930ad57ec3dfaa27 Parents: 846ef94 Author: ddekany <ddek...@apache.org> Authored: Fri Mar 9 22:06:45 2018 +0100 Committer: ddekany <ddek...@apache.org> Committed: Fri Mar 9 22:06:45 2018 +0100 ---------------------------------------------------------------------- .../java/freemarker/core/BuiltinVariable.java | 14 ++ src/main/java/freemarker/core/Environment.java | 30 ++++ .../java/freemarker/template/Configuration.java | 3 +- src/manual/en_US/book.xml | 31 ++++ .../core/MacroCallerTemplateNameTest.java | 155 +++++++++++++++++++ 5 files changed, 232 insertions(+), 1 deletion(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/4a5eec42/src/main/java/freemarker/core/BuiltinVariable.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java index 1b7617c..9b9b135 100644 --- a/src/main/java/freemarker/core/BuiltinVariable.java +++ b/src/main/java/freemarker/core/BuiltinVariable.java @@ -74,6 +74,8 @@ final class BuiltinVariable extends Expression { static final String NOW = "now"; static final String GET_OPTIONAL_TEMPLATE = "get_optional_template"; static final String GET_OPTIONAL_TEMPLATE_CC = "getOptionalTemplate"; + static final String MACRO_CALLER_TEMPLATE_NAME = "macro_caller_template_name"; + static final String MACRO_CALLER_TEMPLATE_NAME_CC = "macroCallerTemplateName"; static final String[] SPEC_VAR_NAMES = new String[] { AUTO_ESC_CC, AUTO_ESC, @@ -94,6 +96,8 @@ final class BuiltinVariable extends Expression { LOCALE_OBJECT_CC, LOCALE_OBJECT, LOCALS, + MACRO_CALLER_TEMPLATE_NAME_CC, + MACRO_CALLER_TEMPLATE_NAME, MAIN, MAIN_TEMPLATE_NAME_CC, MAIN_TEMPLATE_NAME, @@ -248,6 +252,16 @@ final class BuiltinVariable extends Expression { if (name == GET_OPTIONAL_TEMPLATE_CC) { return GetOptionalTemplateMethod.INSTANCE_CC; } + if (name == MACRO_CALLER_TEMPLATE_NAME || name == MACRO_CALLER_TEMPLATE_NAME_CC) { + UnifiedCall caller; + try { + caller = env.getMacroCaller(); + } catch (IllegalStateException e) { + throw new TemplateException("Failed to resolve ." + name + ": " + e.getMessage(), e, env); + } + String name = caller.getTemplate().getName(); + return name != null ? new SimpleScalar(name) : SimpleScalar.EMPTY_STRING; + } throw new _MiscTemplateException(this, "Invalid special variable: ", name); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/4a5eec42/src/main/java/freemarker/core/Environment.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index 209464c..5f20c26 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -281,6 +281,36 @@ public final class Environment extends Configurable { } /** + * Gets the non-{@code null} {@link UnifiedCall} of the caller of the macro whose context we are in; note + * that you can't call this from everywhere. Specifically, the FTL call stack must not contain {@code #nested} or a + * call to user-defined-directive after the stack entry of the {@code #macro} directive. This practically means that + * this should be called on the top-level inside {@code #macro}, or inside some core directives like {@code #if}. + * + * @throws IllegalStateException + * If there's no macro caller or it can't be figured out at this point of the template execution. + */ + UnifiedCall getMacroCaller() throws IllegalStateException { + for (int ln = instructionStackSize - 1; ln > 0; ln--) { + TemplateElement te = instructionStack[ln]; + if (te instanceof Macro) { + TemplateElement macroCaller = instructionStack[ln - 1]; + if (macroCaller instanceof UnifiedCall) { + return (UnifiedCall) macroCaller; + } + } + // Avoid returning the caller of @nested in `<#macro called><@inner>${getMacroCallerHere()}</@></#macro>`; + // the #macro that defines "inner" would break our logic above. + if (te instanceof BodyInstruction) { + throw new IllegalStateException( + "Can't get the location of the macro caller here, because you are inside an user-defined " + + "directive call that's nested inside the #macro directive. (You may " + + "need to get the caller location earlier, and store it in a local variable for later use.)"); + } + } + throw new IllegalStateException("There's no macro caller at this point."); + } + + /** * Deletes cached values that meant to be valid only during a single template execution. */ private void clearCachedValues() { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/4a5eec42/src/main/java/freemarker/template/Configuration.java ---------------------------------------------------------------------- diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java index c560d70..3673272 100644 --- a/src/main/java/freemarker/template/Configuration.java +++ b/src/main/java/freemarker/template/Configuration.java @@ -863,7 +863,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf * argument list contains {@code .current_template_name}, now it will correctly evaluate to the template * that contains the call, rather than to the template that contains the macro or function definition. * (Of course, the parameter default value expression is still evaluated in the context of the called - * macro or function.) + * macro or function.) Similarly, {@code .macro_caller_template_name} (which itself was added in 2.3.28), + * when used in a macro call argument, won't be incorrectly evaluated in the context of the called macro. * </ul> * </li> * </ul> http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/4a5eec42/src/manual/en_US/book.xml ---------------------------------------------------------------------- diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index 2346be1..6c9c8bb 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -23311,6 +23311,27 @@ There was no specific handler for node y </listitem> <listitem> + <para><indexterm> + <primary>macro_caller_template_name</primary> + </indexterm><literal>macro_caller_template_name</literal>: Returns + the name (path) of the template from which the current macro was + called. It's mostly useful if you want to resolve paths relative to + the caller template. If the caller template is nameless, this will + be an empty string (not a missing value). Reading this variable will + cause error if you aren't inside a macro call, also if you are + inside an user-defined directive call + (<literal><@<replaceable>...</replaceable>></literal>) that's + nested inside the <literal>macro</literal> directive (as in + <literal><#macro + m><@x>${.macro_caller_template_name}<#-- FAILS! + --></@></#macro></literal>). (Note that if <link + linkend="pgui_config_incompatible_improvements">incompatible + improvements</link> is set to less than 2.3.28, then when using this + variable in an argument to a macro, it will be incorrectly evaluated + to the caller of the called macro.)</para> + </listitem> + + <listitem> <para><literal>main</literal>: A hash that you can use to access the main <link linkend="dgui_misc_namespace">namespace</link>. Note that global variables like the variables of data-model are @@ -27269,6 +27290,16 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> </listitem> <listitem> + <para>Added new <link linkend="ref_specvar">special + variable</link>, <literal>macro_caller_template_name</literal> + (<link + xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-83">FREEMARKER-83</link>), + which returns the name (path) of the template from which the + current macro was called. It's mostly useful if you want to + resolve paths relative to the caller template.</para> + </listitem> + + <listitem> <para>Bug fixed (<link xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-83">FREEMARKER-83</link>); this fix is only active when <link http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/4a5eec42/src/test/java/freemarker/core/MacroCallerTemplateNameTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/freemarker/core/MacroCallerTemplateNameTest.java b/src/test/java/freemarker/core/MacroCallerTemplateNameTest.java new file mode 100644 index 0000000..32b1d4e --- /dev/null +++ b/src/test/java/freemarker/core/MacroCallerTemplateNameTest.java @@ -0,0 +1,155 @@ +/* + * 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 org.junit.Test; + +import freemarker.template.Configuration; +import freemarker.test.TemplateTest; + +public class MacroCallerTemplateNameTest extends TemplateTest { + + @Override + protected Configuration createConfiguration() throws Exception { + Configuration cfg = super.createConfiguration(); + cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_28); + return cfg; + } + + @Test + public void testNoCaller() throws Exception { + assertErrorContains("${.macroCallerTemplateName}", "no macro caller", ".macroCallerTemplateName"); + assertErrorContains("${.macro_caller_template_name}", "no macro caller", ".macro_caller_template_name"); + + assertErrorContains("" + + "<#macro m><#nested></#macro>" + + "<@m>${.macroCallerTemplateName}</@>", + "nested", ".macroCallerTemplateName"); + + assertErrorContains("" + + "<#macro m><#nested></#macro>" + + "<#macro m2><@m>${.macroCallerTemplateName}</@></#macro>" + + "<@m2/>", + "nested", ".macroCallerTemplateName"); + assertOutput("" + + "<#macro m2>${.macroCallerTemplateName}</#macro>" + + "[<@m2/>]", + "[]"); + + addTemplate("main.ftl", "${.macroCallerTemplateName}"); + assertErrorContainsForNamed("main.ftl", "no macro caller"); + } + + @Test + public void testSameTemplateCaller() throws Exception { + addTemplate("main.ftl", "" + + "<#macro m>${.macroCallerTemplateName}</#macro>" + + "<@m />, <#attempt>${.macroCallerTemplateName}<#recover>-</#attempt>"); + assertOutputForNamed("main.ftl", "main.ftl, -"); + } + + @Test + public void testIncludedTemplateCaller() throws Exception { + addTemplate("main.ftl", "" + + "<#include 'lib/foo.ftl'>" + + "<@m />, <@m2 />"); + addTemplate("lib/foo.ftl", "" + + "<#macro m>${.macroCallerTemplateName}</#macro>" + + "<#macro m2><@m3/></#macro>" + + "<#macro m3>${.macroCallerTemplateName}</#macro>"); + assertOutputForNamed("main.ftl", + "main.ftl, lib/foo.ftl"); + } + + @Test + public void testImportedTemplateCaller() throws Exception { + addTemplate("main.ftl", "" + + "<#import 'lib/foo.ftl' as foo>" + + "<@foo.m />, <@foo.m2 />"); + addTemplate("lib/foo.ftl", "" + + "<#macro m>${.macroCallerTemplateName}</#macro>" + + "<#macro m2><@m3/></#macro>" + + "<#macro m3>${.macroCallerTemplateName}</#macro>"); + assertOutputForNamed("main.ftl", + "main.ftl, lib/foo.ftl"); + } + + @Test + public void testNestedIntoNonUserDirectives() throws Exception { + addTemplate("main.ftl", "" + + "<#macro m><#list 1..2 as _><#if true>${.macroCallerTemplateName}</#if>;</#list></#macro>" + + "<@m/>"); + assertOutputForNamed("main.ftl", "main.ftl;main.ftl;"); + } + + @Test + public void testMulitpleLevels() throws Exception { + addTemplate("main.ftl", "" + + "<#include 'inc1.ftl'>" + + "<@m1 />"); + addTemplate("inc1.ftl", "" + + "<#include 'inc2.ftl'>" + + "<#macro m1>m1: ${.macroCallerTemplateName}; <@m2 /></#macro>"); + addTemplate("inc2.ftl", "" + + "<#macro m2>m2: ${.macroCallerTemplateName};</#macro>"); + assertOutputForNamed("main.ftl", "m1: main.ftl; m2: inc1.ftl;"); + } + + @Test + public void testUsedInArgument() throws Exception { + addTemplate("main.ftl", "" + + "<#include 'inc.ftl'>" + + "<#macro start>" + + "<@m .macroCallerTemplateName />" + + "<@m2 />" + + "</#macro>" + + "<@start />"); + addTemplate("inc.ftl", "" + + "<#macro m x y=.macroCallerTemplateName>" + + "x: ${x}; y: ${y}; caller: ${.macroCallerTemplateName};" + + "</#macro>" + + "<#macro m2><@m .macroCallerTemplateName /></#macro>"); + + assertOutputForNamed("main.ftl", "" + + "x: main.ftl; y: main.ftl; caller: main.ftl;" + + "x: main.ftl; y: inc.ftl; caller: inc.ftl;"); + getConfiguration().setIncompatibleImprovements(Configuration.VERSION_2_3_27); + assertOutputForNamed("main.ftl", "" + + "x: main.ftl; y: main.ftl; caller: main.ftl;" + + "x: inc.ftl; y: inc.ftl; caller: inc.ftl;"); + } + + @Test + public void testReturnsLookupName() throws Exception { + addTemplate("main_en.ftl", "" + + "<#macro m>${.macroCallerTemplateName}</#macro>" + + "<@m />"); + assertOutputForNamed("main.ftl", "main.ftl"); // Not main_en.ftl + } + + @Test + public void testLegacyCall() throws Exception { + addTemplate("main_en.ftl", "" + + "<#macro m>${.macroCallerTemplateName}</#macro>" + + "<#call m>"); + assertOutputForNamed("main.ftl", "main.ftl"); // Not main_en.ftl + } + +}