diff --git a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java index d58483865..475355a16 100644 --- a/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java +++ b/src/main/java/com/hubspot/jinjava/interpret/JinjavaInterpreter.java @@ -50,6 +50,7 @@ import com.hubspot.jinjava.tree.output.OutputNode; import com.hubspot.jinjava.tree.output.RenderedOutputNode; import com.hubspot.jinjava.util.EagerReconstructionUtils; +import com.hubspot.jinjava.util.RenderLimitUtils; import com.hubspot.jinjava.util.Variable; import com.hubspot.jinjava.util.WhitespaceUtils; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -259,7 +260,11 @@ public String renderFlat(String template) { * @return rendered result */ public String render(String template) { - return render(parse(template), true); + return render(template, config.getMaxOutputSize()); + } + + public String render(String template, long renderLimit) { + return render(parse(template), true, renderLimit); } /** @@ -270,7 +275,19 @@ public String render(String template) { * @return rendered result */ public String render(Node root) { - return render(root, true); + return render(root, true, config.getMaxOutputSize()); + } + + /** + * Render the given root node with an option to process extend parents. + * Equivalent to render(root, processExtendRoots). + * @param root + * node to render + * @param processExtendRoots + * @return + */ + public String render(Node root, boolean processExtendRoots) { + return render(root, processExtendRoots, config.getMaxOutputSize()); } /** @@ -280,11 +297,14 @@ public String render(Node root) { * node to render * @param processExtendRoots * if true, also render all extend parents + * @param renderLimit + * the number of characters the result may contain * @return rendered result */ - public String render(Node root, boolean processExtendRoots) { - OutputList output = new OutputList(config.getMaxOutputSize()); - + private String render(Node root, boolean processExtendRoots, long renderLimit) { + OutputList output = new OutputList( + RenderLimitUtils.clampProvidedRenderLimitToConfig(renderLimit, config) + ); for (Node node : root.getChildren()) { lineNumber = node.getLineNumber(); position = node.getStartPosition(); @@ -340,8 +360,8 @@ public String render(Node root, boolean processExtendRoots) { return output.getValue(); } } - StringBuilder ignoredOutput = new StringBuilder(); + StringBuilder ignoredOutput = new StringBuilder(); // render all extend parents, keeping the last as the root output if (processExtendRoots) { Set extendPaths = new HashSet<>(); @@ -406,6 +426,7 @@ public String render(Node root, boolean processExtendRoots) { } resolveBlockStubs(output); + if (ignoredOutput.length() > 0) { return ( EagerReconstructionUtils.labelWithNotes( @@ -421,7 +442,6 @@ public String render(Node root, boolean processExtendRoots) { output.getValue() ); } - return output.getValue(); } diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilter.java new file mode 100644 index 000000000..f15315bd8 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilter.java @@ -0,0 +1,26 @@ +package com.hubspot.jinjava.lib.filter; + +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import java.util.Objects; +import org.jsoup.Jsoup; + +@JinjavaDoc( + value = "Closes open HTML tags in a string", + input = @JinjavaParam(value = "s", desc = "String to close", required = true), + snippets = { @JinjavaSnippet(code = "{{ \"

Hello, world\"|closehtml }}") } +) +public class CloseHtmlFilter implements Filter { + + @Override + public String getName() { + return "closehtml"; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + return Jsoup.parseBodyFragment(Objects.toString(var)).body().html(); + } +} diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java index c257815a3..4cd6763cb 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/RenderFilter.java @@ -1,10 +1,12 @@ package com.hubspot.jinjava.lib.filter; +import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.doc.annotations.JinjavaDoc; import com.hubspot.jinjava.doc.annotations.JinjavaParam; import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import java.util.Objects; +import org.apache.commons.lang3.math.NumberUtils; @JinjavaDoc( value = "Renders a template string early to be used by other filters and functions", @@ -24,6 +26,16 @@ public String getName() { @Override public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + if (args.length > 0) { + String firstArg = args[0]; + return interpreter.render( + Objects.toString(var), + NumberUtils.toLong( + firstArg, + JinjavaConfig.newBuilder().build().getMaxOutputSize() + ) + ); + } return interpreter.render(Objects.toString(var)); } } diff --git a/src/main/java/com/hubspot/jinjava/util/RenderLimitUtils.java b/src/main/java/com/hubspot/jinjava/util/RenderLimitUtils.java new file mode 100644 index 000000000..61f2de594 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/util/RenderLimitUtils.java @@ -0,0 +1,23 @@ +package com.hubspot.jinjava.util; + +import com.hubspot.jinjava.JinjavaConfig; + +public class RenderLimitUtils { + + public static long clampProvidedRenderLimitToConfig( + long providedLimit, + JinjavaConfig jinjavaConfig + ) { + long configMaxOutput = jinjavaConfig.getMaxOutputSize(); + + if (configMaxOutput <= 0) { + return providedLimit; + } + + if (providedLimit <= 0) { + return configMaxOutput; + } + + return Math.min(providedLimit, configMaxOutput); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilterTest.java new file mode 100644 index 000000000..4b7f3be51 --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/CloseHtmlFilterTest.java @@ -0,0 +1,35 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseInterpretingTest; +import org.junit.Before; +import org.junit.Test; + +public class CloseHtmlFilterTest extends BaseInterpretingTest { + CloseHtmlFilter f; + + @Before + public void setup() { + f = new CloseHtmlFilter(); + } + + @Test + public void itClosesTags() { + String openTags = "

Hello, world!"; + assertThat(f.filter(openTags, interpreter)).isEqualTo("

Hello, world!

"); + } + + @Test + public void itIgnoresClosedTags() { + String openTags = "

Hello, world!

"; + assertThat(f.filter(openTags, interpreter)).isEqualTo("

Hello, world!

"); + } + + @Test + public void itClosesMultipleTags() { + String openTags = "

Hello, world!"; + assertThat(f.filter(openTags, interpreter)) + .isEqualTo("

Hello, world!

"); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java index 04e238b7a..156720a10 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/RenderFilterTest.java @@ -20,4 +20,42 @@ public void itRendersObject() { assertThat(filter.filter(stringToRender, interpreter)).isEqualTo("world"); } + + @Test + public void itRendersObjectWithinLimit() { + String stringToRender = "{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "5")).isEqualTo("world"); + } + + @Test + public void itDoesNotRenderObjectOverLimit() { + String stringToRender = "{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "4")).isEqualTo(""); + } + + @Test + public void itRendersPartialObjectOverLimit() { + String stringToRender = "Hello{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "7")).isEqualTo("Hello"); + } + + @Test + public void itCountsHtmlTags() { + String stringToRender = "

Hello

{% if null %}Hello{% else %}world{% endif %}"; + + assertThat(filter.filter(stringToRender, interpreter, "15")) + .isEqualTo("

Hello

"); + } + + @Test + public void itDoesNotAlwaysCompleteHtmlTags() { + String stringToRender = + "

Hello, {% if null %}world{% else %}world!{% endif %}

"; + + assertThat(filter.filter(stringToRender, interpreter, "17")) + .isEqualTo("

Hello, world!"); + } } diff --git a/src/test/java/com/hubspot/jinjava/util/RenderLimitUtilsTest.java b/src/test/java/com/hubspot/jinjava/util/RenderLimitUtilsTest.java new file mode 100644 index 000000000..898e5a40d --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/util/RenderLimitUtilsTest.java @@ -0,0 +1,46 @@ +package com.hubspot.jinjava.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.JinjavaConfig; +import org.junit.Test; + +public class RenderLimitUtilsTest { + + @Test + public void itPicksLowerLimitWhenConfigIsSet() { + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(100, configWithOutputSize(10)) + ) + .isEqualTo(10); + } + + @Test + public void itKeepsConfigLimitWhenConfigSetAndUnlimitedProvided() { + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(0, configWithOutputSize(10)) + ) + .isEqualTo(10); + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(-10, configWithOutputSize(10)) + ) + .isEqualTo(10); + } + + @Test + public void itUsesProvidedLimitWhenConfigIsUnlimited() { + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(10, configWithOutputSize(0)) + ) + .isEqualTo(10); + + assertThat( + RenderLimitUtils.clampProvidedRenderLimitToConfig(10, configWithOutputSize(-10)) + ) + .isEqualTo(10); + } + + private JinjavaConfig configWithOutputSize(long size) { + return JinjavaConfig.newBuilder().withMaxOutputSize(size).build(); + } +}