FREEMARKER-219: The truncate family of built-ins, as in maybeLong?truncate(10, ''), if the terminator string is set to 0 length, now it will not add a space before the terminator string when the cut happened exactly after the end of a word. Also, improved truncate-related documentation.
diff --git a/freemarker-core/src/main/java/freemarker/core/Configurable.java b/freemarker-core/src/main/java/freemarker/core/Configurable.java
index 1882025..fc98db5 100644
--- a/freemarker-core/src/main/java/freemarker/core/Configurable.java
+++ b/freemarker-core/src/main/java/freemarker/core/Configurable.java
@@ -1703,10 +1703,10 @@
     }
 
     /**
-     * Specifies the algorithm used for {@code ?truncate}. Defaults to
+     * Specifies the algorithm used for {@code ?truncate}, {@code ?truncate_w}, and {@code ?truncate_c}. 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.
+     * creating a new {@link DefaultTruncateBuiltinAlgorithm} with the proper constructor parameters. Otherwise, users
+     * may 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)}.
diff --git a/freemarker-core/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java b/freemarker-core/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java
index 99625b5..1a0eda7 100644
--- a/freemarker-core/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java
+++ b/freemarker-core/src/main/java/freemarker/core/DefaultTruncateBuiltinAlgorithm.java
@@ -143,9 +143,9 @@
      * @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.
+     *            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
+     *            The assumed length of {@code defaultTerminator}, or {@code null} if the assumed length is simply
      *            {@code defaultTerminator.length()}.
      * @param defaultTerminatorRemovesDots
      *            Whether dots and ellipsis characters that the {@code defaultTerminator} touches should be removed. If
@@ -157,8 +157,12 @@
      *            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}.
+     *            The assumed length of the terminator, or {@code null} if the assumed length will be
+     *            {@link #getMTerminatorLength(TemplateMarkupOutputModel)}. Note that if you have HTML tags, or entity
+     *            references in the {@code defaultMTerminator}, then the visual length differs from the string length,
+     *            and {@link #getMTerminatorLength(TemplateMarkupOutputModel)} accounts for these complications to an
+     *            extent, but it for example it won't know what CSS does, or if the nested content of some HTML elements
+     *            are not displayed.
      * @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
@@ -168,17 +172,17 @@
      * @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
+     *            instead of "too long[...]". When the truncation happens inside a word, this has no 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
+     *            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
+     *            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 always be accepted, even if it's
      *            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.
@@ -355,7 +359,7 @@
      *
      * <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
+     * 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.
      */
@@ -394,9 +398,6 @@
                 : true;
     }
 
-    /**
-     * Deals with both CB and WB truncation, hence it's unified.
-     */
     private TemplateModel unifiedTruncate(
             String s, int maxLength,
             TemplateModel terminator, Integer terminatorLength,
@@ -437,7 +438,7 @@
                 terminator, terminatorLength, terminatorRemovesDots,
                 mode);
 
-        // The terminator is always shown, even if with that we exceed maxLength. Otherwise the user couldn't
+        // 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;
@@ -447,7 +448,7 @@
             truncatedS.append(((TemplateScalarModel) terminator).getAsString());
             return new SimpleScalar(truncatedS.toString());
         } else if (terminator instanceof TemplateMarkupOutputModel) {
-            TemplateMarkupOutputModel markup = (TemplateMarkupOutputModel) terminator;
+            TemplateMarkupOutputModel markup = (TemplateMarkupOutputModel<?>) terminator;
             MarkupOutputFormat outputFormat = markup.getOutputFormat();
             return outputFormat.concat(outputFormat.fromPlainTextByEscaping(truncatedS.toString()), markup);
         } else {
@@ -470,6 +471,8 @@
             return null;
         }
 
+        boolean addSpaceAtWordBoundary = this.addSpaceAtWordBoundary && terminatorLength != 0;
+
         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.
@@ -527,7 +530,7 @@
 
         // 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)) {
+        if (cbLastCIdx == cbInitialLastCIdx && addSpaceAtWordBoundary && isWordEnd(s, cbLastCIdx)) {
             cbLastCIdx--;
             if (cbLastCIdx < 0) {
                 return null;
diff --git a/freemarker-core/src/test/java/freemarker/core/TruncateBuiltInTest.java b/freemarker-core/src/test/java/freemarker/core/TruncateBuiltInTest.java
index 2cfe037..9929151 100644
--- a/freemarker-core/src/test/java/freemarker/core/TruncateBuiltInTest.java
+++ b/freemarker-core/src/test/java/freemarker/core/TruncateBuiltInTest.java
@@ -71,7 +71,8 @@
 
     @Test
     public void testTruncateM() throws IOException, TemplateException {
-        assertOutput("${t?truncateM(15)}", "Some text <span class='truncateTerminator'>[&#8230;]</span>"); // String arg allowed...
+        assertOutput("${t?truncateM(15)}",
+                "Some text <span class='truncateTerminator'>[&#8230;]</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);
@@ -150,4 +151,20 @@
         assertOutput("${t?truncateM(20)}", "Some text for " + M_TERM_SRC);
     }
 
-}
+    @Test
+    public void testJiraIssueFREEMARKER219() throws IOException, TemplateException {
+        assertOutput("${'1 3'?truncate_c(2, '|')}", "|");
+        assertOutput("${' 2 '?truncate_c(2, '|')}", "|");
+        assertOutput("${'1 '?truncate_c(1, '|')}", "|");
+        assertOutput("${' 2'?truncate_c(1, '|')}", "|");
+        assertOutput("${'1234 SOMESTREETSSS AVE NE 123'?truncate_c(25, '|')}", "1234 SOMESTREETSSS AVE N|");
+
+        assertOutput("${'1 3'?truncate_c(2, '')}", "1");
+        assertOutput("${' 2 '?truncate_c(2, '')}", " 2");
+        assertOutput("${'1 '?truncate_c(1, '')}", "1");
+        assertOutput("${' 2'?truncate_c(1, '')}", "");
+        assertOutput("${'1234 SOMESTREETSSS AVE NE 123'?truncate_c(25, '')}", "1234 SOMESTREETSSS AVE NE");
+        assertOutput("${'1234 SOMESTREETSSS AVE NE 123'?truncate_c(24, '')}", "1234 SOMESTREETSSS AVE N");
+        assertOutput("${'1234 SOMESTREETSSS AVE NE 123'?truncate_c(23, '')}", "1234 SOMESTREETSSS AVE");
+    }
+}
\ No newline at end of file
diff --git a/freemarker-manual/src/main/docgen/en_US/book.xml b/freemarker-manual/src/main/docgen/en_US/book.xml
index fa06667..fce81f3 100644
--- a/freemarker-manual/src/main/docgen/en_US/book.xml
+++ b/freemarker-manual/src/main/docgen/en_US/book.xml
@@ -15115,11 +15115,27 @@
             <primary>truncate_w_m built-in</primary>
           </indexterm>
 
+          <note>
+            <para>If you just want to limit the length of string with
+            straightforward behavior, then do not use this built in, but the
+            <link linkend="dgui_template_exp_seqenceop_slice">sequence
+            slicing</link>, and <link
+            linkend="dgui_template_exp_direct_ranges">..* length limited
+            range</link> operators. For example, <literal>s[0 ..*
+            10]</literal> will give the first 10 characters of
+            <literal>s</literal>, if <literal>s</literal> is longer than that,
+            otherwise it just gives <literal>s</literal> as is. While
+            <literal>s?truncate(10, '')</literal> expresses similar intent, it
+            has complicated rules to give a result that looks nicer for
+            humans, like it trims the right side at the cut, and sometimes
+            cuts a bit early to avoid cutting into the last word.</para>
+          </note>
+
           <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>
+          under 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">&lt;#assign shortName='This is short'&gt;
 &lt;#assign longName='This is a too long name'&gt;
@@ -15143,7 +15159,7 @@
 Truncated at "character boundary":
 This isonev[...]</programlisting>
 
-          <para>Things to note above:</para>
+          <para>Notes on some tricky aspects for truncation:</para>
 
           <itemizedlist>
             <listitem>
@@ -15159,9 +15175,9 @@
               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
+              terminator is still returned as is. Also, an algorithm other
               than the default might choses to return a longer string, as the
-              length parameter is in principle just hint for the desired
+              length parameter is in principle just a hint for the desired
               visual length.</para>
             </listitem>
 
@@ -15180,7 +15196,21 @@
               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>
+              intuitive results. (Except, if the terminator string is set to
+              be 0 length, no space is added before it, starting from
+              FreeMarker 2.3.33.)</para>
+            </listitem>
+
+            <listitem>
+              <para>Before adding the terminator string (possibly with a word
+              boundary space before it, as explained above) after the string
+              whose length was already cut, trailing whitespace is removed
+              from that. For example <literal>'1
+              67890A'?truncate(10)</literal>, where there are 4 spaces between
+              the <literal>1</literal> and <literal>6</literal>, will give
+              <quote><literal>1 [...]</literal></quote> (7 characters), not
+              <quote><literal>1 [...]</literal></quote> (10
+              characters).</para>
             </listitem>
           </itemizedlist>
 
@@ -15220,7 +15250,11 @@
                     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>
+                    are just after the end of a word, etc. (Except, space is
+                    not re-added if the terminator string is set to be 0
+                    length, starting from FreeMarker 2.3.33.) If you need
+                    exact length, simply use <literal>longName[0 ..*
+                    16]</literal>.</para>
                   </listitem>
                 </itemizedlist>
               </listitem>
@@ -15229,11 +15263,11 @@
                 <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
+                built-ins have an optional 2nd parameter for that. 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>
 
@@ -30143,6 +30177,20 @@
 
             <listitem>
               <para><link
+              xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-219">FREEMARKER-219</link>:
+              The <link linkend="ref_builtin_truncate"><quote>truncate</quote>
+              family of built-ins</link>, as in
+              <literal>maybeLong?truncate(10, '')</literal>, if the terminator
+              string is set to 0 length, now it will not add a space before
+              the terminator string when the cut happened exactly after the
+              end of a word. (Note that if you are using something like
+              <literal>maybeLong?truncate_c(10, '')</literal>, then certainly
+              what you really want is <literal>maybeLong[0 ..* 10]</literal>,
+              as that doesn't do trimming at the cut.)</para>
+            </listitem>
+
+            <listitem>
+              <para><link
               xlink:href="https://github.com/apache/freemarker/pull/89">GitHub
               PR 89</link>: Added <literal>TemplateProcessingTracer</literal>
               mechanism, that can be used to monitor coverage, and performance