Skip to content
6 changes: 4 additions & 2 deletions src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,16 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) {
Map<String, String> imports = getImportMap(helper);

try {
String template = interpreter.getResource(templateFile);
Node node = interpreter.parse(template);
Node node = ImportTag.parseTemplateAsNode(interpreter, templateFile);

JinjavaInterpreter child = interpreter
.getConfig()
.getInterpreterFactory()
.newInstance(interpreter);
child.getContext().put(Context.IMPORT_RESOURCE_PATH_KEY, templateFile);

JinjavaInterpreter.pushCurrent(child);

try {
child.render(node);
} finally {
Expand Down Expand Up @@ -125,6 +126,7 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) {
}
} finally {
interpreter.getContext().popFromStack();
interpreter.getContext().getCurrentPathStack().pop();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.hubspot.jinjava.lib.fn.eager.EagerMacroFunction;
import com.hubspot.jinjava.lib.tag.DoTag;
import com.hubspot.jinjava.lib.tag.FromTag;
import com.hubspot.jinjava.lib.tag.ImportTag;
import com.hubspot.jinjava.lib.tag.eager.importing.EagerImportingStrategyFactory;
import com.hubspot.jinjava.tree.Node;
import com.hubspot.jinjava.tree.parse.TagToken;
Expand Down Expand Up @@ -81,8 +82,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter
String templateFile = maybeTemplateFile.get();
try {
try {
String template = interpreter.getResource(templateFile);
Node node = interpreter.parse(template);
Node node = ImportTag.parseTemplateAsNode(interpreter, templateFile);

JinjavaInterpreter child = interpreter
.getConfig()
Expand Down Expand Up @@ -138,6 +138,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter
}
} finally {
interpreter.getContext().popFromStack();
interpreter.getContext().getCurrentPathStack().pop();
}
}

Expand Down
204 changes: 196 additions & 8 deletions src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.hubspot.jinjava.lib.tag;

import static com.hubspot.jinjava.lib.tag.ResourceLocatorTestHelper.getTestResourceLocator;
import static com.hubspot.jinjava.loader.RelativePathResolver.CURRENT_PATH_CONTEXT_KEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;

import com.google.common.io.Resources;
import com.hubspot.jinjava.BaseInterpretingTest;
Expand All @@ -16,6 +16,7 @@
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
Expand All @@ -26,7 +27,8 @@ public class FromTagTest extends BaseInterpretingTest {
public void setup() {
jinjava.setResourceLocator(
new ResourceLocator() {
private RelativePathResolver relativePathResolver = new RelativePathResolver();
private final RelativePathResolver relativePathResolver =
new RelativePathResolver();

@Override
public String getString(
Expand Down Expand Up @@ -66,25 +68,27 @@ public void itImportsAliasedMacroName() {
}

@Test
public void importedCycleDected() {
public void importedCycleDetected() {
fixture("from-recursion");
assertTrue(
assertThat(
interpreter
.getErrorsCopy()
.stream()
.anyMatch(e -> e.getCategory() == BasicTemplateErrorCategory.FROM_CYCLE_DETECTED)
);
)
.isTrue();
}

@Test
public void importedIndirectCycleDected() {
public void importedIndirectCycleDetected() {
fixture("from-a-to-b");
assertTrue(
assertThat(
interpreter
.getErrorsCopy()
.stream()
.anyMatch(e -> e.getCategory() == BasicTemplateErrorCategory.FROM_CYCLE_DETECTED)
);
)
.isTrue();
}

@Test
Expand Down Expand Up @@ -112,6 +116,190 @@ public void itDefersImport() {
assertThat(spacer.isDeferred()).isTrue();
}

@Test
public void itResolvesNestedRelativeImports() throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"level0.jinja",
"{% from 'level1/nested.jinja' import macro1 %}{{ macro1() }}",
"level1/nested.jinja",
"{% from '../level1/deeper/macro.jinja' import macro2 %}{% macro macro1() %}L1:{{ macro2() }}{% endmacro %}",
"level1/deeper/macro.jinja",
"{% from '../../utils/helper.jinja' import helper %}{% macro macro2() %}L2:{{ helper() }}{% endmacro %}",
"utils/helper.jinja",
"{% macro helper() %}HELPER{% endmacro %}"
)
)
);

interpreter.getContext().getCurrentPathStack().push("level0.jinja", 1, 0);
String result = interpreter.render(interpreter.getResource("level0.jinja"));

assertThat(interpreter.getErrors()).isEmpty();
assertThat(result.trim()).isEqualTo("L1:L2:HELPER");
}

@Test
public void itMaintainsPathStackIntegrity() throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"root.jinja",
"{% from 'simple/macro.jinja' import simple_macro %}{{ simple_macro() }}",
"simple/macro.jinja",
"{% macro simple_macro() %}SIMPLE{% endmacro %}"
)
)
);

interpreter.getContext().getCurrentPathStack().push("root.jinja", 1, 0);
Optional<String> initialTopPath = interpreter
.getContext()
.getCurrentPathStack()
.peek();

interpreter.render(interpreter.getResource("root.jinja"));

assertThat(interpreter.getContext().getCurrentPathStack().peek())
.isEqualTo(initialTopPath);
assertThat(interpreter.getErrors()).isEmpty();
}

@Test
public void itWorksWithIncludeAndFromTogether() throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"mixed-tags.jinja",
"{% from 'macros/test.jinja' import test_macro %}{% include 'includes/content.jinja' %}{{ test_macro() }}",
"macros/test.jinja",
"{% from '../utils/shared.jinja' import shared %}{% macro test_macro() %}MACRO:{{ shared() }}{% endmacro %}",
"includes/content.jinja",
"{% from '../utils/shared.jinja' import shared %}INCLUDE:{{ shared() }}",
"utils/shared.jinja",
"{% macro shared() %}SHARED{% endmacro %}"
)
)
);

interpreter.getContext().getCurrentPathStack().push("mixed-tags.jinja", 1, 0);
String result = interpreter.render(interpreter.getResource("mixed-tags.jinja"));

assertThat(interpreter.getErrors()).isEmpty();
assertThat(result.trim()).contains("INCLUDE:SHARED");
assertThat(result.trim()).contains("MACRO:SHARED");
}

@Test
public void itResolvesUpAndAcrossDirectoryPaths() throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"theme/hubl-modules/navigation.module/module.hubl.html",
"{% from '../../partials/atoms/link/link.hubl.html' import link_macro %}{{ link_macro() }}",
"theme/partials/atoms/link/link.hubl.html",
"{% from '../icons/icons.hubl.html' import icon_macro %}{% macro link_macro() %}LINK:{{ icon_macro() }}{% endmacro %}",
"theme/partials/atoms/icons/icons.hubl.html",
"{% macro icon_macro() %}ICON{% endmacro %}"
)
)
);

interpreter
.getContext()
.getCurrentPathStack()
.push("theme/hubl-modules/navigation.module/module.hubl.html", 1, 0);
String result = interpreter.render(
interpreter.getResource("theme/hubl-modules/navigation.module/module.hubl.html")
);

assertThat(interpreter.getErrors()).isEmpty();
assertThat(result.trim()).isEqualTo("LINK:ICON");
}

@Test
public void itResolvesProjectsAbsolutePathsWithNestedRelativeImports()
throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"@projects/theme-a/modules/header/header.html",
"{% from '../../components/button.html' import render_button %}{{ render_button('primary') }}",
"@projects/theme-a/components/button.html",
"{% from '../utils/icons.html' import get_icon %}{% macro render_button(type) %}{{ type }}-{{ get_icon() }}{% endmacro %}",
"@projects/theme-a/utils/icons.html",
"{% macro get_icon() %}ICON{% endmacro %}"
)
)
);

interpreter
.getContext()
.getCurrentPathStack()
.push("@projects/theme-a/modules/header/header.html", 1, 0);
String result = interpreter.render(
interpreter.getResource("@projects/theme-a/modules/header/header.html")
);

assertThat(interpreter.getErrors()).isEmpty();
assertThat(result.trim()).isEqualTo("primary-ICON");
}

@Test
public void itResolvesHubspotAbsolutePathsWithNestedRelativeImports() throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"@hubspot/modules/forms/contact-form.html",
"{% from '../../shared/validation.html' import validate_field %}{{ validate_field('email') }}",
"@hubspot/shared/validation.html",
"{% from '../helpers/formatters.html' import format_error %}{% macro validate_field(field) %}{{ format_error(field) }}{% endmacro %}",
"@hubspot/helpers/formatters.html",
"{% macro format_error(field) %}ERROR:{{ field }}{% endmacro %}"
)
)
);

interpreter
.getContext()
.getCurrentPathStack()
.push("@hubspot/modules/forms/contact-form.html", 1, 0);
String result = interpreter.render(
interpreter.getResource("@hubspot/modules/forms/contact-form.html")
);

assertThat(interpreter.getErrors()).isEmpty();
assertThat(result.trim()).isEqualTo("ERROR:email");
}

@Test
public void itResolvesMixedAbsoluteAndRelativeImports() throws Exception {
jinjava.setResourceLocator(
getTestResourceLocator(
Map.of(
"@projects/mixed/module.html",
"{% from '@hubspot/shared/globals.html' import global_helper %}{{ global_helper() }}",
"@hubspot/shared/globals.html",
"{% from '../utils/common.html' import format_text %}{% macro global_helper() %}{{ format_text('MIXED') }}{% endmacro %}",
"@hubspot/utils/common.html",
"{% macro format_text(text) %}FORMAT:{{ text }}{% endmacro %}"
)
)
);

interpreter
.getContext()
.getCurrentPathStack()
.push("@projects/mixed/module.html", 1, 0);
String result = interpreter.render(
interpreter.getResource("@projects/mixed/module.html")
);

assertThat(interpreter.getErrors()).isEmpty();
assertThat(result.trim()).isEqualTo("FORMAT:MIXED");
}

private String fixture(String name) {
return interpreter.renderFlat(fixtureText(name));
}
Expand Down
Loading