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:22:47 UTC
[freemarker] branch 2.3-gae updated: Added ?truncate built-ins and
related setting
This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/2.3-gae by this push:
new 7c5ef10 Added ?truncate built-ins and related setting
7c5ef10 is described below
commit 7c5ef10ef3da3b94fc5cdf9d61c966282b6cd8ac
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Jan 16 20:22:36 2019 +0100
Added ?truncate built-ins and related setting
---
src/main/java/freemarker/core/BuiltIn.java | 8 +-
.../freemarker/core/BuiltInsForStringsBasic.java | 135 ++++
src/main/java/freemarker/core/Configurable.java | 97 ++-
.../core/DefaultTruncateBuiltinAlgorithm.java | 769 +++++++++++++++++++++
.../freemarker/core/TemplateConfiguration.java | 7 +
.../freemarker/core/TruncateBuiltinAlgorithm.java | 143 ++++
.../core/_ObjectBuilderSettingEvaluator.java | 4 +-
src/manual/en_US/book.xml | 240 ++++++-
.../core/DefaultTruncateBuiltinAlgorithmTest.java | 669 ++++++++++++++++++
.../freemarker/core/TemplateConfigurationTest.java | 1 +
.../java/freemarker/core/TruncateBuiltInTest.java | 153 ++++
.../freemarker/template/ConfigurationTest.java | 63 +-
12 files changed, 2277 insertions(+), 12 deletions(-)
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 19af35e..fb2cb9a 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -84,7 +84,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
static final Set<String> CAMEL_CASE_NAMES = new TreeSet<String>();
static final Set<String> SNAKE_CASE_NAMES = new TreeSet<String>();
- static final int NUMBER_OF_BIS = 268;
+ static final int NUMBER_OF_BIS = 279;
static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
static {
@@ -280,6 +280,12 @@ abstract class BuiltIn extends Expression implements Cloneable {
putBI("time", new BuiltInsForMultipleTypes.dateBI(TemplateDateModel.TIME));
putBI("time_if_unknown", "timeIfUnknown", new BuiltInsForDates.dateType_if_unknownBI(TemplateDateModel.TIME));
putBI("trim", new BuiltInsForStringsBasic.trimBI());
+ putBI("truncate", new BuiltInsForStringsBasic.truncateBI());
+ putBI("truncate_w", "truncateW", new BuiltInsForStringsBasic.truncate_wBI());
+ putBI("truncate_c", "truncateC", new BuiltInsForStringsBasic.truncate_cBI());
+ putBI("truncate_m", "truncateM", new BuiltInsForStringsBasic.truncate_mBI());
+ putBI("truncate_w_m", "truncateWM", new BuiltInsForStringsBasic.truncate_w_mBI());
+ putBI("truncate_c_m", "truncateCM", new BuiltInsForStringsBasic.truncate_c_mBI());
putBI("uncap_first", "uncapFirst", new BuiltInsForStringsBasic.uncap_firstBI());
putBI("upper_abc", "upperAbc", new BuiltInsForNumbers.upper_abcBI());
putBI("upper_case", "upperCase", new BuiltInsForStringsBasic.upper_caseBI());
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
index 27a6212..7583984 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsBasic.java
@@ -642,6 +642,141 @@ class BuiltInsForStringsBasic {
}
}
+ static abstract class AbstractTruncateBI extends BuiltInForString {
+ @Override
+ TemplateModel calculateResult(final String s, final Environment env) {
+ return new TemplateMethodModelEx() {
+ public Object exec(java.util.List args) throws TemplateModelException {
+ int argCount = args.size();
+ checkMethodArgCount(argCount, 1, 3);
+
+ int maxLength = getNumberMethodArg(args, 0).intValue();
+ if (maxLength < 0) {
+ throw new _TemplateModelException("?", key, "(...) argument #1 can't be negative.");
+ }
+
+ TemplateModel terminator;
+ Integer terminatorLength;
+ if (argCount > 1) {
+ terminator = (TemplateModel) args.get(1);
+ if (!(terminator instanceof TemplateScalarModel)) {
+ if (allowMarkupTerminator()) {
+ if (!(terminator instanceof TemplateMarkupOutputModel)) {
+ throw _MessageUtil.newMethodArgMustBeStringOrMarkupOutputException(
+ "?" + key, 1, terminator);
+ }
+ } else {
+ throw _MessageUtil.newMethodArgMustBeStringException(
+ "?" + key, 1, terminator);
+ }
+ }
+
+ Number terminatorLengthNum = getOptNumberMethodArg(args, 2);
+ terminatorLength = terminatorLengthNum != null ? terminatorLengthNum.intValue() : null;
+ if (terminatorLength != null && terminatorLength < 0) {
+ throw new _TemplateModelException("?", key, "(...) argument #3 can't be negative.");
+ }
+ } else {
+ terminator = null;
+ terminatorLength = null;
+ }
+ try {
+ TruncateBuiltinAlgorithm algorithm = env.getTruncateBuiltinAlgorithm();
+ return truncate(algorithm, s, maxLength, terminator, terminatorLength, env);
+ } catch (TemplateException e) {
+ throw new _TemplateModelException(
+ AbstractTruncateBI.this, e, env, "Truncation failed; see cause exception");
+ }
+ }
+ };
+ }
+
+ 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, (TemplateScalarModel) 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, (TemplateScalarModel) 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, (TemplateScalarModel) 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/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index 28803f1..70f2dfa 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -240,7 +240,14 @@ public class Configurable {
public static final String API_BUILTIN_ENABLED_KEY_CAMEL_CASE = "apiBuiltinEnabled";
/** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. @since 2.3.22 */
public static final String API_BUILTIN_ENABLED_KEY = API_BUILTIN_ENABLED_KEY_SNAKE_CASE;
-
+
+ /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.29 */
+ public static final String TRUNCATE_BUILTIN_ALGORITHM_KEY_SNAKE_CASE = "truncate_builtin_algorithm";
+ /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.29 */
+ public static final String TRUNCATE_BUILTIN_ALGORITHM_KEY_CAMEL_CASE = "truncateBuiltinAlgorithm";
+ /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+ public static final String TRUNCATE_BUILTIN_ALGORITHM_KEY = TRUNCATE_BUILTIN_ALGORITHM_KEY_SNAKE_CASE;
+
/** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
public static final String LOG_TEMPLATE_EXCEPTIONS_KEY_SNAKE_CASE = "log_template_exceptions";
/** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
@@ -282,7 +289,7 @@ public class Configurable {
public static final String AUTO_INCLUDE_KEY_CAMEL_CASE = "autoInclude";
/** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
public static final String AUTO_INCLUDE_KEY = AUTO_INCLUDE_KEY_SNAKE_CASE;
-
+
/** @deprecated Use {@link #STRICT_BEAN_MODELS_KEY} instead. */
@Deprecated
public static final String STRICT_BEAN_MODELS = STRICT_BEAN_MODELS_KEY;
@@ -315,6 +322,7 @@ public class Configurable {
TEMPLATE_EXCEPTION_HANDLER_KEY_SNAKE_CASE,
TIME_FORMAT_KEY_SNAKE_CASE,
TIME_ZONE_KEY_SNAKE_CASE,
+ TRUNCATE_BUILTIN_ALGORITHM_KEY_SNAKE_CASE,
URL_ESCAPING_CHARSET_KEY_SNAKE_CASE,
WRAP_UNCHECKED_EXCEPTIONS_KEY_SNAKE_CASE
};
@@ -347,6 +355,7 @@ public class Configurable {
TEMPLATE_EXCEPTION_HANDLER_KEY_CAMEL_CASE,
TIME_FORMAT_KEY_CAMEL_CASE,
TIME_ZONE_KEY_CAMEL_CASE,
+ TRUNCATE_BUILTIN_ALGORITHM_KEY_CAMEL_CASE,
URL_ESCAPING_CHARSET_KEY_CAMEL_CASE,
WRAP_UNCHECKED_EXCEPTIONS_KEY_CAMEL_CASE
};
@@ -376,9 +385,10 @@ public class Configurable {
private String urlEscapingCharset;
private boolean urlEscapingCharsetSet;
private Boolean autoFlush;
- private TemplateClassResolver newBuiltinClassResolver;
private Boolean showErrorTips;
+ private TemplateClassResolver newBuiltinClassResolver;
private Boolean apiBuiltinEnabled;
+ private TruncateBuiltinAlgorithm truncateBuiltinAlgorithm;
private Boolean logTemplateExceptions;
private Boolean wrapUncheckedExceptions;
private Map<String, ? extends TemplateDateFormatFactory> customDateFormats;
@@ -452,7 +462,9 @@ public class Configurable {
newBuiltinClassResolver = TemplateClassResolver.UNRESTRICTED_RESOLVER;
properties.setProperty(NEW_BUILTIN_CLASS_RESOLVER_KEY, newBuiltinClassResolver.getClass().getName());
-
+
+ truncateBuiltinAlgorithm = DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE;
+
showErrorTips = Boolean.TRUE;
properties.setProperty(SHOW_ERROR_TIPS_KEY, showErrorTips.toString());
@@ -1476,7 +1488,10 @@ public class Configurable {
}
outputEncodingSet = true;
}
-
+
+ /**
+ * Getter pair of {@link #setOutputEncoding(String)}.
+ */
public String getOutputEncoding() {
return outputEncodingSet
? outputEncoding
@@ -1666,7 +1681,41 @@ public class Configurable {
public boolean isAPIBuiltinEnabledSet() {
return apiBuiltinEnabled != null;
}
-
+
+ /**
+ * Specifies 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 #setSetting(String, String)}.
+ *
+ * @since 2.3.29
+ */
+ public void setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm truncateBuiltinAlgorithm) {
+ NullArgumentException.check("truncateBuiltinAlgorithm", truncateBuiltinAlgorithm);
+ this.truncateBuiltinAlgorithm = truncateBuiltinAlgorithm;
+ }
+
+ /**
+ * See {@link #setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)}
+ *
+ * @since 2.3.29
+ */
+ public TruncateBuiltinAlgorithm getTruncateBuiltinAlgorithm() {
+ return truncateBuiltinAlgorithm != null ? truncateBuiltinAlgorithm : parent.getTruncateBuiltinAlgorithm();
+ }
+
+ /**
+ * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+ *
+ * @since 2.3.29
+ */
+ public boolean isTruncateBuiltinAlgorithmSet() {
+ return truncateBuiltinAlgorithm != null;
+ }
+
/**
* Specifies if {@link TemplateException}-s thrown by template processing are logged by FreeMarker or not. The
* default is {@code true} for backward compatibility, but that results in logging the exception twice in properly
@@ -2287,7 +2336,28 @@ public class Configurable {
* See {@link #setAPIBuiltinEnabled(boolean)}.
* Since 2.3.22.
* <br>String value: {@code "true"}, {@code "false"}, {@code "y"}, etc.
- *
+ *
+ * <li><p>{@code "truncate_builtin_algorithm"}:
+ * See {@link #setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)}.
+ * Since 2.3.19.
+ * <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>freemarker.core.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 Configurable}) also understands these:</p>
@@ -2498,7 +2568,7 @@ public class Configurable {
* {@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 UndefinedOutputFormat}, {@link Configuration}, {@link DefaultTruncateBuiltinAlgorithm}.
* </li>
* <li>
* <p>{@link TimeZone} objects can be created like {@code TimeZone("UTC")}, despite that there's no a such
@@ -2663,6 +2733,17 @@ public class Configurable {
} else if (API_BUILTIN_ENABLED_KEY_SNAKE_CASE.equals(name)
|| API_BUILTIN_ENABLED_KEY_CAMEL_CASE.equals(name)) {
setAPIBuiltinEnabled(StringUtil.getYesNo(value));
+ } else if (TRUNCATE_BUILTIN_ALGORITHM_KEY_SNAKE_CASE.equals(name)
+ || TRUNCATE_BUILTIN_ALGORITHM_KEY_CAMEL_CASE.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_SNAKE_CASE.equals(name)
|| NEW_BUILTIN_CLASS_RESOLVER_KEY_CAMEL_CASE.equals(name)) {
if ("unrestricted".equals(value)) {
diff --git a/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java b/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java
new file mode 100644
index 0000000..279e037
--- /dev/null
+++ b/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java
@@ -0,0 +1,769 @@
+/*
+ * 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 freemarker.core;
+
+import freemarker.template.SimpleScalar;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.utility.ClassUtil;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * The default {@link TruncateBuiltinAlgorithm} implementation; see
+ * {@link Configurable#setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)}.
+ * 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.
+ *
+ * @since 2.3.29
+ */
+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 (TemplateModelException 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 Configurable#setTruncateBuiltinAlgorithm(TruncateBuiltinAlgorithm)}. 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 TemplateScalarModel 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 SimpleScalar(defaultTerminator);
+ try {
+ this.defaultTerminatorLength = defaultTerminatorLength != null ? defaultTerminatorLength
+ : defaultTerminator.length();
+
+ this.defaultTerminatorRemovesDots = defaultTerminatorRemovesDots != null ? defaultTerminatorRemovesDots
+ : getTerminatorRemovesDots(defaultTerminator);
+ } catch (TemplateModelException 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 (TemplateModelException 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 TemplateScalarModel truncate(
+ String s, int maxLength,
+ TemplateScalarModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return (TemplateScalarModel) unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.AUTO, false);
+ }
+
+ @Override
+ public TemplateScalarModel truncateW(
+ String s, int maxLength,
+ TemplateScalarModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return (TemplateScalarModel) unifiedTruncate(
+ s, maxLength, terminator, terminatorLength,
+ TruncationMode.WORD_BOUNDARY, false);
+ }
+
+ @Override
+ public TemplateScalarModel truncateC(
+ String s, int maxLength,
+ TemplateScalarModel terminator, Integer terminatorLength,
+ Environment env) throws TemplateException {
+ return (TemplateScalarModel) 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 (TemplateModelException 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 TemplateModelException {
+ 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 TemplateScalarModel} or {@link TemplateMarkupOutputModel}. Not {@code null}.
+ */
+ protected boolean getTerminatorRemovesDots(String terminator) throws TemplateModelException {
+ 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 TemplateModelException {
+ 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 SimpleScalar(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 TemplateScalarModel) {
+ truncatedS.append(((TemplateScalarModel) terminator).getAsString());
+ return new SimpleScalar(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: "
+ + ClassUtil.getFTLTypeDescription(terminator));
+ }
+ }
+
+ private StringBuilder unifiedTruncateWithoutTerminatorAdded(
+ String s, int maxLength,
+ TemplateModel terminator, int terminatorLength, Boolean terminatorRemovesDots,
+ TruncationMode mode) throws TemplateModelException {
+ 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 TemplateModelException {
+ return terminator instanceof TemplateScalarModel
+ ? ((TemplateScalarModel) terminator).getAsString().length()
+ : getMTerminatorLength((TemplateMarkupOutputModel<?>) terminator);
+ }
+
+ private boolean getTerminatorRemovesDots(TemplateModel terminator) throws TemplateModelException {
+ return terminator instanceof TemplateScalarModel
+ ? getTerminatorRemovesDots(((TemplateScalarModel) 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;
+ }
+
+}
diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java
index ad7b1f8..4fb907b 100644
--- a/src/main/java/freemarker/core/TemplateConfiguration.java
+++ b/src/main/java/freemarker/core/TemplateConfiguration.java
@@ -208,6 +208,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
if (tc.isNewBuiltinClassResolverSet()) {
setNewBuiltinClassResolver(tc.getNewBuiltinClassResolver());
}
+ if (tc.isTruncateBuiltinAlgorithmSet()) {
+ setTruncateBuiltinAlgorithm(tc.getTruncateBuiltinAlgorithm());
+ }
if (tc.isNumberFormatSet()) {
setNumberFormat(tc.getNumberFormat());
}
@@ -344,6 +347,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
if (isNewBuiltinClassResolverSet() && !template.isNewBuiltinClassResolverSet()) {
template.setNewBuiltinClassResolver(getNewBuiltinClassResolver());
}
+ if (isTruncateBuiltinAlgorithmSet() && !template.isTruncateBuiltinAlgorithmSet()) {
+ template.setTruncateBuiltinAlgorithm(getTruncateBuiltinAlgorithm());
+ }
if (isNumberFormatSet() && !template.isNumberFormatSet()) {
template.setNumberFormat(getNumberFormat());
}
@@ -669,6 +675,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
|| isLogTemplateExceptionsSet()
|| isWrapUncheckedExceptionsSet()
|| isNewBuiltinClassResolverSet()
+ || isTruncateBuiltinAlgorithmSet()
|| isNumberFormatSet()
|| isObjectWrapperSet()
|| isOutputEncodingSet()
diff --git a/src/main/java/freemarker/core/TruncateBuiltinAlgorithm.java b/src/main/java/freemarker/core/TruncateBuiltinAlgorithm.java
new file mode 100644
index 0000000..b56b2dc
--- /dev/null
+++ b/src/main/java/freemarker/core/TruncateBuiltinAlgorithm.java
@@ -0,0 +1,143 @@
+/*
+ * 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 freemarker.core;
+
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateScalarModel;
+
+/**
+ * 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 TemplateScalarModel}) 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 TemplateScalarModel}. 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 TemplateScalarModel} (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 TemplateScalarModel truncate(
+ String s, int maxLength, TemplateScalarModel 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 TemplateScalarModel truncateW(
+ String s, int maxLength, TemplateScalarModel 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 TemplateScalarModel truncateC(
+ String s, int maxLength, TemplateScalarModel 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/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java b/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java
index 6475444..7e4e18b 100644
--- a/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java
+++ b/src/main/java/freemarker/core/_ObjectBuilderSettingEvaluator.java
@@ -688,7 +688,9 @@ public class _ObjectBuilderSettingEvaluator {
addWithSimpleName(SHORTHANDS, RTFOutputFormat.class);
addWithSimpleName(SHORTHANDS, PlainTextOutputFormat.class);
addWithSimpleName(SHORTHANDS, UndefinedOutputFormat.class);
-
+
+ addWithSimpleName(SHORTHANDS, DefaultTruncateBuiltinAlgorithm.class);
+
addWithSimpleName(SHORTHANDS, Locale.class);
SHORTHANDS.put("TimeZone", "freemarker.core._TimeZone");
SHORTHANDS.put("markup", "freemarker.core._Markup");
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index b7df2a0..acce261 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -13047,6 +13047,11 @@ grant codeBase "file:/path/to/freemarker.jar"
</listitem>
<listitem>
+ <para><link linkend="ref_builtin_truncate">truncate,
+ truncate_<replaceable>...</replaceable></link></para>
+ </listitem>
+
+ <listitem>
<para><link
linkend="ref_builtin_uncap_first">uncap_first</link></para>
</listitem>
@@ -14635,6 +14640,228 @@ foobar</programlisting>
<programlisting role="output">(green mouse)</programlisting>
</section>
+ <section xml:id="ref_builtin_truncate">
+ <title>truncate, truncate_...</title>
+
+ <indexterm>
+ <primary>truncate built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>truncate_c built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>truncate_wbuilt-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>truncate_m built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>truncate_c_m built-in</primary>
+ </indexterm>
+
+ <indexterm>
+ <primary>truncate_w_m built-in</primary>
+ </indexterm>
+
+ <para>Cuts off the end of a string if that's necessary to keep it
+ under a the length given as parameter, and appends a terminator
+ string (<literal>[...]</literal> by default) to indicate that the
+ string was truncated. Example (assuming default FreeMarker
+ configuration settings):</para>
+
+ <programlisting role="template"><#assign shortName='This is short'>
+<#assign longName='This is a too long name'>
+<#assign difficultName='This isoneveryverylongword'>
+
+No truncation needed:
+${shortName?truncate(16)}
+
+Truncated at word boundary:
+${longName?truncate(16)}
+
+Truncated at "character boundary":
+${difficultName?truncate(16)}</programlisting>
+
+ <programlisting role="output">No truncation needed:
+This is short
+
+Truncated at word boundary:
+This is a [...]
+
+Truncated at "character boundary":
+This isonev[...]</programlisting>
+
+ <para>Things to note above:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>The string is returned as is if its length doesn't exceed
+ the specified length (16 in this case).</para>
+ </listitem>
+
+ <listitem>
+ <para>When the string exceeded that length, its end was cut off
+ in a way so that together with the added terminator string
+ (<literal>[...]</literal> here) its length won't exceed 16. The
+ result length is possibly shorter than 16, for the sake of
+ better look (see later). Actually, the result length can also be
+ longer than the parameter length, when the desired length is
+ shorter than the terminator string alone, in which case the
+ terminator is still returned as is. Also, an algorithms other
+ than the default might choses to return a longer string, as the
+ length parameter is in principle just hint for the desired
+ visual length.</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>truncate</literal> prefers cutting at word
+ boundary, rather than mid-word, however, if doing so would give
+ a result that's shorter than the 75% of the length specified
+ with the argument, it falls back to cut mid-word. In the last
+ line of the above example, <quote>This [...]</quote> would be
+ too short (11 < 16 * 75%), so it was cut mid-word
+ instead.</para>
+ </listitem>
+
+ <listitem>
+ <para>If the cut happened at word boundary, there's a space
+ between the word end and the terminator string, otherwise
+ there's no space between them. Only whitespace is treated as
+ word separator, not punctuation, so this generally gives
+ intuitive results.</para>
+ </listitem>
+ </itemizedlist>
+
+ <simplesect>
+ <title>Adjusting truncation rules</title>
+
+ <para>Truncation rules are highly configurable by setting the
+ <literal>truncate_builtin_algorithm</literal> configuration
+ setting. This can be done by the programmers, not template
+ authors, so for more details and examples please see the JavaDoc
+ of <link
+ xlink:href="api/freemarker/core/Configurable.html#setTruncateBuiltinAlgorithm-freemarker.core.TruncateBuiltinAlgorithm-">Configurable.setTruncateBuiltinAlgorithm</link>.</para>
+
+ <para>Truncation rules can also be influenced right in the
+ template to a smaller extent:</para>
+
+ <itemizedlist>
+ <listitem>
+ <para>Specifying if the truncation should happen at word
+ boundary or not: </para>
+
+ <itemizedlist>
+ <listitem>
+ <para><literal>truncate_w</literal> will always truncate
+ at word boundary. For example,
+ <literal>difficultName?truncate_w(16)</literal> returns
+ <quote>This [...]</quote>, rather than <quote>This
+ isonev[...]</quote> (as saw in earlier example).</para>
+ </listitem>
+
+ <listitem>
+ <para><literal>truncate_c</literal> will truncate at any
+ character, not just at word ends. For example,
+ <literal>longName?truncate_c(16)</literal> returns
+ <quote>This is a t[...]</quote>, rather than <quote>This
+ is a [...]</quote> (as saw in earlier example). This tends
+ to give a string length closer to the length specified,
+ but still not an exact length, as it removes white-space
+ before the terminator string, and re-adds a space if we
+ are just after the end of a word, etc.</para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
+
+ <listitem>
+ <para>Specifying the terminator string (instead of relying on
+ its default): <literal>truncate</literal> and all
+ <literal>truncate_<replaceable>...</replaceable></literal>
+ built-ins have an additional optional parameter for it. After
+ that, a further optional parameter can specify the assumed
+ length of the terminator string (otherwise its real length
+ will be used). If you find yourself specifying the terminator
+ string often, then certainly the defaults should be configured
+ instead (via <literal>truncate_builtin_algorithm
+ configuration</literal> - see earlier). Example:</para>
+
+ <programlisting role="template">${longName?truncate(16, '...')}
+${longName?truncate(16, '...', 1)}</programlisting>
+
+ <programlisting role="output">This is a ...
+This is a too ...</programlisting>
+
+ <para>When the terminator string starts with dot
+ (<literal>.</literal>) or ellipsis (<literal>…</literal>), the
+ default algorithm will remove the dots and ellipses that the
+ terminator touches, to prevent ending up with more than 3 dots
+ at the end:</para>
+
+ <programlisting role="template">${'Foo bar.baaz'?truncate(11, '---')}
+${'Foo bar.baaz'?truncate(11, '...')} (Not "Foo bar....")
+${'Fo bar. baaz'?truncate(11, '...')} (Word separator space prevents touching)</programlisting>
+
+ <programlisting role="output">Foo bar.---
+Foo bar... (Not "Foo bar....")
+Fo bar. ... (Word separator space prevents touching)</programlisting>
+ </listitem>
+ </itemizedlist>
+ </simplesect>
+
+ <simplesect>
+ <title>Using markup as terminator string</title>
+
+ <para>Each truncation built-in has a variation whose name ends
+ with <literal>_m</literal> (for markup). These allow using markup
+ (like HTML) as terminator, which is useful if you want the
+ terminator to be styled differently than the truncated text. By
+ default the markup terminator is <literal><span
+ class='truncateTerminator'>[&#8230;]</span></literal>,
+ (where <literal>&#8230;</literal> prints an ellipsis
+ character), but of course this can be changed with the
+ <literal>truncate_builtin_algorithm</literal> configuration
+ setting (see earlier). Example (see the variables used in earlier
+ example):</para>
+
+ <programlisting role="template">${longName?truncate_m(16)}
+${difficultName?truncate_w_m(16)}
+${longName?truncate_c_m(16)}</programlisting>
+
+ <programlisting role="output">This is a <span class='truncateTerminator'>[&#8230;]</span>
+This <span class='truncateTerminator'>[&#8230;]</span>
+This is a to<span class='truncateTerminator'>[&#8230;]</span></programlisting>
+
+ <para>Note above that the terminator string was considered to be
+ only 3 characters long (<literal>'['</literal>,
+ <literal>'…'</literal>, <literal>']'</literal>) by the truncation
+ built-ins, because inside the terminator string they only count
+ the characters outside HTML/XML tags and comments, and they can
+ also interpret numeric character references (but not other entity
+ references). (The same applies when they decide if the terminator
+ starts with dot or ellipsis; preceding tags/comments are skipped,
+ etc.)</para>
+
+ <para>If a markup terminator is used (like above), the return
+ value of the
+ <literal>truncate<replaceable>...</replaceable>_m</literal>
+ built-in will be markup as well, which means that <link
+ linkend="dgui_misc_autoescaping">auto-escaping</link> won't escape
+ it. Of course, the content of the truncated string itself will be
+ still auto-escaped:</para>
+
+ <programlisting role="template"><#ftl output_format='HTML'>
+${'This is auto-escaped: <span>'}
+${'This is auto-escaped: <span>, but not the terminator string'?truncate_m(41)}</programlisting>
+
+ <programlisting role="output">This is auto-escaped: &lt;span&gt;
+This is auto-escaped: &lt;span&gt;, but not <span class='truncateTerminator'>[&#8230;]</span></programlisting>
+ </simplesect>
+ </section>
+
<section xml:id="ref_builtin_uncap_first">
<title>uncap_first</title>
@@ -27623,7 +27850,18 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
<itemizedlist>
<listitem>
- <para>[FIXME]</para>
+ <para>Added new built-ins for truncating text.
+ <literal><replaceable>string</replaceable>?truncate(<replaceable>length</replaceable>)</literal>
+ truncates the text to the given length, and by default adds
+ <literal>[...]</literal> at the end if truncation has happened.
+ Truncation happens at word boundaries, unless the result is too
+ short that way, in which case it falls back to truncation mid
+ word. There's also <literal>?truncate_w</literal> to force Word
+ Boundary truncation, and <literal>?truncate_c</literal> (for
+ Character Boundary) that doesn't care about word boundaries. The
+ truncation algorithm is pluggable in the FreeMarker
+ configuration. See <link linkend="ref_builtin_truncate">the
+ reference</link> for more details.</para>
</listitem>
</itemizedlist>
</section>
diff --git a/src/test/java/freemarker/core/DefaultTruncateBuiltinAlgorithmTest.java b/src/test/java/freemarker/core/DefaultTruncateBuiltinAlgorithmTest.java
new file mode 100644
index 0000000..533b3aa
--- /dev/null
+++ b/src/test/java/freemarker/core/DefaultTruncateBuiltinAlgorithmTest.java
@@ -0,0 +1,669 @@
+/*
+ * 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 freemarker.core;
+
+import static freemarker.core.DefaultTruncateBuiltinAlgorithm.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.SimpleNumber;
+import freemarker.template.SimpleScalar;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+
+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 (TemplateModelException 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 SimpleScalar("."), 1, env);
+
+ try {
+ ASCII_INSTANCE.truncate("", -1, new SimpleScalar("."), 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 SimpleScalar("."), -1, env);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), containsString("terminatorLength"));
+ }
+ }
+
+ private Environment createEnvironment() {
+ try {
+ return new Template("", "", new Configuration(Configuration.VERSION_2_3_28)).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(TemplateScalarModel.class));
+ assertEquals(alg.getDefaultTerminator(), ((TemplateScalarModel) truncated).getAsString());
+ }
+ SimpleScalar stringTerminator = new SimpleScalar("|");
+ 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 {
+ TemplateScalarModel 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 {
+ TemplateScalarModel 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 {
+ TemplateScalarModel 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();
+ TemplateScalarModel ellipsis = new SimpleScalar("\u2026");
+ TemplateScalarModel squEllipsis = new SimpleScalar("[\u2026]");
+
+ // Length detection
+ {
+ TemplateScalarModel actual = ASCII_INSTANCE.truncate("abcd", 3, ellipsis, null, env);
+ assertEquals("ab\u2026", actual.getAsString());
+ }
+ {
+ TemplateScalarModel actual = ASCII_INSTANCE.truncate("abcdef", 5, squEllipsis, null, env);
+ assertEquals("ab[\u2026]", actual.getAsString());
+ }
+ {
+ TemplateScalarModel actual = ASCII_INSTANCE.truncate("abcdef", 5, squEllipsis, 1, env);
+ assertEquals("abcd[\u2026]", actual.getAsString());
+ }
+
+ // Dot removal
+ {
+ TemplateScalarModel actual = ASCII_INSTANCE.truncate("a.cd", 3, ellipsis, null, env);
+ assertEquals("a\u2026", actual.getAsString());
+ }
+ {
+ TemplateScalarModel actual = ASCII_INSTANCE.truncate("a.cdef", 5, squEllipsis, null, env);
+ assertEquals("a.[\u2026]", actual.getAsString());
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/core/TemplateConfigurationTest.java b/src/test/java/freemarker/core/TemplateConfigurationTest.java
index 76199ad..5be84d7 100644
--- a/src/test/java/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/freemarker/core/TemplateConfigurationTest.java
@@ -180,6 +180,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("tagSyntax", Configuration.SQUARE_BRACKET_TAG_SYNTAX);
diff --git a/src/test/java/freemarker/core/TruncateBuiltInTest.java b/src/test/java/freemarker/core/TruncateBuiltInTest.java
new file mode 100644
index 0000000..2cfe037
--- /dev/null
+++ b/src/test/java/freemarker/core/TruncateBuiltInTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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 freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModelException;
+import freemarker.test.TemplateTest;
+
+public class TruncateBuiltInTest extends TemplateTest {
+
+ private static final String M_TERM_SRC = "<span class=trunc>&hellips;</span>";
+
+ @Override
+ protected Configuration createConfiguration() throws Exception {
+ Configuration cfg = super.createConfiguration();
+ cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
+ return cfg;
+ }
+
+ @Before
+ public void setup() throws TemplateModelException {
+ 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)}", "#2", "string", "markup");
+ assertErrorContains("${t?truncate(-1)}", "#1", "negative");
+ assertErrorContains("${t?truncate(200, 'x', -1)}", "#3", "negative");
+ }
+
+ @Test
+ public void testTruncateM() throws IOException, TemplateException {
+ assertOutput("${t?truncateM(15)}", "Some text <span class='truncateTerminator'>[…]</span>"); // String arg allowed...
+ assertOutput("${t?truncate_m(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?truncate_c(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)}", "#2", "string", "markup");
+
+ assertOutput("${t?truncateC(20)?isMarkupOutput?c}", "false");
+ }
+
+ @Test
+ public void testTruncateCM() throws IOException, TemplateException {
+ assertOutput("${t?truncate_c_m(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?truncate_w(20)}", "Some text for [...]");
+ assertOutput("${t?truncateW(20)}", "Some text for [...]");
+ assertOutput("${u?truncateW(20)}", "[...]"); // Proof of no fallback to C
+
+ assertErrorContains("${t?truncateW(200, mTerm)}", "#2", "string", "markup");
+
+ assertOutput("${t?truncateW(20)?isMarkupOutput?c}", "false");
+ assertOutput("${t?truncateW(20, '|')?isMarkupOutput?c}", "false");
+ }
+
+ @Test
+ public void testTruncateWM() throws IOException, TemplateException {
+ assertOutput("${t?truncate_w_m(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[...]");
+ getConfiguration().setTruncateBuiltinAlgorithm(DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE);
+ 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>");
+ getConfiguration().setTruncateBuiltinAlgorithm(new DefaultTruncateBuiltinAlgorithm(
+ "|...", HTMLOutputFormat.INSTANCE.fromMarkup(M_TERM_SRC), true));
+ assertOutput("${t?truncate(20)}", "Some text for |...");
+ assertOutput("${t?truncateM(20)}", "Some text for " + M_TERM_SRC);
+ }
+
+}
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index f3e8be9..cd71ca1 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -59,6 +59,7 @@ import freemarker.core.Configurable.SettingValueAssignmentException;
import freemarker.core.Configurable.UnknownSettingException;
import freemarker.core.ConfigurableTest;
import freemarker.core.CustomHTMLOutputFormat;
+import freemarker.core.DefaultTruncateBuiltinAlgorithm;
import freemarker.core.DummyOutputFormat;
import freemarker.core.Environment;
import freemarker.core.EpochMillisDivTemplateDateFormatFactory;
@@ -1801,7 +1802,67 @@ public class ConfigurationTest extends TestCase {
cfg.setSetting(Configuration.NEW_BUILTIN_CLASS_RESOLVER_KEY_SNAKE_CASE, "allows_nothing");
assertSame(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER, cfg.getNewBuiltinClassResolver());
}
-
+
+ public void testTruncateBuiltinAlgorithm() throws TemplateException {
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
+ assertSame(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE, cfg.getTruncateBuiltinAlgorithm());
+
+ cfg.setSetting("truncateBuiltinAlgorithm", "unicodE");
+ assertSame(DefaultTruncateBuiltinAlgorithm.UNICODE_INSTANCE, cfg.getTruncateBuiltinAlgorithm());
+
+ cfg.setSetting("truncate_builtin_algorithm", "ASCII");
+ assertSame(DefaultTruncateBuiltinAlgorithm.ASCII_INSTANCE, cfg.getTruncateBuiltinAlgorithm());
+
+ {
+ cfg.setSetting("truncate_builtin_algorithm",
+ "DefaultTruncateBuiltinAlgorithm('...', false)");
+ DefaultTruncateBuiltinAlgorithm alg =
+ (DefaultTruncateBuiltinAlgorithm) cfg.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());
+ }
+
+ {
+ cfg.setSetting("truncate_builtin_algorithm",
+ "DefaultTruncateBuiltinAlgorithm(" +
+ "'...', " +
+ "markup(HTMLOutputFormat(), '<span class=trunc>...</span>'), " +
+ "true)");
+ DefaultTruncateBuiltinAlgorithm alg =
+ (DefaultTruncateBuiltinAlgorithm) cfg.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());
+ }
+
+ {
+ cfg.setSetting("truncate_builtin_algorithm",
+ "DefaultTruncateBuiltinAlgorithm(" +
+ "DefaultTruncateBuiltinAlgorithm.STANDARD_ASCII_TERMINATOR, null, null, " +
+ "DefaultTruncateBuiltinAlgorithm.STANDARD_M_TERMINATOR, null, null, " +
+ "true, 0.5)");
+ DefaultTruncateBuiltinAlgorithm alg =
+ (DefaultTruncateBuiltinAlgorithm) cfg.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());
+ }
+ }
+
@Test
public void testGetSettingNamesAreSorted() throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);