diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java index d10755403..4ab54480e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/CallTag.java @@ -1,11 +1,20 @@ package com.hubspot.jinjava.lib.tag; import java.util.LinkedHashMap; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.JinjavaInterpreter.InterpreterScopeClosable; +import com.hubspot.jinjava.interpret.TemplateSyntaxException; import com.hubspot.jinjava.lib.fn.MacroFunction; import com.hubspot.jinjava.tree.TagNode; @@ -52,6 +61,9 @@ public class CallTag implements Tag { private static final long serialVersionUID = 7231253469979314727L; + private static final Pattern CALL_PATTERN = Pattern.compile("(?:\\(([^\\)]*)\\))?(.*)"); + private static final Splitter ARGS_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults(); + @Override public String getName() { return "call"; @@ -64,15 +76,49 @@ public String getEndTagName() { @Override public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { - String macroExpr = "{{" + tagNode.getHelpers().trim() + "}}"; + Matcher matcher = CALL_PATTERN.matcher(tagNode.getHelpers().trim()); + if (!matcher.find()) { + throw new TemplateSyntaxException(tagNode.getMaster().getImage(), "Unable to parse call block: " + tagNode.getHelpers(), tagNode.getLineNumber(), tagNode.getStartPosition()); + } + + String args = Strings.nullToEmpty(matcher.group(1)); + String macro = matcher.group(2); + + LinkedHashMap argNamesWithDefaults = new LinkedHashMap<>(); + + List argList = Lists.newArrayList(ARGS_SPLITTER.split(args)); + for (int i = 0; i < argList.size(); i++) { + String arg = argList.get(i); + + if (arg.contains("=")) { + String argName = StringUtils.substringBefore(arg, "=").trim(); + StringBuilder argValStr = new StringBuilder(StringUtils.substringAfter(arg, "=").trim()); + + if (StringUtils.startsWith(argValStr, "[") && !StringUtils.endsWith(argValStr, "]")) { + while (i + 1 < argList.size() && !StringUtils.endsWith(argValStr, "]")) { + argValStr.append(", ").append(argList.get(i + 1)); + i++; + } + } + + Object argVal = interpreter.resolveELExpression(argValStr.toString(), tagNode.getLineNumber()); + argNamesWithDefaults.put(argName, argVal); + } else { + argNamesWithDefaults.put(arg, null); + } + } + + String macroExpr = "{{" + macro + "}}"; + String result = null; try (InterpreterScopeClosable c = interpreter.enterScope()) { - LinkedHashMap args = new LinkedHashMap<>(); - MacroFunction caller = new MacroFunction(tagNode.getChildren(), "caller", args, false, false, true, interpreter.getContext()); + MacroFunction caller = new MacroFunction(tagNode.getChildren(), "caller", argNamesWithDefaults, false, false, true, interpreter.getContext()); interpreter.getContext().addGlobalMacro(caller); - return interpreter.render(macroExpr); + result = interpreter.render(macroExpr); } + + return result; } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java index 883b8fd85..224421777 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/CallTagTest.java @@ -4,6 +4,12 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; @@ -14,15 +20,18 @@ import com.google.common.base.Throwables; import com.google.common.io.Resources; import com.hubspot.jinjava.Jinjava; +import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.JinjavaInterpreter; public class CallTagTest { JinjavaInterpreter interpreter; + Context context; @Before public void setup() { interpreter = new Jinjava().newInterpreter(); + context = interpreter.getContext(); JinjavaInterpreter.pushCurrent(interpreter); } @@ -38,6 +47,72 @@ public void testSimpleFn() { assertThat(dom.select("div.contents").text().trim()).isEqualTo("This is a simple dialog rendered by using a macro and a call block."); } + @Test + public void testArgsFn() { + List> users = new ArrayList>(); + Map u = new HashMap(); + u.put("username", "jdoe"); + u.put("realname", "John Doe"); + u.put("description", "Test user"); + users.add(u); + u = new HashMap(); + u.put("username", "root"); + u.put("realname", "God"); + u.put("description", "Superuser"); + users.add(u); + context.put("list_of_user", users); + Document dom = Jsoup.parseBodyFragment(interpreter.render(fixture("args"))); + assertThat(dom.select("ul p").text().trim()).isEqualTo("jdoe root"); + assertThat(dom.select("li:first-child dl > dl:first-child + dd").text().trim()).isEqualTo("John Doe"); + assertThat(dom.select("li:first-child dl > dl:first-child + dd + dl + dd").text().trim()).isEqualTo("Test user"); + assertThat(dom.select("li:first-child + li dl > dl:first-child + dd").text().trim()).isEqualTo("God"); + assertThat(dom.select("li:first-child + li dl > dl:first-child + dd + dl + dd").text().trim()).isEqualTo("Superuser"); + } + + @Test + public void testMultTable() { + context.put("list_of_numbers", IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList())); + Document dom = Jsoup.parseBodyFragment(interpreter.render(fixture("mult_table"))); + assertThat(dom.select("#3x3").text().trim()).isEqualTo("9"); + assertThat(dom.select("#9x3").text().trim()).isEqualTo("27"); + assertThat(dom.select("#3x9").text().trim()).isEqualTo("27"); + } + + @Test + public void testCallerDefaultArgs() { + String template = fixture("default_args"); + String out = interpreter.render(template).trim(); + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(out).contains("regular=some_arg"); + assertThat(out).contains("default_str=foo"); + assertThat(out).contains("default_expr=12"); + assertThat(out).contains("default_list=[42, 23, 666]"); + } + + @Test + public void testCallerOverrideArgs() { + String template = fixture("override_args"); + String out = interpreter.render(template).trim(); + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(out).contains("regular=some_arg"); + assertThat(out).contains("default_str=123"); + assertThat(out).contains("default_expr=42"); + assertThat(out).contains("default_list=oops, no list"); + } + + @Test + public void testCallerVarKwArgs() { + String template = fixture("caller_var_kw"); + String out = interpreter.render(template).trim(); + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(out).contains("arg=some_arg"); + assertThat(out).contains("vararg: var_arg"); + assertThat(out).contains("vararg: 666"); + assertThat(out).contains("kw: default_list=oops, no list"); + assertThat(out).contains("kw: default_str=123"); + assertThat(out).contains("kw: default_expr=42"); + } + private String fixture(String name) { try { return Resources.toString(Resources.getResource(String.format("tags/calltag/%s.jinja", name)), StandardCharsets.UTF_8); diff --git a/src/test/resources/tags/calltag/args.jinja b/src/test/resources/tags/calltag/args.jinja new file mode 100755 index 000000000..1fbddc771 --- /dev/null +++ b/src/test/resources/tags/calltag/args.jinja @@ -0,0 +1,16 @@ +{% macro dump_users(users) -%} +
    + {%- for user in users %} +
  • {{ user.username|e }}

    {{ caller(user) }}
  • + {%- endfor %} +
+{%- endmacro %} + +{% call(user) dump_users(list_of_user) %} +
+
Realname
+
{{ user.realname|e }}
+
Description
+
{{ user.description }}
+
+{% endcall %} diff --git a/src/test/resources/tags/calltag/caller_var_kw.jinja b/src/test/resources/tags/calltag/caller_var_kw.jinja new file mode 100644 index 000000000..41f904462 --- /dev/null +++ b/src/test/resources/tags/calltag/caller_var_kw.jinja @@ -0,0 +1,13 @@ +{% macro callme() %} + {{ caller("some_arg", "var_arg", 666, default_list="oops, no list", default_str=123, default_expr=42) }} +{% endmacro %} + +{% call(arg) callme() %} + arg={{arg}} + {% for a in varargs %} + vararg: {{a}} + {% endfor %} + {% for k,v in kwargs.items() %} + kw: {{k}}={{v}} + {% endfor %} +{% endcall %} diff --git a/src/test/resources/tags/calltag/default_args.jinja b/src/test/resources/tags/calltag/default_args.jinja new file mode 100644 index 000000000..e162303ff --- /dev/null +++ b/src/test/resources/tags/calltag/default_args.jinja @@ -0,0 +1,10 @@ +{% macro callme() %} + {{ caller("some_arg") }} +{% endmacro %} + +{% call(regular, default_str="foo", default_expr=3*4, default_list=[42,23,666]) callme() %} + regular={{regular}} + default_str={{default_str}} + default_expr={{default_expr}} + default_list={{default_list}} +{% endcall %} diff --git a/src/test/resources/tags/calltag/mult_table.jinja b/src/test/resources/tags/calltag/mult_table.jinja new file mode 100755 index 000000000..03bb3831d --- /dev/null +++ b/src/test/resources/tags/calltag/mult_table.jinja @@ -0,0 +1,17 @@ +{% macro multiplication_table(numbers) -%} + + {%- for a in numbers %} + + {%- for b in numbers -%} + {{ caller(a, b, a*b) }} + {%- endfor -%} + + {%- endfor %} +
+{%- endmacro %} + +{% call(a, b, prod) multiplication_table(list_of_numbers) %} + + {{ prod }} + +{% endcall %} diff --git a/src/test/resources/tags/calltag/override_args.jinja b/src/test/resources/tags/calltag/override_args.jinja new file mode 100644 index 000000000..4aa2a91d3 --- /dev/null +++ b/src/test/resources/tags/calltag/override_args.jinja @@ -0,0 +1,10 @@ +{% macro callme() %} + {{ caller("some_arg", default_list="oops, no list", default_str=123, default_expr=42) }} +{% endmacro %} + +{% call(regular, default_str="foo", default_expr=3*4, default_list=[42,23,666]) callme() %} + regular={{regular}} + default_str={{default_str}} + default_expr={{default_expr}} + default_list={{default_list}} +{% endcall %}