diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java index 8bbb4df65..508e03ec8 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/FromTag.java @@ -85,15 +85,16 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { Map 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 { @@ -125,6 +126,7 @@ public String interpret(TagNode tagNode, JinjavaInterpreter interpreter) { } } finally { interpreter.getContext().popFromStack(); + interpreter.getContext().getCurrentPathStack().pop(); } } diff --git a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java index 6514a82a0..b723d2db6 100644 --- a/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java +++ b/src/main/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTag.java @@ -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; @@ -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() @@ -138,6 +138,7 @@ public String getEagerTagImage(TagToken tagToken, JinjavaInterpreter interpreter } } finally { interpreter.getContext().popFromStack(); + interpreter.getContext().getCurrentPathStack().pop(); } } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java index 36dd47886..77d694445 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/FromTagTest.java @@ -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; @@ -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; @@ -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( @@ -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 @@ -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 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)); } diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java index 722bc0aa2..b4d726ea2 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ImportTagTest.java @@ -1,5 +1,6 @@ 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.assertj.core.api.Assertions.entry; @@ -370,6 +371,100 @@ public void itCorrectlySetsNestedPaths() { .isEqualTo("double-import-macro.jinja\n\nimport-macro.jinja\nfoo\n"); } + @Test + public void itResolvesNestedRelativeImports() throws Exception { + jinjava.setResourceLocator( + getTestResourceLocator( + Map.of( + "level0.jinja", + "{% import './level1/level1.jinja' as l1 %}{{ l1.macro_level1() }}", + "level1/level1.jinja", + "{% import './deeper/macro.jinja' as helper %}{% macro macro_level1() %}L1:{{ helper.helper_macro() }}{% endmacro %}", + "level1/deeper/macro.jinja", + "{% macro helper_macro() %}L2: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 itResolvesUpAndAcrossDirectoryPaths() throws Exception { + jinjava.setResourceLocator( + getTestResourceLocator( + Map.of( + "base.jinja", + "{% import './theme/modules/header/header.hubl.html' as header %}{{ header.render_header() }}", + "theme/modules/header/header.hubl.html", + "{% import '../../partials/atoms/link/link.hubl.html' as link %}{% macro render_header() %}{{ link.render_link() }}{% endmacro %}", + "theme/partials/atoms/link/link.hubl.html", + "{% macro render_link() %}LINK{% endmacro %}" + ) + ) + ); + + interpreter.getContext().getCurrentPathStack().push("base.jinja", 1, 0); + String result = interpreter.render(interpreter.getResource("base.jinja")); + + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(result.trim()).isEqualTo("LINK"); + } + + @Test + public void itResolvesProjectsAbsolutePaths() throws Exception { + jinjava.setResourceLocator( + getTestResourceLocator( + Map.of( + "@projects/theme-name/modules/header.html", + "{% import '@projects/theme-name/utils/helpers.html' as helpers %}{{ helpers.render_header() }}", + "@projects/theme-name/utils/helpers.html", + "{% macro render_header() %}HEADER{% endmacro %}" + ) + ) + ); + + interpreter + .getContext() + .getCurrentPathStack() + .push("@projects/theme-name/modules/header.html", 1, 0); + String result = interpreter.render( + interpreter.getResource("@projects/theme-name/modules/header.html") + ); + + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(result.trim()).isEqualTo("HEADER"); + } + + @Test + public void itResolvesHubspotAbsolutePaths() throws Exception { + jinjava.setResourceLocator( + getTestResourceLocator( + Map.of( + "@hubspot/common/macros.html", + "{% import '@hubspot/common/utils.html' as utils %}{{ utils.common_macro() }}", + "@hubspot/common/utils.html", + "{% macro common_macro() %}COMMON{% endmacro %}" + ) + ) + ); + + interpreter + .getContext() + .getCurrentPathStack() + .push("@hubspot/common/macros.html", 1, 0); + String result = interpreter.render( + interpreter.getResource("@hubspot/common/macros.html") + ); + + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(result.trim()).isEqualTo("COMMON"); + } + private String fixture(String name) { try { return interpreter.renderFlat( diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/ResourceLocatorTestHelper.java b/src/test/java/com/hubspot/jinjava/lib/tag/ResourceLocatorTestHelper.java new file mode 100644 index 000000000..ced8d71b7 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/tag/ResourceLocatorTestHelper.java @@ -0,0 +1,38 @@ +package com.hubspot.jinjava.lib.tag; + +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.loader.LocationResolver; +import com.hubspot.jinjava.loader.RelativePathResolver; +import com.hubspot.jinjava.loader.ResourceLocator; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.Optional; + +public class ResourceLocatorTestHelper { + + public static ResourceLocator getTestResourceLocator(Map templates) { + return new ResourceLocator() { + private final RelativePathResolver relativePathResolver = + new RelativePathResolver(); + + @Override + public String getString( + String fullName, + Charset encoding, + JinjavaInterpreter interpreter + ) throws IOException { + String template = templates.get(fullName); + if (template == null) { + throw new IOException("Template not found: " + fullName); + } + return template; + } + + @Override + public Optional getLocationResolver() { + return Optional.of(relativePathResolver); + } + }; + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java index 54d288f1d..460869295 100644 --- a/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/tag/eager/EagerFromTagTest.java @@ -1,5 +1,6 @@ package com.hubspot.jinjava.lib.tag.eager; +import static com.hubspot.jinjava.lib.tag.ResourceLocatorTestHelper.getTestResourceLocator; import static org.assertj.core.api.Assertions.assertThat; import com.google.common.io.Resources; @@ -16,6 +17,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.After; import org.junit.Before; @@ -28,7 +30,8 @@ public class EagerFromTagTest extends FromTagTest { public void eagerSetup() { jinjava.setResourceLocator( new ResourceLocator() { - private RelativePathResolver relativePathResolver = new RelativePathResolver(); + private final RelativePathResolver relativePathResolver = + new RelativePathResolver(); @Override public String getString( @@ -123,4 +126,26 @@ public void itReconstructsCurrentPath() { @Ignore @Override public void itDefersImport() {} + + @Test + public void itResolvesNestedRelativeImportsInEagerMode() throws Exception { + jinjava.setResourceLocator( + getTestResourceLocator( + Map.of( + "root.jinja", + "{% from 'sub/nested.jinja' import test_macro %}{{ test_macro() }}", + "sub/nested.jinja", + "{% from '../helper.jinja' import helper %}{% macro test_macro() %}{{ helper() }}{% endmacro %}", + "helper.jinja", + "{% macro helper() %}HELPER{% endmacro %}" + ) + ) + ); + + interpreter.getContext().getCurrentPathStack().push("root.jinja", 1, 0); + String result = interpreter.render(interpreter.getResource("root.jinja")); + + assertThat(interpreter.getErrors()).isEmpty(); + assertThat(result).contains("HELPER"); + } }