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>&lt;@<replaceable>...</replaceable>&gt;</literal>) that's
+          nested inside the <literal>macro</literal> directive (as in
+          <literal>&lt;#macro
+          m&gt;&lt;@x&gt;${.macro_caller_template_name}&lt;#-- FAILS!
+          --&gt;&lt;/@&gt;&lt;/#macro&gt;</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
+    }
+    
+}

Reply via email to