diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java index defef913e..2ce2afa4e 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilter.java @@ -8,6 +8,8 @@ import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.lib.fn.Functions; import com.hubspot.jinjava.objects.SafeString; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.Map; import java.util.Objects; import org.apache.commons.lang3.BooleanUtils; @@ -52,6 +54,7 @@ public class TruncateHtmlFilter implements AdvancedFilter { private static final int DEFAULT_TRUNCATE_LENGTH = 255; private static final String DEFAULT_END = "..."; + private static final String DELETE_MARKER_CLASS = "__deleteme"; private static final String LENGTH_KEY = "length"; private static final String END_KEY = "end"; private static final String BREAKWORD_KEY = "breakword"; @@ -148,7 +151,7 @@ public Object filter(Object var, JinjavaInterpreter interpreter, String... args) killwords ); dom.select("body").traverse(visitor); - dom.select(".__deleteme").remove(); + dom.select("." + DELETE_MARKER_CLASS).remove(); return dom.select("body").html(); } @@ -162,6 +165,8 @@ private static class ContentTruncatingNodeVisitor implements NodeVisitor { private int textLen; private final String ending; private final boolean killwords; + private final Deque keptVoidElementsAtEntry = new ArrayDeque<>(); + private int keptVoidElements; ContentTruncatingNodeVisitor(int maxTextLen, String ending, boolean killwords) { this.maxTextLen = maxTextLen; @@ -188,17 +193,33 @@ public void head(Node node, int depth) { } else { textLen += textContent.length(); } + } else if (node instanceof Element el) { + keptVoidElementsAtEntry.push(keptVoidElements); + if (isVoidElement(el)) { + if (textLen >= maxTextLen) { + el.addClass(DELETE_MARKER_CLASS); + } else { + keptVoidElements++; + } + } } } @Override public void tail(Node node, int depth) { - if (node instanceof Element) { - Element el = (Element) node; - if (StringUtils.isBlank(el.text())) { - el.addClass("__deleteme"); + if (node instanceof Element el) { + boolean containsKeptVoidElement = + keptVoidElements > keptVoidElementsAtEntry.pop(); + if ( + !isVoidElement(el) && !containsKeptVoidElement && StringUtils.isBlank(el.text()) + ) { + el.addClass(DELETE_MARKER_CLASS); } } } + + private static boolean isVoidElement(Element el) { + return el.tag().isEmpty(); + } } } diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java index 963bc2e85..55bf267de 100644 --- a/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java +++ b/src/test/java/com/hubspot/jinjava/lib/filter/TruncateHtmlFilterTest.java @@ -78,6 +78,85 @@ public void itTakesKwargs() { ); } + @Test + public void itPreservesVoidElementsWhenContentIsUnderTheLimit() { + String result = (String) filter.filter( + "

Hey,
Great to hear!

", + interpreter, + "255" + ); + assertThat(result) + .isEqualTo("

Hey,\n
\n Great to hear!

\n

"); + } + + @Test + public void itPreservesVoidElementsBeforeTheTruncationPoint() { + String result = (String) filter.filter( + "

Hello
world, this is long text

", + interpreter, + "10", + "...", + "true" + ); + assertThat(result).isEqualTo("

Hello\n
\n world...

"); + } + + @Test + public void itRemovesVoidElementsAfterTheTruncationPoint() { + String result = (String) filter.filter( + "

Hello world

more
text

", + interpreter, + "5", + "...", + "true" + ); + assertThat(result).isEqualTo("

Hello...

"); + } + + @Test + public void itPreservesSelfClosingBrTagsBeforeTheTruncationPoint() { + String result = (String) filter.filter( + "

Hey,
Great to hear!

", + interpreter, + "255" + ); + assertThat(result).isEqualTo("

Hey,\n
\n Great to hear!

"); + } + + @Test + public void itRemovesSelfClosingBrTagsAfterTheTruncationPoint() { + String result = (String) filter.filter( + "

Hello world
more text


", + interpreter, + "5", + "...", + "true" + ); + assertThat(result).isEqualTo("

Hello...

"); + } + + @Test + public void itPreservesVoidElementsWithExplicitClosingTagsBeforeTheTruncationPoint() { + String result = (String) filter.filter( + "

Great to hear!

", + interpreter, + "255" + ); + assertThat(result).isEqualTo("

Great to hear!

"); + } + + @Test + public void itRemovesVoidElementsWithExplicitClosingTagsAfterTheTruncationPoint() { + String result = (String) filter.filter( + "

Hello world

", + interpreter, + "5", + "...", + "true" + ); + assertThat(result).isEqualTo("

Hello...

"); + } + private static String fixture(String name) { try { return Resources.toString(Resources.getResource(name), StandardCharsets.UTF_8);