You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2019/01/16 19:24:17 UTC
[freemarker] branch 3 updated: Forward ported from 2.3-gae: Added
?truncate built-in and related setting
This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/3 by this push:
new 69c2f71 Forward ported from 2.3-gae: Added ?truncate built-in and related setting
69c2f71 is described below
commit 69c2f71271a54fa74f2e2a95f3b772ae2f567e87
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Jan 16 20:24:07 2019 +0100
Forward ported from 2.3-gae: Added ?truncate built-in and related setting
---
...tableParsingAndProcessingConfigurationTest.java | 62 ++
.../freemarker/core/TemplateConfigurationTest.java | 2 +
.../freemarker/core/TruncateBuiltInTest.java | 135 ++++
.../impl/DefaultTruncateBuiltinAlgorithmTest.java | 672 ++++++++++++++++++
.../org/apache/freemarker/core/ASTExpBuiltIn.java | 8 +-
.../freemarker/core/BuiltInsForStringsBasic.java | 141 ++++
.../org/apache/freemarker/core/Configuration.java | 25 +-
.../org/apache/freemarker/core/Environment.java | 6 +
.../core/MutableProcessingConfiguration.java | 80 ++-
.../freemarker/core/ProcessingConfiguration.java | 20 +
.../java/org/apache/freemarker/core/Template.java | 11 +
.../freemarker/core/TemplateConfiguration.java | 25 +
.../core/_ObjectBuilderSettingEvaluator.java | 3 +
.../pluggablebuiltin/TruncateBuiltinAlgorithm.java | 126 ++++
.../impl/DefaultTruncateBuiltinAlgorithm.java | 755 +++++++++++++++++++++
15 files changed, 2067 insertions(+), 4 deletions(-)
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableParsingAndProcessingConfigurationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableParsingAndProcessingConfigurationTest.java
index 56f8581..394e060 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableParsingAndProcessingConfigurationTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/MutableParsingAndProcessingConfigurationTest.java
@@ -32,6 +32,7 @@ import java.util.TimeZone;
import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
import org.apache.freemarker.core.util._DateUtils;
import org.apache.freemarker.core.util._NullArgumentException;
import org.junit.Test;
@@ -278,6 +279,67 @@ public class MutableParsingAndProcessingConfigurationTest {
assertTrue(cfgB.isTimeZoneSet());
}
+
+ public void testTruncateBuiltinAlgorithm() throws TemplateException {
+ Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+ assertSame(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE, cfgB.getTruncateBuiltinAlgorithm());
+
+ cfgB.setSetting("truncateBuiltinAlgorithm", "unicodE");
+ assertSame(DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE, cfgB.getTruncateBuiltinAlgorithm());
+
+ cfgB.setSetting("truncate_builtin_algorithm", "ASCII");
+ assertSame(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE, cfgB.getTruncateBuiltinAlgorithm());
+
+ {
+ cfgB.setSetting("truncate_builtin_algorithm",
+ "DefaultTruncateBuiltinAlgorithm('...', false)");
+ DefaultTruncateBuiltinAlgorithm alg =
+ (DefaultTruncateBuiltinAlgorithm) cfgB.getTruncateBuiltinAlgorithm();
+ assertEquals("...", alg.getDefaultTerminator());
+ assertFalse(alg.getAddSpaceAtWordBoundary());
+ assertEquals(3, alg.getDefaultTerminatorLength());
+ assertNull(alg.getDefaultMTerminator());
+ assertNull(alg.getDefaultMTerminatorLength());
+ assertEquals(DefaultTruncateBuiltinAlgorithm.DEFAULT_WORD_BOUNDARY_MIN_LENGTH,
+ alg.getWordBoundaryMinLength());
+ }
+
+ {
+ cfgB.setSetting("truncate_builtin_algorithm",
+ "DefaultTruncateBuiltinAlgorithm(" +
+ "'...', " +
+ "markup(HTMLOutputFormat(), '<span class=trunc>...</span>'), " +
+ "true)");
+ DefaultTruncateBuiltinAlgorithm alg =
+ (DefaultTruncateBuiltinAlgorithm) cfgB.getTruncateBuiltinAlgorithm();
+ assertEquals("...", alg.getDefaultTerminator());
+ assertTrue(alg.getAddSpaceAtWordBoundary());
+ assertEquals(3, alg.getDefaultTerminatorLength());
+ assertEquals("markupOutput(format=HTML, markup=<span class=trunc>...</span>)",
+ alg.getDefaultMTerminator().toString());
+ assertEquals(Integer.valueOf(3), alg.getDefaultMTerminatorLength());
+ assertEquals(DefaultTruncateBuiltinAlgorithm.DEFAULT_WORD_BOUNDARY_MIN_LENGTH,
+ alg.getWordBoundaryMinLength());
+ }
+
+ {
+ cfgB.setSetting("truncate_builtin_algorithm",
+ "DefaultTruncateBuiltinAlgorithm(" +
+ "DefaultTruncateBuiltinAlgorithm.STANDARD_ASCII_TERMINATOR, null, null, " +
+ "DefaultTruncateBuiltinAlgorithm.STANDARD_M_TERMINATOR, null, null, " +
+ "true, 0.5)");
+ DefaultTruncateBuiltinAlgorithm alg =
+ (DefaultTruncateBuiltinAlgorithm) cfgB.getTruncateBuiltinAlgorithm();
+ assertEquals(DefaultTruncateBuiltinAlgorithm.STANDARD_ASCII_TERMINATOR, alg.getDefaultTerminator());
+ assertTrue(alg.getAddSpaceAtWordBoundary());
+ assertEquals(5, alg.getDefaultTerminatorLength());
+ assertEquals(DefaultTruncateBuiltinAlgorithm.STANDARD_M_TERMINATOR.toString(),
+ alg.getDefaultMTerminator().toString());
+ assertEquals(Integer.valueOf(3), alg.getDefaultMTerminatorLength());
+ assertEquals(0.5, alg.getWordBoundaryMinLength(), 0);
+ }
+ }
+
// ------------
@Test
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
index 145e347..ea6477b 100644
--- a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java
@@ -50,6 +50,7 @@ import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
import org.apache.freemarker.core.templateresolver.FileExtensionMatcher;
import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
@@ -188,6 +189,7 @@ public class TemplateConfigurationTest {
ImmutableMap.of("dummy", HexTemplateNumberFormatFactory.INSTANCE));
SETTING_ASSIGNMENTS.put("customDateFormats",
ImmutableMap.of("dummy", EpochMillisTemplateDateFormatFactory.INSTANCE));
+ SETTING_ASSIGNMENTS.put("truncateBuiltinAlgorithm", DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE);
// Parser-only settings:
SETTING_ASSIGNMENTS.put("templateLanguage", UnparsedTemplateLanguage.F3UU);
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TruncateBuiltInTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TruncateBuiltInTest.java
new file mode 100644
index 0000000..33ddd3f
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TruncateBuiltInTest.java
@@ -0,0 +1,135 @@
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TruncateBuiltInTest extends TemplateTest {
+
+ private static final String M_TERM_SRC = "<span class=trunc>&hellips;</span>";
+
+ @Override
+ protected Configuration createDefaultConfiguration() throws Exception {
+ return createConfigurationBuilder().build();
+ }
+
+ private TestConfigurationBuilder createConfigurationBuilder() {
+ return new TestConfigurationBuilder().outputFormat(HTMLOutputFormat.INSTANCE);
+ }
+
+ @Before
+ public void setup() throws TemplateException {
+ addToDataModel("t", "Some text for truncation testing.");
+ addToDataModel("u", "CaNotBeBrokenAnywhere");
+ addToDataModel("mTerm", HTMLOutputFormat.INSTANCE.fromMarkup(M_TERM_SRC));
+ }
+
+ @Test
+ public void testTruncate() throws IOException, TemplateException {
+ assertOutput("${t?truncate(20)}", "Some text for [...]");
+ assertOutput("${t?truncate(20, '|')}", "Some text for |");
+ assertOutput("${t?truncate(20, '|', 7)}", "Some text |");
+
+ assertOutput("${u?truncate(20)}", "CaNotBeBrokenAn[...]");
+ assertOutput("${u?truncate(20, '|')}", "CaNotBeBrokenAnywhe|");
+ assertOutput("${u?truncate(20, '|', 3)}", "CaNotBeBrokenAnyw|");
+
+ assertOutput("${t?truncate(20)?isMarkupOutput?c}", "false");
+
+ // Edge cases that are still allowed:
+ assertOutput("${t?truncate(0)}", "[...]");
+ assertOutput("${u?truncate(3, '', 0)}", "CaN");
+
+ // Disallowed:
+ assertErrorContains("${t?truncate(200, mTerm)}", "2nd", "string", "markupOutput");
+ assertErrorContains("${t?truncate(-1)}", "1st", "at least 0");
+ assertErrorContains("${t?truncate(200, 'x', -1)}", "3rd", "at least 0");
+ }
+
+ @Test
+ public void testTruncateM() throws IOException, TemplateException {
+ assertOutput("${t?truncateM(15)}", "Some text <span class='truncateTerminator'>[…]</span>"); // String arg allowed...
+ assertOutput("${t?truncateM(15, mTerm)}", "Some text for " + M_TERM_SRC);
+ assertOutput("${t?truncateM(15, mTerm)}", "Some text for " + M_TERM_SRC);
+ assertOutput("${t?truncateM(15, mTerm, 3)}", "Some text " + M_TERM_SRC);
+
+ assertOutput("${u?truncateM(20, mTerm)}", "CaNotBeBrokenAnywhe" + M_TERM_SRC);
+ assertOutput("${u?truncateM(20, mTerm, 3)}", "CaNotBeBrokenAnyw" + M_TERM_SRC);
+
+ assertOutput("${t?truncateM(15, '|')}", "Some text for |"); // String arg allowed...
+ assertOutput("${t?truncateM(15, '|')?isMarkupOutput?c}", "false"); // ... and results in string.
+ assertOutput("${t?truncateM(15, mTerm)?isMarkupOutput?c}", "true");
+ }
+
+ @Test
+ public void testTruncateC() throws IOException, TemplateException {
+ assertOutput("${t?truncateC(20)}", "Some text for t[...]");
+ assertOutput("${t?truncateC(20)}", "Some text for t[...]");
+ assertOutput("${t?truncateC(20, '|')}", "Some text for trunc|");
+ assertOutput("${t?truncateC(20, '|', 0)}", "Some text for trunca|");
+
+ assertErrorContains("${t?truncateC(200, mTerm)}", "2nd", "string", "markupOutput");
+
+ assertOutput("${t?truncateC(20)?isMarkupOutput?c}", "false");
+ }
+
+ @Test
+ public void testTruncateCM() throws IOException, TemplateException {
+ assertOutput("${t?truncateCM(20, mTerm)}", "Some text for trunc" + M_TERM_SRC);
+ assertOutput("${t?truncateCM(20, mTerm, 3)}", "Some text for tru" + M_TERM_SRC);
+
+ assertOutput("${t?truncateCM(20)?isMarkupOutput?c}", "true");
+ assertOutput("${t?truncateCM(20, '|')?isMarkupOutput?c}", "false");
+ assertOutput("${t?truncateCM(20, mTerm)?isMarkupOutput?c}", "true");
+ }
+
+ @Test
+ public void testTruncateW() throws IOException, TemplateException {
+ assertOutput("${t?truncateW(20)}", "Some text for [...]");
+ assertOutput("${u?truncateW(20)}", "[...]"); // Proof of no fallback to C
+
+ assertErrorContains("${t?truncateW(200, mTerm)}", "2nd", "string", "markupOutput");
+
+ assertOutput("${t?truncateW(20)?isMarkupOutput?c}", "false");
+ assertOutput("${t?truncateW(20, '|')?isMarkupOutput?c}", "false");
+ }
+
+ @Test
+ public void testTruncateWM() throws IOException, TemplateException {
+ assertOutput("${t?truncateWM(15, mTerm)}", "Some text for " + M_TERM_SRC);
+ assertOutput("${t?truncateWM(15, mTerm)}", "Some text for " + M_TERM_SRC);
+ assertOutput("${t?truncateWM(15, mTerm, 3)}", "Some text " + M_TERM_SRC);
+
+ assertOutput("${u?truncateWM(20, mTerm)}", M_TERM_SRC); // Proof of no fallback to C
+
+ assertOutput("${t?truncateCM(20)?isMarkupOutput?c}", "true");
+ assertOutput("${t?truncateCM(20, '|')?isMarkupOutput?c}", "false");
+ assertOutput("${t?truncateCM(20, mTerm)?isMarkupOutput?c}", "true");
+ }
+
+ @Test
+ public void testSettingHasEffect() throws IOException, TemplateException {
+ assertOutput("${t?truncate(20)}", "Some text for [...]");
+ assertOutput("${t?truncateC(20)}", "Some text for t[...]");
+ setConfiguration(createConfigurationBuilder().truncateBuiltinAlgorithm(DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE).build());
+ assertOutput("${t?truncate(20)}", "Some text for [\u2026]");
+ assertOutput("${t?truncateC(20)}", "Some text for tru[\u2026]");
+ }
+
+ @Test
+ public void testDifferentMarkupSeparatorSetting() throws IOException, TemplateException {
+ assertOutput("${t?truncate(20)}", "Some text for [...]");
+ assertOutput("${t?truncateM(20)}", "Some text for <span class='truncateTerminator'>[…]</span>");
+ setConfiguration(createConfigurationBuilder().truncateBuiltinAlgorithm(
+ new DefaultTruncateBuiltinAlgorithm("|...", HTMLOutputFormat.INSTANCE.fromMarkup(M_TERM_SRC), true))
+ .build());
+ assertOutput("${t?truncate(20)}", "Some text for |...");
+ assertOutput("${t?truncateM(20)}", "Some text for " + M_TERM_SRC);
+ }
+
+}
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/pluggablebuiltin/impl/DefaultTruncateBuiltinAlgorithmTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/pluggablebuiltin/impl/DefaultTruncateBuiltinAlgorithmTest.java
new file mode 100644
index 0000000..f567825
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/pluggablebuiltin/impl/DefaultTruncateBuiltinAlgorithmTest.java
@@ -0,0 +1,672 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.pluggablebuiltin.impl;
+
+import static org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleString;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.TemplateHTMLOutputModel;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
+import org.junit.Test;
+
+public class DefaultTruncateBuiltinAlgorithmTest {
+
+ private static final DefaultTruncateBuiltinAlgorithm EMPTY_TERMINATOR_INSTANCE =
+ new DefaultTruncateBuiltinAlgorithm("", false);
+ private static final DefaultTruncateBuiltinAlgorithm DOTS_INSTANCE =
+ new DefaultTruncateBuiltinAlgorithm("...", true);
+ private static final DefaultTruncateBuiltinAlgorithm DOTS_NO_W_SPACE_INSTANCE =
+ new DefaultTruncateBuiltinAlgorithm("...", false);
+ private static final DefaultTruncateBuiltinAlgorithm ASCII_NO_W_SPACE_INSTANCE =
+ new DefaultTruncateBuiltinAlgorithm("[...]", false);
+ private static final DefaultTruncateBuiltinAlgorithm M_TERM_INSTANCE;
+
+ static {
+ try {
+ M_TERM_INSTANCE = new DefaultTruncateBuiltinAlgorithm(
+ "...", null, true,
+ HTMLOutputFormat.INSTANCE.fromMarkup("<r>...</r>"), null, true,
+ true, 0.75);
+ } catch (TemplateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testConstructorIllegalArguments() throws TemplateException {
+ try {
+ new DefaultTruncateBuiltinAlgorithm(
+ null, null, true,
+ HTMLOutputFormat.INSTANCE.fromMarkup("<r>...</r>"), null, true,
+ true, 0.75);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("defaultTerminator"));
+ }
+ }
+
+ @Test
+ public void testTruncateIllegalArguments() throws TemplateException {
+ Environment env = createEnvironment();
+
+ ASCII_INSTANCE.truncate("", 0, new SimpleString("."), 1, env);
+
+ try {
+ ASCII_INSTANCE.truncate("", -1, new SimpleString("."), 1, env);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("maxLength"));
+ }
+
+ try {
+ ASCII_INSTANCE.truncateM("sss", 2, new SimpleNumber(1), 1, env);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("SimpleNumber"));
+ }
+
+ try {
+ ASCII_INSTANCE.truncate("sss", 2, new SimpleString("."), -1, env);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("terminatorLength"));
+ }
+ }
+
+ private Environment createEnvironment() {
+ try {
+ return new Template("", "", new Configuration.Builder(Configuration.VERSION_3_0_0).build())
+ .createProcessingEnvironment(null,null);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Test
+ public void testCSimple() {
+ assertC(ASCII_INSTANCE, "12345678", 9, "12345678");
+ assertC(ASCII_INSTANCE, "12345678", 8, "12345678");
+ assertC(ASCII_INSTANCE, "12345678", 7, "12[...]");
+ assertC(ASCII_INSTANCE, "12345678", 6, "1[...]");
+ for (int maxLength = 5; maxLength >= 0; maxLength--) {
+ assertC(ASCII_INSTANCE, "12345678", maxLength, "[...]");
+ }
+
+ assertC(UNICODE_INSTANCE, "12345678", 9, "12345678");
+ assertC(UNICODE_INSTANCE, "12345678", 8, "12345678");
+ assertC(UNICODE_INSTANCE, "12345678", 7, "1234[\u2026]");
+ assertC(UNICODE_INSTANCE, "12345678", 6, "123[\u2026]");
+ assertC(UNICODE_INSTANCE, "12345678", 5, "12[\u2026]");
+ assertC(UNICODE_INSTANCE, "12345678", 4, "1[\u2026]");
+ for (int maxLength = 3; maxLength >= 0; maxLength--) {
+ assertC(UNICODE_INSTANCE, "12345678", maxLength, "[\u2026]");
+ }
+
+ assertC(EMPTY_TERMINATOR_INSTANCE, "12345678", 9, "12345678");
+ for (int length = 8; length >= 0; length--) {
+ assertC(EMPTY_TERMINATOR_INSTANCE, "12345678", length, "12345678".substring(0, length));
+ }
+ }
+
+ @Test
+ public void testCSpaceAndDot() {
+ assertC(ASCII_INSTANCE, "123456 ", 9, "123456 ");
+ assertC(ASCII_INSTANCE, "123456 ", 8, "123456 ");
+ assertC(ASCII_INSTANCE, "123456 ", 7, "12[...]");
+ assertC(ASCII_INSTANCE, "123456 ", 6, "1[...]");
+ assertC(ASCII_INSTANCE, "123456 ", 5, "[...]");
+ assertC(ASCII_INSTANCE, "123456 ", 4, "[...]");
+
+ assertC(ASCII_INSTANCE, "1 345 ", 13, "1 345 ");
+ assertC(ASCII_INSTANCE, "1 345 ", 12, "1 345 [...]"); // Not "1 345 [...]"
+ assertC(ASCII_INSTANCE, "1 345 ", 11, "1 345 [...]");
+ assertC(ASCII_INSTANCE, "1 345 ", 10, "1 34[...]"); // Not "12345[...]"
+ assertC(ASCII_INSTANCE, "1 345 ", 9, "1 34[...]");
+ assertC(ASCII_INSTANCE, "1 345 ", 8, "1 3[...]");
+ assertC(ASCII_INSTANCE, "1 345 ", 7, "1 [...]");
+ assertC(ASCII_INSTANCE, "1 345 ", 6, "[...]"); // Not "1[...]"
+ assertC(ASCII_INSTANCE, "1 345 ", 5, "[...]");
+ assertC(ASCII_INSTANCE, "1 345 ", 4, "[...]");
+
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 13, "1 345 ");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 12, "1 345[...]"); // Differs!
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 11, "1 345[...]"); // Differs!
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 10, "1 345[...]"); // Differs!
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 9, "1 34[...]");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 8, "1 3[...]");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 7, "1[...]"); // Differs!
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 6, "1[...]"); // Differs!
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 5, "[...]");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 345 ", 4, "[...]");
+
+ assertC(ASCII_INSTANCE, "1 4567890", 9, "1 4[...]");
+ assertC(ASCII_INSTANCE, "1 4567890", 8, "1 [...]");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 4567890", 9, "1 4[...]");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, "1 4567890", 8, "1[...]");
+
+ assertC(ASCII_INSTANCE, " 3456789", 9, " 3456789");
+ assertC(ASCII_INSTANCE, " 3456789", 8, " 3[...]");
+ assertC(ASCII_INSTANCE, " 3456789", 7, "[...]");
+ assertC(ASCII_INSTANCE, " 3456789", 6, "[...]");
+
+ assertC(ASCII_NO_W_SPACE_INSTANCE, " 3456789", 8, " 3[...]");
+ assertC(ASCII_NO_W_SPACE_INSTANCE, " 3456789", 7, "[...]");
+
+ // Dots aren't treated specially by default:
+ assertC(ASCII_INSTANCE, "1. 56...012345", 15, "1. 56...012345");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 14, "1. 56...[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 13, "1. 56..[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 12, "1. 56.[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 11, "1. 56[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 10, "1. 5[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 9, "1. [...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 8, "1. [...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 7, "1[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 6, "1[...]");
+ assertC(ASCII_INSTANCE, "1. 56...012345", 5, "[...]");
+
+ // Dots are treated specially here:
+ assertC(DOTS_INSTANCE, "1. 56...012345", 15, "1. 56...012345");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 14, "1. 56...01...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 13, "1. 56...0...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 12, "1. 56...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 11, "1. 56...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 10, "1. 56...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 9, "1. 56...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 8, "1. 5...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 7, "1. ...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 6, "1. ...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 5, "1...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 4, "1...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 3, "...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 2, "...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 1, "...");
+ assertC(DOTS_INSTANCE, "1. 56...012345", 0, "...");
+
+ assertC(DOTS_NO_W_SPACE_INSTANCE, "1. 56...012345", 8, "1. 5...");
+ assertC(DOTS_NO_W_SPACE_INSTANCE, "1. 56...012345", 7, "1...");
+ assertC(DOTS_NO_W_SPACE_INSTANCE, "1. 56...012345", 6, "1...");
+ assertC(DOTS_NO_W_SPACE_INSTANCE, "1. 56...012345", 5, "1...");
+ assertC(DOTS_NO_W_SPACE_INSTANCE, "1. 56...012345", 4, "1...");
+ assertC(DOTS_NO_W_SPACE_INSTANCE, "1. 56...012345", 3, "...");
+
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 6, "ab. cd");
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 5, "ab. c");
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 4, "ab.");
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 3, "ab.");
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 2, "ab");
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 1, "a");
+ assertC(EMPTY_TERMINATOR_INSTANCE, "ab. cd", 0, "");
+ }
+
+ @Test
+ public void testWSimple() {
+ assertW(ASCII_INSTANCE, "word1 word2 word3", 18, "word1 word2 word3");
+ assertW(ASCII_INSTANCE, "word1 word2 word3", 17, "word1 word2 word3");
+ assertW(ASCII_INSTANCE, "word1 word2 word3", 16, "word1 [...]");
+ assertW(ASCII_INSTANCE, "word1 word2 word3", 11, "word1 [...]");
+ for (int maxLength = 10; maxLength >= 0; maxLength--) {
+ assertW(ASCII_INSTANCE, "word1 word2 word3", maxLength, "[...]");
+ }
+
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", 18, "word1 word2 word3");
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", 17, "word1 word2 word3");
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", 16, "word1 word2 [\u2026]");
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", 15, "word1 word2 [\u2026]");
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", 14, "word1 [\u2026]");
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", 9, "word1 [\u2026]");
+ for (int maxLength = 8; maxLength >= 0; maxLength--) {
+ assertW(UNICODE_INSTANCE, "word1 word2 word3", maxLength, "[\u2026]");
+ }
+
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", 18, "word1 word2 word3");
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", 17, "word1 word2 word3");
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", 16, "word1 word2");
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", 11, "word1 word2");
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", 10, "word1");
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", 5, "word1");
+ for (int maxLength = 4; maxLength >= 0; maxLength--) {
+ assertW(EMPTY_TERMINATOR_INSTANCE, "word1 word2 word3", maxLength, "");
+ }
+ }
+
+ @Test
+ public void testWSpaceAndDot() {
+ assertW(DOTS_INSTANCE, " word1 word2 ", 16, " word1 word2 ");
+ assertW(DOTS_INSTANCE, " word1 word2 ", 15, " word1 ...");
+ assertW(DOTS_INSTANCE, " word1 word2 ", 11, " word1 ...");
+ for (int maxLength = 10; maxLength >= 0; maxLength--) {
+ assertW(DOTS_INSTANCE, " word1 word2 ", maxLength, "...");
+ }
+
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " word1 word2 ", 16, " word1 word2 ");
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " word1 word2 ", 15, " word1...");
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " word1 word2 ", 10, " word1...");
+ for (int maxLength = 9; maxLength >= 0; maxLength--) {
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " word1 word2 ", maxLength, "...");
+ }
+
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 23, " . . word1.. word2 ");
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 22, " . . word1.. ...");
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 16, " . . word1.. ...");
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 15, " . . ...");
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 8, " . . ...");
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 7, " . ...");
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", 6, " . ...");
+ for (int maxLength = 5; maxLength >= 0; maxLength--) {
+ assertW(DOTS_INSTANCE, " . . word1.. word2 ", maxLength, "...");
+ }
+
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " . . word1.. word2 ", 23, " . . word1.. word2 ");
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " . . word1.. word2 ", 22, " . . word1.. word2...");
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " . . word1.. word2 ", 21, " . . word1...");
+ for (int maxLength = 13; maxLength >= 0; maxLength--) {
+ assertW(DOTS_NO_W_SPACE_INSTANCE, " . . word1.. word2 ", maxLength, "...");
+ }
+ }
+
+ /**
+ * "Auto" means plain trunce(..) call, because the tested implementation chooses between CB and WB automatically.
+ */
+ @Test
+ public void testAuto() {
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 24, "1 234567 90ABCDEFGHIJKL");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 23, "1 234567 90ABCDEFGHIJKL");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 21, "1 234567 90ABCDE[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 20, "1 234567 90ABCD[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 19, "1 234567 90ABC[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 18, "1 234567 [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 17, "1 234567 [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 16, "1 234567 [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 15, "1 234567 [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 14, "1 234567 [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 13, "1 23456[...]"); // wb space
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJKL", 12, "1 23456[...]");
+
+ assertAuto(ASCII_INSTANCE, "1 234567 0ABCDEFGHIJKL", 22, "1 234567 0ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 9 ABCDEFGHIJKL", 22, "1 234567 9 ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90 BCDEFGHIJKL", 22, "1 234567 90 [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90A CDEFGHIJKL", 22, "1 234567 90A [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90AB DEFGHIJKL", 22, "1 234567 90AB [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABC EFGHIJKL", 22, "1 234567 90ABC [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCD FGHIJKL", 22, "1 234567 90ABCD [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDE GHIJKL", 22, "1 234567 90ABCDE [...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEF HIJKL", 22, "1 234567 90ABCDE[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFG IJKL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGH JKL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHI KL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJ L", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_INSTANCE, "1 234567 90ABCDEFGHIJK ", 22, "1 234567 90ABCDEF[...]");
+
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 0ABCDEFGHIJKL", 22, "1 234567 0ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 9 ABCDEFGHIJKL", 22, "1 234567 9 ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90 BCDEFGHIJKL", 22, "1 234567 90 BCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90A CDEFGHIJKL", 22, "1 234567 90A[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90AB DEFGHIJKL", 22, "1 234567 90AB[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABC EFGHIJKL", 22, "1 234567 90ABC[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCD FGHIJKL", 22, "1 234567 90ABCD[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDE GHIJKL", 22, "1 234567 90ABCDE[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDEF HIJKL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDEFG IJKL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDEFGH JKL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDEFGHI KL", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDEFGHIJ L", 22, "1 234567 90ABCDEF[...]");
+ assertAuto(ASCII_NO_W_SPACE_INSTANCE, "1 234567 90ABCDEFGHIJK ", 22, "1 234567 90ABCDEF[...]");
+
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 24, "12390ABCD.. . EFGHIJK .");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 23, "12390ABCD.. . ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 22, "12390ABCD.. . ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 21, "12390ABCD.. . ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 20, "12390ABCD.. . ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 19, "12390ABCD.. . ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 18, "12390ABCD.. . ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 17, "12390ABCD.. ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 16, "12390ABCD.. ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 15, "12390ABCD.. ...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 14, "12390ABCD...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 13, "12390ABCD...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 12, "12390ABCD...");
+ assertAuto(DOTS_INSTANCE, "12390ABCD.. . EFGHIJK .", 11, "12390ABC...");
+
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 27, "word0 word1. word2 w3 . ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 26, "word0 word1. word2 w3 ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 25, "word0 word1. word2 w3 ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 24, "word0 word1. word2 ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 22, "word0 word1. word2 ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 21, "word0 word1. ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 16, "word0 word1. ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 15, "word0 word1...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 14, "word0 word1...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 13, "word0 word...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 12, "word0 ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 9, "word0 ...");
+ assertAuto(DOTS_INSTANCE, "word0 word1. word2 w3 . . w4", 8, "word...");
+ }
+
+ @Test
+ public void testExtremeWordBoundaryMinLengths() {
+ assertC(ASCII_INSTANCE, "1 3456789", 8, "1 3[...]");
+ assertW(ASCII_INSTANCE, "1 3456789", 8, "1 [...]");
+ DefaultTruncateBuiltinAlgorithm wbMinLen1Algo = new DefaultTruncateBuiltinAlgorithm(
+ ASCII_INSTANCE.getDefaultTerminator(), null, null,
+ null, null, null,
+ true, 1.0);
+ assertAuto(wbMinLen1Algo, "1 3456789", 8, "1 3[...]");
+
+ assertAuto(ASCII_INSTANCE, "123456789", 8, "123[...]");
+ DefaultTruncateBuiltinAlgorithm wbMinLen0Algo = new DefaultTruncateBuiltinAlgorithm(
+ ASCII_INSTANCE.getDefaultTerminator(), null, null,
+ null, null, null,
+ true, 0.0);
+ assertAuto(wbMinLen0Algo, "123456789", 8, "[...]");
+ }
+
+ @Test
+ public void testSimpleEdgeCases() throws TemplateException {
+ Environment env = createEnvironment();
+ for (final DefaultTruncateBuiltinAlgorithm alg : new DefaultTruncateBuiltinAlgorithm[] {
+ ASCII_INSTANCE, UNICODE_INSTANCE,
+ EMPTY_TERMINATOR_INSTANCE, DOTS_INSTANCE, ASCII_NO_W_SPACE_INSTANCE, M_TERM_INSTANCE }) {
+ for (TruncateCaller tc : new TruncateCaller[] {
+ new TruncateCaller() {
+ public TemplateModel truncate(String s, int maxLength, TemplateModel terminator,
+ Integer terminatorLength, Environment env) throws
+ TemplateException {
+ return alg.truncateM(s, maxLength, terminator, terminatorLength, env);
+ }
+ },
+ new TruncateCaller() {
+ public TemplateModel truncate(String s, int maxLength, TemplateModel terminator,
+ Integer terminatorLength, Environment env) throws
+ TemplateException {
+ return alg.truncateCM(s, maxLength, terminator, terminatorLength, env);
+ }
+ },
+ new TruncateCaller() {
+ public TemplateModel truncate(String s, int maxLength, TemplateModel terminator,
+ Integer terminatorLength, Environment env) throws
+ TemplateException {
+ return alg.truncateWM(s, maxLength, terminator, terminatorLength, env);
+ }
+ }
+ }) {
+ assertEquals("", tc.truncate("", 0, null, null, env).toString(), "");
+ assertEquals("", tc.truncate("", 0, null, null, env).toString(), "");
+ if (alg.getDefaultMTerminator() != null) {
+ TemplateModel truncated = tc.truncate("x", 0, null, null, env);
+ assertThat(truncated, instanceOf(TemplateMarkupOutputModel.class));
+ assertSame(alg.getDefaultMTerminator(), truncated);
+ } else {
+ TemplateModel truncated = tc.truncate("x", 0, null, null, env);
+ assertThat(truncated, instanceOf(TemplateStringModel.class));
+ assertEquals(alg.getDefaultTerminator(), ((TemplateStringModel) truncated).getAsString());
+ }
+ SimpleString stringTerminator = new SimpleString("|");
+ assertSame(stringTerminator, tc.truncate("x", 0, stringTerminator, null, env));
+ TemplateHTMLOutputModel htmlTerminator = HTMLOutputFormat.INSTANCE.fromMarkup("<x>.</x>");
+ assertSame(htmlTerminator, tc.truncate("x", 0, htmlTerminator, null, env));
+ }
+ }
+ }
+
+ @Test
+ public void testStandardInstanceSettings() throws TemplateException {
+ Environment env = createEnvironment();
+
+ assertEquals(
+ "123[...]",
+ ASCII_INSTANCE.truncate("1234567890", 8, null, null, env)
+ .getAsString());
+ assertEquals(
+ "12345<span class='truncateTerminator'>[…]</span>",
+ HTMLOutputFormat.INSTANCE.getMarkupString(
+ ((TemplateHTMLOutputModel) ASCII_INSTANCE
+ .truncateM("1234567890", 8, null, null, env))
+ ));
+
+ assertEquals(
+ "12345[\u2026]",
+ UNICODE_INSTANCE.truncate("1234567890", 8, null, null, env)
+ .getAsString());
+ assertEquals(
+ "12345<span class='truncateTerminator'>[…]</span>",
+ HTMLOutputFormat.INSTANCE.getMarkupString(
+ ((TemplateHTMLOutputModel) UNICODE_INSTANCE
+ .truncateM("1234567890", 8, null, null, env))
+ ));
+ }
+
+ private void assertC(TruncateBuiltinAlgorithm algorithm, String in, int maxLength, String expected) {
+ try {
+ TemplateStringModel actual = algorithm.truncateC(in, maxLength, null, null, null);
+ assertEquals(expected, actual.getAsString());
+ } catch (TemplateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void assertW(TruncateBuiltinAlgorithm algorithm, String in, int maxLength, String expected) {
+ try {
+ TemplateStringModel actual = algorithm.truncateW(in, maxLength, null, null, null);
+ assertEquals(expected, actual.getAsString());
+ } catch (TemplateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void assertAuto(TruncateBuiltinAlgorithm algorithm, String in, int maxLength, String expected) {
+ try {
+ TemplateStringModel actual = algorithm.truncate(
+ in, maxLength, null, null, null);
+ assertEquals(expected, actual.getAsString());
+ } catch (TemplateException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ interface TruncateCaller {
+ TemplateModel truncate(
+ String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+ }
+
+ @Test
+ public void testGetLengthWithoutTags() {
+ assertEquals(0, getLengthWithoutTags(""));
+ assertEquals(1, getLengthWithoutTags("a"));
+ assertEquals(2, getLengthWithoutTags("ab"));
+ assertEquals(0, getLengthWithoutTags("<tag>"));
+ assertEquals(1, getLengthWithoutTags("<tag>a"));
+ assertEquals(2, getLengthWithoutTags("<tag>a</tag>b"));
+ assertEquals(4, getLengthWithoutTags("ab<tag>cd</tag>"));
+ assertEquals(2, getLengthWithoutTags("ab<tag></tag>"));
+
+ assertEquals(2, getLengthWithoutTags("&chr;a"));
+ assertEquals(4, getLengthWithoutTags("&chr;a&chr;b"));
+ assertEquals(6, getLengthWithoutTags("ab&chr;cd&chr;"));
+ assertEquals(4, getLengthWithoutTags("ab&chr;&chr;"));
+ assertEquals(4, getLengthWithoutTags("ab<tag>&chr;</tag>&chr;"));
+
+ assertEquals(2, getLengthWithoutTags("<!--c-->ab"));
+ assertEquals(2, getLengthWithoutTags("a<!--c-->b<!--c-->"));
+ assertEquals(2, getLengthWithoutTags("a<!-->--><!---->b"));
+
+ assertEquals(3, getLengthWithoutTags("a<![CDATA[b]]>c"));
+ assertEquals(2, getLengthWithoutTags("a<![CDATA[]]>b"));
+ assertEquals(0, getLengthWithoutTags("<![CDATA[]]>"));
+ assertEquals(3, getLengthWithoutTags("<![CDATA[123"));
+ assertEquals(4, getLengthWithoutTags("<![CDATA[123]"));
+ assertEquals(5, getLengthWithoutTags("<![CDATA[123]]"));
+ assertEquals(3, getLengthWithoutTags("<![CDATA[123]]>"));
+
+ assertEquals(2, getLengthWithoutTags("ab<!--"));
+ assertEquals(2, getLengthWithoutTags("ab<tag"));
+ assertEquals(3, getLengthWithoutTags("ab&chr"));
+ assertEquals(2, getLengthWithoutTags("ab<!-"));
+ assertEquals(2, getLengthWithoutTags("ab<"));
+ assertEquals(3, getLengthWithoutTags("ab&"));
+ assertEquals(3, getLengthWithoutTags("a&;c"));
+ }
+
+ @Test
+ public void testGetCodeFromNumericalCharReferenceName() {
+ assertEquals(0, getCodeFromNumericalCharReferenceName("#0"));
+ assertEquals(0, getCodeFromNumericalCharReferenceName("#00"));
+ assertEquals(0, getCodeFromNumericalCharReferenceName("#x0"));
+ assertEquals(0, getCodeFromNumericalCharReferenceName("#x00"));
+ assertEquals(1, getCodeFromNumericalCharReferenceName("#1"));
+ assertEquals(1, getCodeFromNumericalCharReferenceName("#01"));
+ assertEquals(1, getCodeFromNumericalCharReferenceName("#x1"));
+ assertEquals(1, getCodeFromNumericalCharReferenceName("#x01"));
+ assertEquals(1, getCodeFromNumericalCharReferenceName("#X1"));
+ assertEquals(1, getCodeFromNumericalCharReferenceName("#X01"));
+ assertEquals(123409, getCodeFromNumericalCharReferenceName("#123409"));
+ assertEquals(123409, getCodeFromNumericalCharReferenceName("#00123409"));
+ assertEquals(0x123A0F, getCodeFromNumericalCharReferenceName("#x123A0F"));
+ assertEquals(0x123A0F, getCodeFromNumericalCharReferenceName("#x123a0f"));
+ assertEquals(0x123A0F, getCodeFromNumericalCharReferenceName("#X00123A0f"));
+ assertEquals(-1, getCodeFromNumericalCharReferenceName("#x1G"));
+ assertEquals(-1, getCodeFromNumericalCharReferenceName("#1A"));
+ }
+
+ @Test
+ public void testIsDotCharReference() {
+ assertTrue(isDotCharReference("#46"));
+ assertTrue(isDotCharReference("#x2E"));
+ assertTrue(isDotCharReference("#x2026"));
+ assertTrue(isDotCharReference("hellip"));
+ assertTrue(isDotCharReference("period"));
+
+ assertFalse(isDotCharReference(""));
+ assertFalse(isDotCharReference("foo"));
+ assertFalse(isDotCharReference("#x46"));
+ assertFalse(isDotCharReference("#boo"));
+ }
+
+ @Test
+ public void testIsHtmlOrXmlStartsWithDot() {
+ assertTrue(doesHtmlOrXmlStartWithDot("."));
+ assertTrue(doesHtmlOrXmlStartWithDot(".etc"));
+ assertTrue(doesHtmlOrXmlStartWithDot("…"));
+ assertTrue(doesHtmlOrXmlStartWithDot("<tag x='y'/>…"));
+ assertTrue(doesHtmlOrXmlStartWithDot("<span class='t'>...</span>"));
+ assertTrue(doesHtmlOrXmlStartWithDot("<span class='t'>…</span>"));
+ assertTrue(doesHtmlOrXmlStartWithDot("<span class='t'>.</span>"));
+ assertTrue(doesHtmlOrXmlStartWithDot("<foo><!-- -->.etc"));
+
+ assertFalse(doesHtmlOrXmlStartWithDot(""));
+ assertFalse(doesHtmlOrXmlStartWithDot("[...]"));
+ assertFalse(doesHtmlOrXmlStartWithDot("etc."));
+ assertFalse(doesHtmlOrXmlStartWithDot("<span class='t'>[...]</span>"));
+ assertFalse(doesHtmlOrXmlStartWithDot("<span class='t'>etc.</span>"));
+ assertFalse(doesHtmlOrXmlStartWithDot("<span class='t'>&46;</span>"));
+ }
+
+ @Test
+ public void testTruncateAdhocHtmlTerminator() throws TemplateException {
+ Environment env = createEnvironment();
+ TemplateHTMLOutputModel htmlEllipsis = HTMLOutputFormat.INSTANCE.fromMarkup("<i>…</i>");
+ TemplateHTMLOutputModel htmlSquEllipsis = HTMLOutputFormat.INSTANCE.fromMarkup("<i>[…]</i>");
+
+ // Length detection
+ {
+ TemplateModel actual = ASCII_INSTANCE.truncateM("abcd", 3, htmlEllipsis, null, env);
+ assertThat(actual, instanceOf(TemplateHTMLOutputModel.class));
+ assertEquals(
+ "ab<i>…</i>",
+ HTMLOutputFormat.INSTANCE.getMarkupString((TemplateHTMLOutputModel) actual));
+ }
+ {
+ TemplateModel actual = ASCII_INSTANCE.truncateM("abcdef", 5, htmlSquEllipsis, null, env);
+ assertThat(actual, instanceOf(TemplateHTMLOutputModel.class));
+ assertEquals(
+ "ab<i>[…]</i>",
+ HTMLOutputFormat.INSTANCE.getMarkupString((TemplateHTMLOutputModel) actual));
+ }
+ {
+ TemplateModel actual = ASCII_INSTANCE.truncateM("abcdef", 5, htmlSquEllipsis, 1, env);
+ assertThat(actual, instanceOf(TemplateHTMLOutputModel.class));
+ assertEquals(
+ "abcd<i>[…]</i>",
+ HTMLOutputFormat.INSTANCE.getMarkupString((TemplateHTMLOutputModel) actual));
+ }
+
+ // Dot removal
+ {
+ TemplateModel actual = ASCII_INSTANCE.truncateM("a.cd", 3, htmlEllipsis, null, env);
+ assertThat(actual, instanceOf(TemplateHTMLOutputModel.class));
+ assertEquals(
+ "a<i>…</i>",
+ HTMLOutputFormat.INSTANCE.getMarkupString((TemplateHTMLOutputModel) actual));
+ }
+ {
+ TemplateModel actual = ASCII_INSTANCE.truncateM("a.cdef", 5, htmlSquEllipsis, null, env);
+ assertThat(actual, instanceOf(TemplateHTMLOutputModel.class));
+ assertEquals(
+ "a.<i>[…]</i>",
+ HTMLOutputFormat.INSTANCE.getMarkupString((TemplateHTMLOutputModel) actual));
+ }
+ }
+
+ @Test
+ public void testTruncateAdhocPlainTextTerminator() throws TemplateException {
+ Environment env = createEnvironment();
+ TemplateStringModel ellipsis = new SimpleString("\u2026");
+ TemplateStringModel squEllipsis = new SimpleString("[\u2026]");
+
+ // Length detection
+ {
+ TemplateStringModel actual = ASCII_INSTANCE.truncate("abcd", 3, ellipsis, null, env);
+ assertEquals("ab\u2026", actual.getAsString());
+ }
+ {
+ TemplateStringModel actual = ASCII_INSTANCE.truncate("abcdef", 5, squEllipsis, null, env);
+ assertEquals("ab[\u2026]", actual.getAsString());
+ }
+ {
+ TemplateStringModel actual = ASCII_INSTANCE.truncate("abcdef", 5, squEllipsis, 1, env);
+ assertEquals("abcd[\u2026]", actual.getAsString());
+ }
+
+ // Dot removal
+ {
+ TemplateStringModel actual = ASCII_INSTANCE.truncate("a.cd", 3, ellipsis, null, env);
+ assertEquals("a\u2026", actual.getAsString());
+ }
+ {
+ TemplateStringModel actual = ASCII_INSTANCE.truncate("a.cdef", 5, squEllipsis, null, env);
+ assertEquals("a.[\u2026]", actual.getAsString());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
index 6324cf4..958be29 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
@@ -75,7 +75,7 @@ abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
protected ASTExpression target;
protected String key;
- static final int NUMBER_OF_BIS = 265;
+ static final int NUMBER_OF_BIS = 271;
static final HashMap<String, ASTExpBuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
static {
@@ -264,6 +264,12 @@ abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME));
putBI("timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME));
putBI("trim", new BuiltInsForStringsBasic.trimBI());
+ putBI("truncate", new BuiltInsForStringsBasic.truncateBI());
+ putBI("truncateW", new BuiltInsForStringsBasic.truncate_wBI());
+ putBI("truncateC", new BuiltInsForStringsBasic.truncate_cBI());
+ putBI("truncateM", new BuiltInsForStringsBasic.truncate_mBI());
+ putBI("truncateWM", new BuiltInsForStringsBasic.truncate_w_mBI());
+ putBI("truncateCM", new BuiltInsForStringsBasic.truncate_c_mBI());
putBI("uncapFirst", new BuiltInsForStringsBasic.uncap_firstBI());
putBI("upperAbc", new BuiltInsForNumbers.upper_abcBI());
putBI("upperCase", new BuiltInsForStringsBasic.upper_caseBI());
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java
index 1a5f1be..13f7327 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java
@@ -29,10 +29,12 @@ import java.util.regex.Pattern;
import org.apache.freemarker.core.model.ArgumentArrayLayout;
import org.apache.freemarker.core.model.TemplateBooleanModel;
import org.apache.freemarker.core.model.TemplateFunctionModel;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateStringModel;
import org.apache.freemarker.core.model.impl.SimpleNumber;
import org.apache.freemarker.core.model.impl.SimpleString;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
import org.apache.freemarker.core.util.CallableUtils;
import org.apache.freemarker.core.util._StringUtils;
@@ -745,6 +747,145 @@ class BuiltInsForStringsBasic {
}
}
+ static abstract class AbstractTruncateBI extends BuiltInForString {
+ @Override
+ TemplateModel calculateResult(final String s, final Environment env) {
+ return new TemplateFunctionModel() {
+
+ @Override
+ public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws
+ TemplateException {
+ int maxLength = CallableUtils.getIntArgument(args, 0, this);
+ if (maxLength < 0) {
+ throw newArgumentValueException(
+ 0, "must be at least 0, but was " + maxLength + ".", this);
+ }
+
+ TemplateModel terminator = args[1];
+ Integer terminatorLength;
+ if (terminator != null) {
+ if (!(terminator instanceof TemplateStringModel)) {
+ if (allowMarkupTerminator()) {
+ if (!(terminator instanceof TemplateMarkupOutputModel)) {
+ throw newArgumentValueTypeException(
+ terminator, 1,
+ new Class[] { TemplateStringModel.class, TemplateMarkupOutputModel.class },
+ "string or markup", this);
+ }
+ } else {
+ throw newArgumentValueTypeException(terminator, 1, TemplateStringModel.class, this);
+ }
+ }
+
+ terminatorLength = CallableUtils.getOptionalIntArgument(args, 2, this);
+ if (terminatorLength != null && terminatorLength < 0) {
+ throw newArgumentValueException(
+ 2, "must be at least 0, but was " + terminatorLength + ".", this);
+ }
+ } else {
+ terminatorLength = null;
+ }
+ try {
+ TruncateBuiltinAlgorithm algorithm = env.getTruncateBuiltinAlgorithm();
+ return truncate(algorithm, s, maxLength, terminator, terminatorLength, env);
+ } catch (TemplateException e) {
+ throw new TemplateException(e, env, "Truncation failed; see cause exception");
+ }
+ }
+
+ @Override
+ public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
+ return ArgumentArrayLayout.THREE_POSITIONAL_PARAMETERS;
+ }
+ };
+ }
+
+ protected abstract TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException;
+
+ protected abstract boolean allowMarkupTerminator();
+ }
+
+ static class truncateBI extends AbstractTruncateBI {
+ protected TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException {
+ return algorithm.truncate(s, maxLength, (TemplateStringModel) terminator, terminatorLength, env);
+ }
+
+ protected boolean allowMarkupTerminator() {
+ return false;
+ }
+ }
+
+ static class truncate_wBI extends AbstractTruncateBI {
+ protected TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException {
+ return algorithm.truncateW(s, maxLength, (TemplateStringModel) terminator, terminatorLength, env);
+ }
+
+ protected boolean allowMarkupTerminator() {
+ return false;
+ }
+ }
+
+ static class truncate_cBI extends AbstractTruncateBI {
+ protected TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException {
+ return algorithm.truncateC(s, maxLength, (TemplateStringModel) terminator, terminatorLength, env);
+ }
+
+ protected boolean allowMarkupTerminator() {
+ return false;
+ }
+ }
+
+ static class truncate_mBI extends AbstractTruncateBI {
+ protected TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException {
+ return algorithm.truncateM(s, maxLength, terminator, terminatorLength, env);
+ }
+
+ protected boolean allowMarkupTerminator() {
+ return true;
+ }
+ }
+
+ static class truncate_w_mBI extends AbstractTruncateBI {
+ protected TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException {
+ return algorithm.truncateWM(s, maxLength, terminator, terminatorLength, env);
+ }
+
+ protected boolean allowMarkupTerminator() {
+ return true;
+ }
+ }
+
+ static class truncate_c_mBI extends AbstractTruncateBI {
+ protected TemplateModel truncate(
+ TruncateBuiltinAlgorithm algorithm, String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength, Environment env)
+ throws TemplateException {
+ return algorithm.truncateCM(s, maxLength, terminator, terminatorLength, env);
+ }
+
+ protected boolean allowMarkupTerminator() {
+ return true;
+ }
+ }
+
static class uncap_firstBI extends BuiltInForString {
@Override
TemplateModel calculateResult(String s, Environment env) {
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
index 9a9d13b..03a8a55 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Configuration.java
@@ -64,6 +64,8 @@ import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XHTMLOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
import org.apache.freemarker.core.templateresolver.CacheStorage;
import org.apache.freemarker.core.templateresolver.GetTemplateResult;
import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
@@ -211,6 +213,7 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
private final TemplateClassResolver newBuiltinClassResolver;
private final Boolean showErrorTips;
private final Boolean apiBuiltinEnabled;
+ private final TruncateBuiltinAlgorithm truncateBuiltinAlgorithm;
private final Map<String, TemplateDateFormatFactory> customDateFormats;
private final Map<String, TemplateNumberFormatFactory> customNumberFormats;
private final Map<String, String> autoImports;
@@ -374,6 +377,7 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
newBuiltinClassResolver = builder.getNewBuiltinClassResolver();
showErrorTips = builder.getShowErrorTips();
apiBuiltinEnabled = builder.getAPIBuiltinEnabled();
+ truncateBuiltinAlgorithm = builder.getTruncateBuiltinAlgorithm();
customDateFormats = _CollectionUtils.mergeImmutableMaps(
builder.getImpliedCustomDateFormats(), builder.getCustomDateFormats(), false);
customNumberFormats = _CollectionUtils.mergeImmutableMaps(
@@ -1116,6 +1120,20 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
* value in the {@link Configuration}.
*/
@Override
+ public boolean isTruncateBuiltinAlgorithmSet() {
+ return true;
+ }
+
+ @Override
+ public TruncateBuiltinAlgorithm getTruncateBuiltinAlgorithm() {
+ return truncateBuiltinAlgorithm;
+ }
+
+ /**
+ * Always {@code true} in {@link Configuration}-s; even if this setting wasn't set in the builder, it gets a default
+ * value in the {@link Configuration}.
+ */
+ @Override
public boolean isAPIBuiltinEnabledSet() {
return true;
}
@@ -1207,7 +1225,7 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
/**
* {@inheritDoc}
* <p>
- * Because {@link Configuration} has on parent, the {@code includeInherited} parameter is ignored.
+ * Because {@link Configuration} has no parent, the {@code includeInherited} parameter is ignored.
*/
@Override
public Map<Serializable, Object> getCustomSettings(boolean includeInherited) {
@@ -2658,6 +2676,11 @@ public final class Configuration implements TopLevelConfiguration, CustomStateSc
}
@Override
+ protected TruncateBuiltinAlgorithm getDefaultTruncateBuiltinAlgorithm() {
+ return DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE;
+ }
+
+ @Override
protected boolean getDefaultLazyImports() {
return false;
}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
index 637901e..e6a91b6 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Environment.java
@@ -65,6 +65,7 @@ import org.apache.freemarker.core.model.TemplateStringModel;
import org.apache.freemarker.core.model.impl.SimpleHash;
import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
import org.apache.freemarker.core.templateresolver.TemplateResolver;
import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat;
@@ -885,6 +886,11 @@ public final class Environment extends MutableProcessingConfiguration<Environmen
}
@Override
+ protected TruncateBuiltinAlgorithm getDefaultTruncateBuiltinAlgorithm() {
+ return getMainTemplate().getTruncateBuiltinAlgorithm();
+ }
+
+ @Override
protected boolean getDefaultLazyImports() {
return getMainTemplate().getLazyImports();
}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
index ab58924..d959251 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/MutableProcessingConfiguration.java
@@ -50,6 +50,8 @@ import org.apache.freemarker.core.outputformat.impl.PlainTextOutputFormat;
import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
import org.apache.freemarker.core.templateresolver.AndMatcher;
import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
@@ -102,6 +104,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
public static final String NEW_BUILTIN_CLASS_RESOLVER_KEY = "newBuiltinClassResolver";
public static final String SHOW_ERROR_TIPS_KEY = "showErrorTips";
public static final String API_BUILTIN_ENABLED_KEY = "apiBuiltinEnabled";
+ public static final String TRUNCATE_BUILTIN_ALGORITHM_KEY = "truncateBuiltinAlgorithm";
public static final String LAZY_IMPORTS_KEY = "lazyImports";
public static final String LAZY_AUTO_IMPORTS_KEY = "lazyAutoImports";
public static final String AUTO_IMPORTS_KEY = "autoImports";
@@ -131,6 +134,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
TEMPLATE_EXCEPTION_HANDLER_KEY,
TIME_FORMAT_KEY,
TIME_ZONE_KEY,
+ TRUNCATE_BUILTIN_ALGORITHM_KEY,
URL_ESCAPING_CHARSET_KEY
);
@@ -154,6 +158,7 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
private TemplateClassResolver newBuiltinClassResolver;
private Boolean showErrorTips;
private Boolean apiBuiltinEnabled;
+ private TruncateBuiltinAlgorithm truncateBuiltinAlgorithm;
private Map<String, TemplateDateFormatFactory> customDateFormats;
private Map<String, TemplateNumberFormatFactory> customNumberFormats;
private Map<String, String> autoImports;
@@ -1049,6 +1054,46 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
return apiBuiltinEnabled != null;
}
+ /**
+ * Setter pair of {@link #getTruncateBuiltinAlgorithm()}
+ */
+ public void setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm value) {
+ _NullArgumentException.check("value", value);
+ truncateBuiltinAlgorithm = value;
+ }
+
+ /**
+ * Fluent API equivalent of {@link #setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)}
+ */
+ public SelfT truncateBuiltinAlgorithm(TruncateBuiltinAlgorithm value) {
+ setTruncateBuiltinAlgorithm(value);
+ return self();
+ }
+
+ /**
+ * Resets the setting value as if it was never set (but it doesn't affect the value inherited from another
+ * {@link ProcessingConfiguration}).
+ */
+ public void unsetTruncateBuiltinAlgorithm() {
+ truncateBuiltinAlgorithm = null;
+ }
+
+ @Override
+ public TruncateBuiltinAlgorithm getTruncateBuiltinAlgorithm() {
+ return isTruncateBuiltinAlgorithmSet() ? truncateBuiltinAlgorithm : getDefaultTruncateBuiltinAlgorithm();
+ }
+
+ /**
+ * Returns the value the getter method returns when the setting is not set (possibly by inheriting the setting value
+ * from another {@link ProcessingConfiguration}), or throws {@link CoreSettingValueNotSetException}.
+ */
+ protected abstract TruncateBuiltinAlgorithm getDefaultTruncateBuiltinAlgorithm();
+
+ @Override
+ public boolean isTruncateBuiltinAlgorithmSet() {
+ return truncateBuiltinAlgorithm != null;
+ }
+
@Override
public boolean getLazyImports() {
return isLazyImportsSet() ? lazyImports : getDefaultLazyImports();
@@ -1445,7 +1490,27 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
* See {@link #setAPIBuiltinEnabled(boolean)}.
* Since 2.3.22.
* <br>String value: {@code "true"}, {@code "false"}, {@code "y"}, etc.
- *
+ *
+ * <li><p>{@code "truncateBuiltinAlgorithm"}:
+ * See {@link #setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)}.
+ * <br>String value: An
+ * <a href="#fm_obe">object builder expression</a>, or one of the predefined values (case insensitive),
+ * {@code ascii} (for {@link DefaultTruncateBuiltinAlgorithm#ASCII_INSTANCE}) and
+ * {@code unicode} (for {@link DefaultTruncateBuiltinAlgorithm#UNICODE_INSTANCE}).
+ * <br>Example object builder expressions:
+ * <br>Use {@code "..."} as terminator (and same as markup terminator), and add space if the
+ * truncation happened on word boundary:
+ * <br>{@code DefaultTruncateBuiltinAlgorithm("...", true)}
+ * <br>Use {@code "..."} as terminator, and also a custom HTML for markup terminator, and add space if the
+ * truncation happened on word boundary:
+ * <br>{@code DefaultTruncateBuiltinAlgorithm("...",
+ * markup(HTMLOutputFormat(), "<span class=trunc>...</span>"), true)}
+ * <br>Recreate default truncate algorithm, but with not preferring truncation at word boundaries (i.e.,
+ * with {@code wordBoundaryMinLength} 1.0):
+ * <br><code>DefaultTruncateBuiltinAlgorithm(<br>
+ * DefaultTruncateBuiltinAlgorithm.STANDARD_ASCII_TERMINATOR, null, null,<br>
+ * DefaultTruncateBuiltinAlgorithm.STANDARD_M_TERMINATOR, null, null,<br>
+ * true, 1.0)</code>
* </ul>
*
* <p>{@link Configuration} (a subclass of {@link MutableProcessingConfiguration}) also understands these:</p>
@@ -1622,7 +1687,8 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
* {@link AndMatcher}, {@link OrMatcher}, {@link NotMatcher}, {@link ConditionalTemplateConfigurationFactory},
* {@link MergingTemplateConfigurationFactory}, {@link FirstMatchTemplateConfigurationFactory},
* {@link HTMLOutputFormat}, {@link XMLOutputFormat}, {@link RTFOutputFormat}, {@link PlainTextOutputFormat},
- * {@link UndefinedOutputFormat}, {@link Configuration}, {@link TemplateLanguage}, {@link TagSyntax}.
+ * {@link UndefinedOutputFormat}, {@link Configuration}, {@link TemplateLanguage}, {@link TagSyntax},
+ * {@link DefaultTruncateBuiltinAlgorithm}.
* </li>
* <li>
* <p>{@link TimeZone} objects can be created like {@code TimeZone("UTC")}, despite that there's no a such
@@ -1759,6 +1825,16 @@ public abstract class MutableProcessingConfiguration<SelfT extends MutableProces
setShowErrorTips(_StringUtils.getYesNo(value));
} else if (API_BUILTIN_ENABLED_KEY.equals(name)) {
setAPIBuiltinEnabled(_StringUtils.getYesNo(value));
+ } else if (TRUNCATE_BUILTIN_ALGORITHM_KEY.equals(name)) {
+ if ("ascii".equalsIgnoreCase(value)) {
+ setTruncateBuiltinAlgorithm(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE);
+ } else if ("unicode".equalsIgnoreCase(value)) {
+ setTruncateBuiltinAlgorithm(DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE);
+ } else {
+ setTruncateBuiltinAlgorithm((TruncateBuiltinAlgorithm) _ObjectBuilderSettingEvaluator.eval(
+ value, TruncateBuiltinAlgorithm.class, false,
+ _SettingEvaluationEnvironment.getCurrent()));
+ }
} else if (NEW_BUILTIN_CLASS_RESOLVER_KEY.equals(name)) {
if ("unrestricted".equals(value)) {
setNewBuiltinClassResolver(TemplateClassResolver.UNRESTRICTED);
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
index 7e1a164..add8387 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ProcessingConfiguration.java
@@ -28,10 +28,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Properties;
import java.util.TimeZone;
import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
import org.apache.freemarker.core.valueformat.TemplateNumberFormat;
import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory;
@@ -751,4 +754,21 @@ public interface ProcessingConfiguration {
*/
Map<Serializable, Object> getCustomSettings(boolean includeInherited);
+ /**
+ * The algorithm used for {@code ?truncate}. Defaults to {@link DefaultTruncateBuiltinAlgorithm#ASCII_INSTANCE}.
+ * Most customization needs can be addressed by creating a new {@link DefaultTruncateBuiltinAlgorithm} with the
+ * proper constructor parameters. Otherwise users my use their own {@link TruncateBuiltinAlgorithm} implementation.
+ *
+ * <p>In case you need to set this with {@link Properties}, or a similar configuration approach that doesn't let you
+ * create the value in Java, see examples at {@link MutableProcessingConfiguration#setSetting(String, String)}.
+ */
+ public TruncateBuiltinAlgorithm getTruncateBuiltinAlgorithm();
+
+ /**
+ * Tells if this setting is set directly in this object. If not, then depending on the implementing class, reading
+ * the setting mights returns a default value, or returns the value of the setting from a parent object, or throws
+ * an {@link CoreSettingValueNotSetException}.
+ */
+ public boolean isTruncateBuiltinAlgorithmSet();
+
}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
index c184281..9f6a7b7 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/Template.java
@@ -52,6 +52,7 @@ import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateNodeModel;
import org.apache.freemarker.core.model.impl.SimpleHash;
import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
import org.apache.freemarker.core.templateresolver.TemplateLoader;
import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver;
@@ -1046,6 +1047,16 @@ public class Template implements ProcessingConfiguration, CustomStateScope {
}
@Override
+ public TruncateBuiltinAlgorithm getTruncateBuiltinAlgorithm() {
+ return tCfg != null && tCfg.isTruncateBuiltinAlgorithmSet() ? tCfg.getTruncateBuiltinAlgorithm() : cfg.getTruncateBuiltinAlgorithm();
+ }
+
+ @Override
+ public boolean isTruncateBuiltinAlgorithmSet() {
+ return tCfg != null && tCfg.isTruncateBuiltinAlgorithmSet();
+ }
+
+ @Override
public boolean getAutoFlush() {
return tCfg != null && tCfg.isAutoFlushSet() ? tCfg.getAutoFlush() : cfg.getAutoFlush();
}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
index 1b1b9a9..3107890 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java
@@ -28,6 +28,7 @@ import java.util.TimeZone;
import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
import org.apache.freemarker.core.util.CommonBuilder;
import org.apache.freemarker.core.util._CollectionUtils;
import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
@@ -73,6 +74,7 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
private final TemplateClassResolver newBuiltinClassResolver;
private final Boolean showErrorTips;
private final Boolean apiBuiltinEnabled;
+ private final TruncateBuiltinAlgorithm truncateBuiltinAlgorithm;
private final Map<String, TemplateDateFormatFactory> customDateFormats;
private final Map<String, TemplateNumberFormatFactory> customNumberFormats;
private final Map<String, String> autoImports;
@@ -113,6 +115,8 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
newBuiltinClassResolver = builder.isNewBuiltinClassResolverSet() ? builder.getNewBuiltinClassResolver() : null;
showErrorTips = builder.isShowErrorTipsSet() ? builder.getShowErrorTips() : null;
apiBuiltinEnabled = builder.isAPIBuiltinEnabledSet() ? builder.getAPIBuiltinEnabled() : null;
+ truncateBuiltinAlgorithm = builder.isTruncateBuiltinAlgorithmSet() ? builder.getTruncateBuiltinAlgorithm()
+ : null;
customDateFormats = builder.isCustomDateFormatsSet() ? builder.getCustomDateFormats() : null;
customNumberFormats = builder.isCustomNumberFormatsSet() ? builder.getCustomNumberFormats() : null;
autoImports = builder.isAutoImportsSet() ? builder.getAutoImports() : null;
@@ -474,6 +478,19 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
}
@Override
+ public TruncateBuiltinAlgorithm getTruncateBuiltinAlgorithm() {
+ if (!isTruncateBuiltinAlgorithmSet()) {
+ throw new CoreSettingValueNotSetException("truncateBuiltinAlgorithm");
+ }
+ return truncateBuiltinAlgorithm;
+ }
+
+ @Override
+ public boolean isTruncateBuiltinAlgorithmSet() {
+ return truncateBuiltinAlgorithm != null;
+ }
+
+ @Override
public boolean getAutoFlush() {
if (!isAutoFlushSet()) {
throw new CoreSettingValueNotSetException("autoFlush");
@@ -709,6 +726,11 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
}
@Override
+ protected TruncateBuiltinAlgorithm getDefaultTruncateBuiltinAlgorithm() {
+ throw new CoreSettingValueNotSetException("truncateBuiltinAlgorithm");
+ }
+
+ @Override
protected boolean getDefaultLazyImports() {
throw new CoreSettingValueNotSetException("lazyImports");
}
@@ -790,6 +812,9 @@ public final class TemplateConfiguration implements ParsingAndProcessingConfigur
if (tc.isNewBuiltinClassResolverSet()) {
setNewBuiltinClassResolver(tc.getNewBuiltinClassResolver());
}
+ if (tc.isTruncateBuiltinAlgorithmSet()) {
+ setTruncateBuiltinAlgorithm(tc.getTruncateBuiltinAlgorithm());
+ }
if (tc.isNumberFormatSet()) {
setNumberFormat(tc.getNumberFormat());
}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java
index 4377372..ec52865 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_ObjectBuilderSettingEvaluator.java
@@ -47,6 +47,7 @@ import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XHTMLOutputFormat;
import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.impl.DefaultTruncateBuiltinAlgorithm;
import org.apache.freemarker.core.templateresolver.AndMatcher;
import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
import org.apache.freemarker.core.templateresolver.FileExtensionMatcher;
@@ -682,6 +683,8 @@ public class _ObjectBuilderSettingEvaluator {
addWithSimpleName(SHORTHANDS, PlainTextOutputFormat.class);
addWithSimpleName(SHORTHANDS, UndefinedOutputFormat.class);
+ addWithSimpleName(SHORTHANDS, DefaultTruncateBuiltinAlgorithm.class);
+
addWithSimpleName(SHORTHANDS, TemplateLanguage.class);
addWithSimpleName(SHORTHANDS, Locale.class);
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/pluggablebuiltin/TruncateBuiltinAlgorithm.java b/freemarker-core/src/main/java/org/apache/freemarker/core/pluggablebuiltin/TruncateBuiltinAlgorithm.java
new file mode 100644
index 0000000..b142649
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/pluggablebuiltin/TruncateBuiltinAlgorithm.java
@@ -0,0 +1,126 @@
+package org.apache.freemarker.core.pluggablebuiltin;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
+
+/**
+ * Used for implementing the "truncate" family of built-ins. There are several variations of the "truncate" built-ins,
+ * each has a corresponding method here. See
+ * {@link #truncateM(String, int, TemplateModel, Integer, Environment)}
+ * as the starting point.
+ *
+ * <p>New methods may be added in later versions, whoever they won't be abstract for backward compatibility.
+ *
+ * @see Configurable#setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)
+ *
+ * @since 2.3.29
+ */
+public abstract class TruncateBuiltinAlgorithm {
+
+ /**
+ * Corresponds to {@code ?truncate_m(...)} in templates. This method decides automatically if it will truncate at
+ * word boundary (see {@link #truncateWM}) or at character boundary (see {@link #truncateCM}). While it depends
+ * on the implementation, the idea is that it should truncate at word boundary, unless that gives a too short
+ * string, in which case it falls back to truncation at character duration.
+ *
+ * <p>The terminator and the return value can be {@link TemplateMarkupOutputModel} (FTL markup output type), not
+ * just {@link String} (FTL string type), hence the "m" in the name.
+ *
+ * @param s
+ * The input string whose length need to be limited. The caller (the FreeMarker core normally) is
+ * expected to guarantee that this won't be {@code null}.
+ *
+ * @param maxLength
+ * The maximum length of the returned string, although the algorithm need not guarantee this strictly.
+ * For example, if this is less than the length of the {@code terminator} string, then most algorithms
+ * should still return the {@code terminator} string. Or, some sophisticated algorithm may counts in
+ * letters differently depending on their visual width. The goal is usually to prevent unusually long
+ * string values to ruin visual layout, while showing clearly to the user that the end of the string
+ * was cut off. If the input string is not longer than the maximum length, then it should be returned
+ * as is. The caller (the FreeMarker core normally) is expected to guarantee that this will be at
+ * least 0.
+ *
+ * @param terminator
+ * The string or markup to show at the end of the returned string if the string was actually truncated.
+ * This can be {@code null}, in which case the default terminator of the algorithm will be used. It
+ * can be an FTL string (a {@link TemplateStringModel}) of any length (including 0), or a
+ * {@link TemplateMarkupOutputModel} (typically HTML markup). If it's {@link TemplateMarkupOutputModel},
+ * then the result is {@link TemplateMarkupOutputModel} of the same output format as well, otherwise
+ * it can remain {@link TemplateStringModel}. Note that the length of the terminator counts into the
+ * result length that shouldn't be exceed ({@code maxLength}) (or at least the algorithm should make
+ * an effort to avoid that).
+ *
+ * @param terminatorLength
+ * The assumed length of the terminator. If this is {@code null} (and typically it is), then the method
+ * decides the length of the terminator. If this is not {@code null}, then the method must pretend
+ * that the terminator length is this. This can be used to specify the visual length of a terminator
+ * explicitly, which can't always be decided well programmatically.
+ *
+ * @param env
+ * The runtime environment from which this algorithm was called. The caller (the FreeMarker core
+ * normally) is expected to guarantee that this won't be {@code null}.
+ *
+ * @return The truncated text, which is either a {@link TemplateStringModel} (FTL string), or a
+ * {@link TemplateMarkupOutputModel}.
+ *
+ * @throws TemplateException
+ * If anything goes wrong during truncating. It's unlikely that an implementation will need this though.
+ */
+ public abstract TemplateModel truncateM(
+ String s, int maxLength, TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+
+ /**
+ * Corresponds to {@code ?truncate(...)} in templates.
+ * Similar to {@link #truncateM(String, int, TemplateModel, Integer, Environment)}, but only allows
+ * an FTL string as terminator, and thence the return value is always an FTL string as well (not
+ * {@link TemplateMarkupOutputModel}).
+ */
+ public abstract TemplateStringModel truncate(
+ String s, int maxLength, TemplateStringModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+
+ /**
+ * Corresponds to {@code ?truncate_w(...)} in templates.
+ * Same as {@link #truncateWM(String, int, TemplateModel, Integer, Environment)}, but only allows
+ * an FTL string as terminator, and thence the return value is always an FTL string as well (not
+ * {@link TemplateMarkupOutputModel}).
+ */
+ public abstract TemplateStringModel truncateW(
+ String s, int maxLength, TemplateStringModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+
+ /**
+ * Corresponds to {@code ?truncate_w_m(...)} in templates.
+ * Similar to {@link #truncateM(String, int, TemplateModel, Integer, Environment)}, but the
+ * truncation should happen at word boundary (hence the "w"). That is, the truncation isn't allowed to truncate a
+ * word. What counts as a word, is up to the implementation, but at least in {@link DefaultTruncateBuiltinAlgorithm}
+ * words are the sections that are separated by whitespace (so punctuation doesn't separate words).
+ */
+ public abstract TemplateModel truncateWM(
+ String s, int maxLength, TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+
+ /**
+ * Corresponds to {@code ?truncate_c_m(...)} in templates.
+ * Same as {@link #truncateCM(String, int, TemplateModel, Integer, Environment)}, but only allows
+ * an FTL string as terminator, and thence the return value is always an FTL string as well (not markup).
+ */
+ public abstract TemplateStringModel truncateC(
+ String s, int maxLength, TemplateStringModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+
+ /**
+ * Corresponds to {@code ?truncate_c_m(...)} in templates.
+ * Similar to {@link #truncateM(String, int, TemplateModel, Integer, Environment)}, but the
+ * truncation should not prefer truncating at word boundaries over the closer approximation of the desired {@code
+ * maxLength}. Hence, we say that it truncates at character boundary (hence the "c").
+ */
+ public abstract TemplateModel truncateCM(
+ String s, int maxLength, TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException;
+
+}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/pluggablebuiltin/impl/DefaultTruncateBuiltinAlgorithm.java b/freemarker-core/src/main/java/org/apache/freemarker/core/pluggablebuiltin/impl/DefaultTruncateBuiltinAlgorithm.java
new file mode 100644
index 0000000..c6ad312
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/pluggablebuiltin/impl/DefaultTruncateBuiltinAlgorithm.java
@@ -0,0 +1,755 @@
+package org.apache.freemarker.core.pluggablebuiltin.impl;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.ProcessingConfiguration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.TemplateMarkupOutputModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
+import org.apache.freemarker.core.model.impl.SimpleString;
+import org.apache.freemarker.core.outputformat.MarkupOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.TemplateHTMLOutputModel;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.pluggablebuiltin.TruncateBuiltinAlgorithm;
+import org.apache.freemarker.core.util.TemplateLanguageUtils;
+import org.apache.freemarker.core.util._NullArgumentException;
+
+/**
+ * The default {@link TruncateBuiltinAlgorithm} implementation; see
+ * {@link ProcessingConfiguration#getTruncateBuiltinAlgorithm()}.
+ * To know the properties of this {@link TruncateBuiltinAlgorithm} implementation, see the
+ * {@linkplain DefaultTruncateBuiltinAlgorithm#DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double) constructor}. You can find more explanation and
+ * examples in the documentation of the {@code truncate} built-in in the FreeMarker Manual.
+ */
+public class DefaultTruncateBuiltinAlgorithm extends TruncateBuiltinAlgorithm {
+
+ /** Used by {@link #ASCII_INSTANCE} as the terminator. */
+ public static final String STANDARD_ASCII_TERMINATOR = "[...]";
+
+ /** Used by {@link #UNICODE_INSTANCE} as the terminator. */
+ public static final String STANDARD_UNICODE_TERMINATOR = "[\u2026]";
+
+ /**
+ * Used by {@link #ASCII_INSTANCE} and {@link #UNICODE_INSTANCE} as the markup terminator;
+ * HTML {@code <span class='truncateTerminator'>[…]</span>}, where {@code …} is the ellipsis (…)
+ * character. Note that while the ellipsis character is not in US-ASCII, this still works safely regardless of
+ * output charset, as {@code …} itself only contains US-ASCII characters.
+ */
+ public static final TemplateHTMLOutputModel STANDARD_M_TERMINATOR;
+ static {
+ try {
+ STANDARD_M_TERMINATOR = HTMLOutputFormat.INSTANCE.fromMarkup(
+ "<span class='truncateTerminator'>[…]</span>");
+ } catch (TemplateException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * The value used in the constructor of {@link #ASCII_INSTANCE} and {@link #UNICODE_INSTANCE} as the
+ * {@code wordBoundaryMinLength} argument.
+ */
+ public static final double DEFAULT_WORD_BOUNDARY_MIN_LENGTH = 0.75;
+
+ /** Used if {@link #getMTerminatorLength(TemplateMarkupOutputModel)} can't detect the length. */
+ private static final int FALLBACK_M_TERMINATOR_LENGTH = 3;
+
+ private enum TruncationMode {
+ CHAR_BOUNDARY, WORD_BOUNDARY, AUTO
+ }
+
+ /**
+ * Instance that uses {@code "[...]"} as the {@code defaultTerminator} constructor argument, and thus is
+ * safe to use for all output charsets. Because of that, this is the default of
+ * {@link ProcessingConfiguration#getTruncateBuiltinAlgorithm()}.
+ * The {@code defaultMTerminator} (markup terminator) is {@link #STANDARD_M_TERMINATOR}, and the
+ * {@code wordBoundaryMinLength} is {@link #DEFAULT_WORD_BOUNDARY_MIN_LENGTH}, and {@code addSpaceAtWordBoundary}
+ * is {@code true}.
+ */
+ public static final DefaultTruncateBuiltinAlgorithm ASCII_INSTANCE = new DefaultTruncateBuiltinAlgorithm(
+ STANDARD_ASCII_TERMINATOR, STANDARD_M_TERMINATOR, true);
+
+ /**
+ * Instance uses that {@code "[…]"} as the {@code defaultTerminator} constructor argument, which contains
+ * ellipsis character ({@code "…"}, U+2026), and thus only works with UTF-8, and the cp125x charsets (like
+ * cp1250), and with some other rarely used ones. It does not work (becomes to a question mark) with ISO-8859-x
+ * charsets (like ISO-8859-1), which are probably the most often used charsets after UTF-8.
+ *
+ * <p>The {@code defaultMTerminator} (markup terminator) is {@link #STANDARD_M_TERMINATOR}, and the
+ * {@code wordBoundaryMinLength} is {@link #DEFAULT_WORD_BOUNDARY_MIN_LENGTH}, and {@code addSpaceAtWordBoundary}
+ * is {@code true}.
+ */
+ public static final DefaultTruncateBuiltinAlgorithm UNICODE_INSTANCE = new DefaultTruncateBuiltinAlgorithm(
+ STANDARD_UNICODE_TERMINATOR, STANDARD_M_TERMINATOR, true);
+
+ private final TemplateStringModel defaultTerminator;
+ private final int defaultTerminatorLength;
+ private final boolean defaultTerminatorRemovesDots;
+
+ private final TemplateMarkupOutputModel<?> defaultMTerminator;
+ private final Integer defaultMTerminatorLength;
+ private final boolean defaultMTerminatorRemovesDots;
+
+ private final double wordBoundaryMinLength;
+ private final boolean addSpaceAtWordBoundary;
+
+ /**
+ * Creates an instance with a string (plain text) terminator and a markup terminator.
+ * See parameters in {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean, TemplateMarkupOutputModel,
+ * Integer, Boolean, boolean, Double)}; the missing parameters will be {@code null}.
+ */
+ public DefaultTruncateBuiltinAlgorithm(
+ String defaultTerminator,
+ TemplateMarkupOutputModel<?> defaultMTerminator,
+ boolean addSpaceAtWordBoundary) {
+ this(
+ defaultTerminator, null, null,
+ defaultMTerminator, null, null,
+ addSpaceAtWordBoundary, null);
+ }
+
+ /**
+ * Creates an instance with string (plain text) terminator.
+ * See parameters in {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean, TemplateMarkupOutputModel,
+ * Integer, Boolean, boolean, Double)}; the missing parameters will be {@code null}.
+ */
+ public DefaultTruncateBuiltinAlgorithm(
+ String defaultTerminator,
+ boolean addSpaceAtWordBoundary) {
+ this(
+ defaultTerminator, null, null,
+ null, null, null,
+ addSpaceAtWordBoundary, null);
+ }
+
+ /**
+ * Creates an instance with markup terminator.
+ * @param defaultTerminator
+ * The terminator to use if the invocation (like {@code s?truncate(20)}) doesn't specify it. The
+ * terminator is the text appended after a truncated string, to indicate that it was truncated.
+ * Typically it's {@code "[...]"} or {@code "..."}, or the same with UNICODE ellipsis character.
+ * @param defaultTerminatorLength
+ * The assumed length of {@code defaultTerminator}, or {@code null} if it should be get via
+ * {@code defaultTerminator.length()}.
+ * @param defaultTerminatorRemovesDots
+ * Whether dots and ellipsis characters that the {@code defaultTerminator} touches should be removed. If
+ * {@code null}, this will be auto-detected based on if {@code defaultTerminator} starts with dot or
+ * ellipsis. The goal is to avoid outcomes where we have more dots next to each other than there are in
+ * the terminator.
+ * @param defaultMTerminator
+ * Similar to {@code defaultTerminator}, but is markup instead of plain text. This can be {@code null},
+ * in which case {@code defaultTerminator} will be used even if {@code ?truncate_m} or similar built-in
+ * is called.
+ * @param defaultMTerminatorLength
+ * The assumed length of the terminator, or {@code null} if it should be get via
+ * {@link #getMTerminatorLength}.
+ * @param defaultMTerminatorRemovesDots
+ * Similar to {@code defaultTerminatorRemovesDots}, but for {@code defaultMTerminator}. If {@code
+ * null}, and {@code defaultMTerminator} is HTML/XML/XHTML, then it will be examined of the
+ * first character of the terminator that's outside a HTML/XML tag or comment is dot or ellipsis
+ * (after resolving numerical character references). For other kind of markup it defaults to {@code
+ * true}, to be on the safe side.
+ * @param addSpaceAtWordBoundary,
+ * Whether to add a space before the terminator if the truncation happens directly after the end of a
+ * word. For example, when "too long sentence" is truncated, it will be a like "too long [...]"
+ * instead of "too long[...]". When the truncation happens inside a word, this has on effect, i.e., it
+ * will be always like "too long se[...]" (no space before the terminator). Note that only whitespace is
+ * considered to be a word separator, not punctuation, so if this is {@code true}, you get results
+ * like "Some sentence. [...]".
+ * @param wordBoundaryMinLength
+ * Used when {@link #truncate} or {@link #truncateM} has to decide between
+ * word boundary truncation and character boundary truncation; it's the minimum length, given as
+ * proportion of {@code maxLength}, that word boundary truncation has to produce. If the resulting
+ * length is less, we do character boundary truncation instead. For example, if {@code maxLength} is
+ * 30, and this parameter is 0.85, then: 30*0.85 = 25.5, rounded up that's 26, so the resulting length
+ * must be at least 26. The result of character boundary truncation will be always accepted, even if its
+ * still too short. If this parameter is {@code null}, then {@link #DEFAULT_WORD_BOUNDARY_MIN_LENGTH}
+ * will be used. If this parameter is 0, then truncation always happens at word boundary. If this
+ * parameter is 1.0, then truncation doesn't prefer word boundaries over other places.
+ */
+ public DefaultTruncateBuiltinAlgorithm(
+ String defaultTerminator, Integer defaultTerminatorLength,
+ Boolean defaultTerminatorRemovesDots,
+ TemplateMarkupOutputModel<?> defaultMTerminator, Integer defaultMTerminatorLength,
+ Boolean defaultMTerminatorRemovesDots,
+ boolean addSpaceAtWordBoundary, Double wordBoundaryMinLength) {
+ _NullArgumentException.check("defaultTerminator", defaultTerminator);
+ this.defaultTerminator = new SimpleString(defaultTerminator);
+ try {
+ this.defaultTerminatorLength = defaultTerminatorLength != null ? defaultTerminatorLength
+ : defaultTerminator.length();
+
+ this.defaultTerminatorRemovesDots = defaultTerminatorRemovesDots != null ? defaultTerminatorRemovesDots
+ : getTerminatorRemovesDots(defaultTerminator);
+ } catch (TemplateException e) {
+ throw new IllegalArgumentException("Failed to examine defaultTerminator", e);
+ }
+
+ this.defaultMTerminator = defaultMTerminator;
+ if (defaultMTerminator != null) {
+ try {
+ this.defaultMTerminatorLength = defaultMTerminatorLength != null ? defaultMTerminatorLength
+ : getMTerminatorLength(defaultMTerminator);
+
+ this.defaultMTerminatorRemovesDots = defaultMTerminatorRemovesDots != null
+ ? defaultMTerminatorRemovesDots
+ : getMTerminatorRemovesDots(defaultMTerminator);
+ } catch (TemplateException e) {
+ throw new IllegalArgumentException("Failed to examine defaultMTerminator", e);
+ }
+ } else {
+ // There's no mTerminator, but these final fields must be set
+ this.defaultMTerminatorLength = null;
+ this.defaultMTerminatorRemovesDots = false;
+ }
+
+ if (wordBoundaryMinLength == null) {
+ wordBoundaryMinLength = DEFAULT_WORD_BOUNDARY_MIN_LENGTH;
+ } else if (wordBoundaryMinLength < 0 || wordBoundaryMinLength > 1) {
+ throw new IllegalArgumentException("wordBoundaryMinLength must be between 0.0 and 1.0 (inclusive)");
+ }
+ this.wordBoundaryMinLength = wordBoundaryMinLength;
+
+ this.addSpaceAtWordBoundary = addSpaceAtWordBoundary;
+ }
+
+ @Override
+ public TemplateStringModel truncate(
+ String s, int maxLength,
+ TemplateStringModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return (TemplateStringModel) unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.AUTO, false);
+ }
+
+ @Override
+ public TemplateStringModel truncateW(
+ String s, int maxLength,
+ TemplateStringModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return (TemplateStringModel) unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.WORD_BOUNDARY, false);
+ }
+
+ @Override
+ public TemplateStringModel truncateC(
+ String s, int maxLength,
+ TemplateStringModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return (TemplateStringModel) unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.CHAR_BOUNDARY, false);
+ }
+
+ @Override
+ public TemplateModel truncateM(
+ String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.AUTO, true);
+ }
+
+ @Override
+ public TemplateModel truncateWM(
+ String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.WORD_BOUNDARY, true);
+ }
+
+ @Override
+ public TemplateModel truncateCM(
+ String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.CHAR_BOUNDARY, true);
+ }
+
+ public String getDefaultTerminator() {
+ try {
+ return defaultTerminator.getAsString();
+ } catch (TemplateException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * See similarly named parameter of {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double)} the construction}.
+ */
+ public int getDefaultTerminatorLength() {
+ return defaultTerminatorLength;
+ }
+
+ /**
+ * See similarly named parameter of {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double)} the construction}.
+ */
+ public boolean getDefaultTerminatorRemovesDots() {
+ return defaultTerminatorRemovesDots;
+ }
+
+ /**
+ * See similarly named parameter of {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double)} the construction}.
+ */
+ public TemplateMarkupOutputModel<?> getDefaultMTerminator() {
+ return defaultMTerminator;
+ }
+
+ /**
+ * See similarly named parameter of {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double)} the construction}.
+ */
+ public Integer getDefaultMTerminatorLength() {
+ return defaultMTerminatorLength;
+ }
+
+ public boolean getDefaultMTerminatorRemovesDots() {
+ return defaultMTerminatorRemovesDots;
+ }
+
+ /**
+ * See similarly named parameter of {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double)} the construction}.
+ */
+ public double getWordBoundaryMinLength() {
+ return wordBoundaryMinLength;
+ }
+
+ /**
+ * See similarly named parameter of {@link #DefaultTruncateBuiltinAlgorithm(String, Integer, Boolean,
+ * TemplateMarkupOutputModel, Integer, Boolean, boolean, Double)} the construction}.
+ */
+ public boolean getAddSpaceAtWordBoundary() {
+ return addSpaceAtWordBoundary;
+ }
+
+ /**
+ * Returns the (estimated) length of the argument terminator. It should only count characters that are visible for
+ * the user (like in the web browser).
+ *
+ * <p>In the implementation in {@link DefaultTruncateBuiltinAlgorithm}, if the markup is HTML/XML/XHTML, then this
+ * counts the characters outside tags and comments, and inside CDATA sections (ignoring the CDATA section
+ * delimiters). Furthermore then it counts character and entity references as having length of 1. If the markup
+ * is not HTML/XML/XHTML (or subclasses of those {@link MarkupOutputFormat}-s) then it doesn't know how to
+ * measure it, and simply returns 3.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ protected int getMTerminatorLength(TemplateMarkupOutputModel<?> mTerminator) throws TemplateException {
+ MarkupOutputFormat format = mTerminator.getOutputFormat();
+ return isHTMLOrXML(format) ?
+ getLengthWithoutTags(format.getMarkupString(mTerminator))
+ : FALLBACK_M_TERMINATOR_LENGTH;
+ }
+
+ /**
+ * Tells if the dots touched by the terminator text should be removed.
+ *
+ * <p>The implementation in {@link DefaultTruncateBuiltinAlgorithm} return {@code true} if the terminator starts
+ * with dot (or ellipsis).
+ *
+ * @param terminator
+ * A {@link TemplateStringModel} or {@link TemplateMarkupOutputModel}. Not {@code null}.
+ */
+ protected boolean getTerminatorRemovesDots(String terminator) throws TemplateException {
+ return terminator.startsWith(".") || terminator.startsWith("\u2026");
+ }
+
+ /**
+ * Same as {@link #getTerminatorRemovesDots(String)}, but invoked for a markup terminator.
+ *
+ * <p>The implementation in {@link DefaultTruncateBuiltinAlgorithm} will skip HTML/XML tags and comments,
+ * and resolves relevant character references to find out if the first character is dot or ellipsis. But it only
+ * does this for HTML/XMl/XHTML (or subclasses of those {@link MarkupOutputFormat}-s), otherwise it always
+ * returns {@code true} to be on the safe side.
+ */
+ protected boolean getMTerminatorRemovesDots(TemplateMarkupOutputModel terminator) throws TemplateException {
+ return isHTMLOrXML(terminator.getOutputFormat())
+ ? doesHtmlOrXmlStartWithDot(terminator.getOutputFormat().getMarkupString(terminator))
+ : true;
+ }
+
+ /**
+ * Deals with both CB and WB truncation, hence it's unified.
+ */
+ private TemplateModel unifiedTruncate(
+ String s, int maxLength,
+ TemplateModel terminator, Integer terminatorLength,
+ TruncationMode mode, boolean allowMarkupResult)
+ throws TemplateException {
+ if (s.length() <= maxLength) {
+ return new SimpleString(s);
+ }
+ if (maxLength < 0) {
+ throw new IllegalArgumentException("maxLength can't be negative");
+ }
+
+ Boolean terminatorRemovesDots;
+ if (terminator == null) {
+ if (allowMarkupResult && defaultMTerminator != null) {
+ terminator = defaultMTerminator;
+ terminatorLength = defaultMTerminatorLength;
+ terminatorRemovesDots = defaultMTerminatorRemovesDots;
+ } else {
+ terminator = defaultTerminator;
+ terminatorLength = defaultTerminatorLength;
+ terminatorRemovesDots = defaultTerminatorRemovesDots;
+ }
+ } else {
+ if (terminatorLength != null) {
+ if (terminatorLength < 0) {
+ throw new IllegalArgumentException("terminatorLength can't be negative");
+ }
+ } else {
+ terminatorLength = getTerminatorLength(terminator);
+ }
+ terminatorRemovesDots = null; // lazily calculated
+ }
+
+ StringBuilder truncatedS = unifiedTruncateWithoutTerminatorAdded(
+ s,
+ maxLength,
+ terminator, terminatorLength, terminatorRemovesDots,
+ mode);
+
+ // The terminator is always shown, even if with that we exceed maxLength. Otherwise the user couldn't
+ // see that the string was truncated.
+ if (truncatedS == null || truncatedS.length() == 0) {
+ return terminator;
+ }
+
+ if (terminator instanceof TemplateStringModel) {
+ truncatedS.append(((TemplateStringModel) terminator).getAsString());
+ return new SimpleString(truncatedS.toString());
+ } else if (terminator instanceof TemplateMarkupOutputModel) {
+ TemplateMarkupOutputModel markup = (TemplateMarkupOutputModel) terminator;
+ MarkupOutputFormat outputFormat = markup.getOutputFormat();
+ return outputFormat.concat(outputFormat.fromPlainTextByEscaping(truncatedS.toString()), markup);
+ } else {
+ throw new IllegalArgumentException("Unsupported terminator type: "
+ + TemplateLanguageUtils.getTypeDescription(terminator));
+ }
+ }
+
+ private StringBuilder unifiedTruncateWithoutTerminatorAdded(
+ String s, int maxLength,
+ TemplateModel terminator, int terminatorLength, Boolean terminatorRemovesDots,
+ TruncationMode mode) throws TemplateException {
+ final int cbInitialLastCIdx = maxLength - terminatorLength - 1;
+ int cbLastCIdx = cbInitialLastCIdx;
+
+ // Why we do this here: If both Word Boundary and Character Boundary truncation will be attempted, then this way
+ // we don't have to skip the WS twice.
+ cbLastCIdx = skipTrailingWS(s, cbLastCIdx);
+ if (cbLastCIdx < 0) {
+ return null;
+ }
+
+ if (mode == TruncationMode.AUTO && wordBoundaryMinLength < 1.0 || mode == TruncationMode.WORD_BOUNDARY) {
+ // Do word boundary truncation. Might not be possible due to minLength restriction (see below), in which
+ // case truncedS stays null.
+ StringBuilder truncedS = null;
+ {
+ final int wordTerminatorLength = addSpaceAtWordBoundary ? terminatorLength + 1 : terminatorLength;
+ final int minIdx = mode == TruncationMode.AUTO
+ ? Math.max(((int) Math.ceil(maxLength * wordBoundaryMinLength)) - wordTerminatorLength - 1, 0)
+ : 0;
+
+ int wbLastCIdx = Math.min(maxLength - wordTerminatorLength - 1, cbLastCIdx);
+ boolean followingCIsWS
+ = s.length() > wbLastCIdx + 1 ? Character.isWhitespace(s.charAt(wbLastCIdx + 1)) : true;
+ executeTruncateWB:
+ while (wbLastCIdx >= minIdx) {
+ char curC = s.charAt(wbLastCIdx);
+ boolean curCIsWS = Character.isWhitespace(curC);
+ if (!curCIsWS && followingCIsWS) {
+ // Note how we avoid getMTerminatorRemovesDots until we absolutely need its result.
+ if (!addSpaceAtWordBoundary && isDot(curC)) {
+ if (terminatorRemovesDots == null) {
+ terminatorRemovesDots = getTerminatorRemovesDots(terminator);
+ }
+ if (terminatorRemovesDots) {
+ while (wbLastCIdx >= minIdx && isDotOrWS(s.charAt(wbLastCIdx))) {
+ wbLastCIdx--;
+ }
+ if (wbLastCIdx < minIdx) {
+ break executeTruncateWB;
+ }
+ }
+ }
+
+ truncedS = new StringBuilder(wbLastCIdx + 1 + wordTerminatorLength);
+ truncedS.append(s, 0, wbLastCIdx + 1);
+ if (addSpaceAtWordBoundary) {
+ truncedS.append(' ');
+ }
+ break executeTruncateWB;
+ }
+
+ followingCIsWS = curCIsWS;
+ wbLastCIdx--;
+ } // executeTruncateWB: while (...)
+ }
+ if (truncedS != null
+ || mode == TruncationMode.WORD_BOUNDARY
+ || mode == TruncationMode.AUTO && wordBoundaryMinLength == 0.0) {
+ return truncedS;
+ }
+ // We are in TruncationMode.AUTO. truncateW wasn't possible, so fall back to character boundary truncation.
+ }
+
+ // Do character boundary truncation.
+
+ // If the truncation point is a word boundary, and thus we add a space before the terminator, then we may run
+ // out of the maxLength by 1. In that case we have to truncate one character earlier.
+ if (cbLastCIdx == cbInitialLastCIdx && addSpaceAtWordBoundary && isWordEnd(s, cbLastCIdx)) {
+ cbLastCIdx--;
+ if (cbLastCIdx < 0) {
+ return null;
+ }
+ }
+
+ // Skip trailing WS, also trailing dots if necessary.
+ boolean skippedDots;
+ do {
+ skippedDots = false;
+
+ cbLastCIdx = skipTrailingWS(s, cbLastCIdx);
+ if (cbLastCIdx < 0) {
+ return null;
+ }
+
+ // Note how we avoid getMTerminatorRemovesDots until we absolutely need its result.
+ if (isDot(s.charAt(cbLastCIdx)) && !(addSpaceAtWordBoundary && isWordEnd(s, cbLastCIdx))) {
+ if (terminatorRemovesDots == null) {
+ terminatorRemovesDots = getTerminatorRemovesDots(terminator);
+ }
+ if (terminatorRemovesDots) {
+ cbLastCIdx = skipTrailingDots(s, cbLastCIdx);
+ if (cbLastCIdx < 0) {
+ return null;
+ }
+ skippedDots = true;
+ }
+ }
+ } while (skippedDots);
+
+ boolean addWordBoundarySpace = addSpaceAtWordBoundary && isWordEnd(s, cbLastCIdx);
+ StringBuilder truncatedS = new StringBuilder(cbLastCIdx + 1 + (addWordBoundarySpace ? 1 : 0) + terminatorLength);
+ truncatedS.append(s, 0, cbLastCIdx + 1);
+ if (addWordBoundarySpace) {
+ truncatedS.append(' ');
+ }
+ return truncatedS;
+ }
+
+ private int getTerminatorLength(TemplateModel terminator) throws TemplateException {
+ return terminator instanceof TemplateStringModel
+ ? ((TemplateStringModel) terminator).getAsString().length()
+ : getMTerminatorLength((TemplateMarkupOutputModel<?>) terminator);
+ }
+
+ private boolean getTerminatorRemovesDots(TemplateModel terminator) throws TemplateException {
+ return terminator instanceof TemplateStringModel
+ ? getTerminatorRemovesDots(((TemplateStringModel) terminator).getAsString())
+ : getMTerminatorRemovesDots((TemplateMarkupOutputModel<?>) terminator);
+ }
+
+ private int skipTrailingWS(String s, int lastCIdx) {
+ while (lastCIdx >= 0 && Character.isWhitespace(s.charAt(lastCIdx))) {
+ lastCIdx--;
+ }
+ return lastCIdx;
+ }
+
+ private int skipTrailingDots(String s, int lastCIdx) {
+ while (lastCIdx >= 0 && isDot(s.charAt(lastCIdx))) {
+ lastCIdx--;
+ }
+ return lastCIdx;
+ }
+
+ private boolean isWordEnd(String s, int lastCIdx) {
+ return lastCIdx + 1 >= s.length() || Character.isWhitespace(s.charAt(lastCIdx + 1));
+ }
+
+ private static boolean isDot(char c) {
+ return c == '.' || c == '\u2026';
+ }
+
+ private static boolean isDotOrWS(char c) {
+ return isDot(c) || Character.isWhitespace(c);
+ }
+
+ private boolean isHTMLOrXML(MarkupOutputFormat<?> outputFormat) {
+ return outputFormat instanceof HTMLOutputFormat || outputFormat instanceof XMLOutputFormat;
+ }
+
+ /**
+ * Returns the length of a string, ignoring HTML/XML tags and comments, also, character and entity references are
+ * count as having length of 1, and CDATA sections are counted in with the length of their content. So for
+ * example, the length of {@code "<span>x&y</span>"} will be 3 (as visually it's {@code x&y}, which is 3
+ * characters).
+ */
+ // Not private for testability
+ static int getLengthWithoutTags(String s) {
+ // Fixes/improvements here should be also done here: doesHtmlOrXmlStartWithDot
+
+ int result = 0;
+ int i = 0;
+ int len = s.length();
+ countChars: while (i < len) {
+ char c = s.charAt(i++);
+ if (c == '<') {
+ if (s.startsWith("!--", i)) {
+ // <!--...-->
+ i += 3;
+ while (i + 2 < len && !(s.charAt(i) == '-' && s.charAt(i + 1) == '-' && s.charAt(i + 2) == '>')) {
+ i++;
+ }
+ i += 3;
+ if (i >= len) {
+ break countChars;
+ }
+ } else if (s.startsWith("![CDATA[", i)) {
+ // <![CDATA[...]]>
+ i += 8;
+ while (i < len
+ && !(s.charAt(i) == ']'
+ && i + 2 < len && s.charAt(i + 1) == ']' && s.charAt(i + 2) == '>')) {
+ result++;
+ i++;
+ }
+ i += 3;
+ if (i >= len) {
+ break countChars;
+ }
+ } else {
+ // <...>
+ while (i < len && s.charAt(i) != '>') {
+ i++;
+ }
+ i++;
+ if (i >= len) {
+ break countChars;
+ }
+ }
+ } else if (c == '&') {
+ // &...;
+ while (i < len && s.charAt(i) != ';') {
+ i++;
+ }
+ i++;
+ result++;
+ if (i >= len) {
+ break countChars;
+ }
+ } else {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Check if the specified HTML or XML starts with dot or ellipsis, if we ignore tags and comments.
+ */
+ // Not private for testability
+ static boolean doesHtmlOrXmlStartWithDot(String s) {
+ // Fixes/improvements here should be also done here: getLengthWithoutTags
+
+ int i = 0;
+ int len = s.length();
+ consumeChars: while (i < len) {
+ char c = s.charAt(i++);
+ if (c == '<') {
+ if (s.startsWith("!--", i)) {
+ // <!--...-->
+ i += 3;
+ while (i + 2 < len
+ && !((c = s.charAt(i)) == '-' && s.charAt(i + 1) == '-' && s.charAt(i + 2) == '>')) {
+ i++;
+ }
+ i += 3;
+ if (i >= len) {
+ break consumeChars;
+ }
+ } else if (s.startsWith("![CDATA[", i)) {
+ // <![CDATA[...]]>
+ i += 8;
+ while (i < len
+ && !((c = s.charAt(i)) == ']'
+ && i + 2 < len
+ && s.charAt(i + 1) == ']' && s.charAt(i + 2) == '>')) {
+ return isDot(c);
+ }
+ i += 3;
+ if (i >= len) {
+ break consumeChars;
+ }
+ } else {
+ // <...>
+ while (i < len && s.charAt(i) != '>') {
+ i++;
+ }
+ i++;
+ if (i >= len) {
+ break consumeChars;
+ }
+ }
+ } else if (c == '&') {
+ // &...;
+ int start = i;
+ while (i < len && s.charAt(i) != ';') {
+ i++;
+ }
+ return isDotCharReference(s.substring(start, i));
+ } else {
+ return isDot(c);
+ }
+ }
+ return false;
+ }
+
+ // Not private for testability
+ static boolean isDotCharReference(String name) {
+ if (name.length() > 2 && name.charAt(0) == '#') {
+ int charCode = getCodeFromNumericalCharReferenceName(name);
+ return charCode == 0x2026 || charCode == 0x2e;
+ }
+ return name.equals("hellip") || name.equals("period");
+ }
+
+ // Not private for testability
+ static int getCodeFromNumericalCharReferenceName(String name) {
+ char c = name.charAt(1);
+ boolean hex = c == 'x' || c == 'X';
+ int code = 0;
+ for (int pos = hex ? 2 : 1; pos < name.length(); pos++) {
+ c = name.charAt(pos);
+ code *= hex ? 16 : 10;
+ if (c >= '0' && c <= '9') {
+ code += c - '0';
+ } else if (hex && c >= 'a' && c <= 'f') {
+ code += c - 'a' + 10;
+ } else if (hex && c >= 'A' && c <= 'F') {
+ code += c - 'A' + 10;
+ } else {
+ return -1;
+ }
+ }
+ return code;
+ }
+
+}