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 2016/06/12 16:53:45 UTC

[04/50] incubator-freemarker git commit: Fixed FREEMARKER-19: Tab size defaults to 8 again. Also added tabSize configuration setting to change that. With tabSize set to 1, the column number equals to the number of characters in the line until and includi

Fixed FREEMARKER-19: Tab size defaults to 8 again. Also added tabSize configuration setting to change that. With tabSize set to 1, the column number equals to the number of characters in the line until and including the pointer characters. Also Template.getSource keeps the tab characters as is in the last case.


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/5141bcd0
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/5141bcd0
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/5141bcd0

Branch: refs/heads/2.3
Commit: 5141bcd0c28575d9d8825521ce9abcbc0f361018
Parents: 27fc644
Author: ddekany <dd...@apache.org>
Authored: Sun Apr 10 20:44:14 2016 +0200
Committer: ddekany <dd...@apache.org>
Committed: Sun Apr 10 20:44:14 2016 +0200

----------------------------------------------------------------------
 .../freemarker/core/BuiltInsForStringsMisc.java | 13 ++--
 .../LegacyConstructorParserConfiguration.java   | 19 ++++-
 .../freemarker/core/ParserConfiguration.java    |  7 ++
 .../java/freemarker/core/StringLiteral.java     | 16 ++--
 .../freemarker/core/TemplateConfiguration.java  | 38 +++++++++-
 ..._ParserConfigurationWithInheritedFormat.java |  4 +
 .../java/freemarker/template/Configuration.java | 44 +++++++++++
 src/main/java/freemarker/template/Template.java | 24 ++++--
 src/main/javacc/FTL.jj                          | 12 ++-
 src/manual/en_US/book.xml                       | 26 ++++++-
 src/test/java/freemarker/core/TabSizeTest.java  | 79 ++++++++++++++++++++
 .../core/TemplateConfigurationTest.java         | 16 +++-
 .../freemarker/template/ConfigurationTest.java  | 54 +++++++++++++
 .../java/freemarker/template/GetSourceTest.java | 35 +++++++++
 14 files changed, 361 insertions(+), 26 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
index 3d21c0a..1b67eed 100644
--- a/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
+++ b/src/main/java/freemarker/core/BuiltInsForStringsMisc.java
@@ -62,14 +62,17 @@ class BuiltInsForStringsMisc {
             Expression exp = null;
             try {
                 try {
+                    ParserConfiguration pCfg = parentTemplate.getParserConfiguration();
+                    
+                    SimpleCharStream simpleCharStream = new SimpleCharStream(
+                            new StringReader("(" + s + ")"),
+                            RUNTIME_EVAL_LINE_DISPLACEMENT, 1,
+                            s.length() + 2);
+                    simpleCharStream.setTabSize(pCfg.getTabSize());
                     FMParserTokenManager tkMan = new FMParserTokenManager(
-                            new SimpleCharStream(
-                                    new StringReader("(" + s + ")"),
-                                    RUNTIME_EVAL_LINE_DISPLACEMENT, 1,
-                                    s.length() + 2));
+                            simpleCharStream);
                     tkMan.SwitchTo(FMParserConstants.FM_EXPRESSION);
 
-                    ParserConfiguration pCfg = parentTemplate.getParserConfiguration();
                     // pCfg.outputFormat is exceptional: it's inherited from the lexical context
                     if (pCfg.getOutputFormat() != outputFormat) {
                         pCfg = new _ParserConfigurationWithInheritedFormat(

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java b/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
index 1f95dac..2ef3b89 100644
--- a/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
+++ b/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java
@@ -34,12 +34,13 @@ class LegacyConstructorParserConfiguration implements ParserConfiguration {
     private ArithmeticEngine arithmeticEngine;
     private Integer autoEscapingPolicy; 
     private OutputFormat outputFormat;
-    private Boolean recognizeStandardFileExtensions; 
+    private Boolean recognizeStandardFileExtensions;
+    private Integer tabSize;
     private final Version incompatibleImprovements;
 
     public LegacyConstructorParserConfiguration(boolean strictSyntaxMode, boolean whitespaceStripping, int tagSyntax,
             int namingConvention, Integer autoEscaping, OutputFormat outputFormat,
-            Boolean recognizeStandardFileExtensions,
+            Boolean recognizeStandardFileExtensions, Integer tabSize,
             Version incompatibleImprovements, ArithmeticEngine arithmeticEngine) {
         this.tagSyntax = tagSyntax;
         this.namingConvention = namingConvention;
@@ -48,6 +49,7 @@ class LegacyConstructorParserConfiguration implements ParserConfiguration {
         this.autoEscapingPolicy = autoEscaping;
         this.outputFormat = outputFormat;
         this.recognizeStandardFileExtensions = recognizeStandardFileExtensions;
+        this.tabSize = tabSize;
         this.incompatibleImprovements = incompatibleImprovements;
         this.arithmeticEngine = arithmeticEngine;
     }
@@ -124,4 +126,17 @@ class LegacyConstructorParserConfiguration implements ParserConfiguration {
         }
     }
 
+    public int getTabSize() {
+        if (tabSize == null) {
+            throw new IllegalStateException();
+        }
+        return tabSize.intValue();
+    }
+    
+    void setTabSizeIfNotSet(int tabSize) {
+        if (this.tabSize == null) {
+            this.tabSize = Integer.valueOf(tabSize);
+        }
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/core/ParserConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/ParserConfiguration.java b/src/main/java/freemarker/core/ParserConfiguration.java
index bae3b9a..4108952 100644
--- a/src/main/java/freemarker/core/ParserConfiguration.java
+++ b/src/main/java/freemarker/core/ParserConfiguration.java
@@ -74,5 +74,12 @@ public interface ParserConfiguration {
      * See {@link Configuration#getIncompatibleImprovements()}.
      */
     Version getIncompatibleImprovements();
+    
+    /**
+     * See {@link Configuration#getTabSize()}.
+     * 
+     * @since 2.3.25
+     */
+    int getTabSize();
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/core/StringLiteral.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/StringLiteral.java b/src/main/java/freemarker/core/StringLiteral.java
index aa32d3e..da1c1bd 100644
--- a/src/main/java/freemarker/core/StringLiteral.java
+++ b/src/main/java/freemarker/core/StringLiteral.java
@@ -46,20 +46,24 @@ final class StringLiteral extends Expression implements TemplateScalarModel {
      *            this to share the {@code namingConvetion} with that.
      */
     void parseValue(FMParserTokenManager parentTkMan, OutputFormat outputFormat) throws ParseException {
-        // The way this work is incorrect (the literal should be parsed without un-escaping),
+        // The way this works is incorrect (the literal should be parsed without un-escaping),
         // but we can't fix this backward compatibly.
         if (value.length() > 3 && (value.indexOf("${") >= 0 || value.indexOf("#{") >= 0)) {
             
             Template parentTemplate = getTemplate();
+            ParserConfiguration pcfg = parentTemplate.getParserConfiguration();
 
             try {
+                SimpleCharStream simpleCharacterStream = new SimpleCharStream(
+                        new StringReader(value),
+                        beginLine, beginColumn + 1,
+                        value.length());
+                simpleCharacterStream.setTabSize(pcfg.getTabSize());
+                
                 FMParserTokenManager tkMan = new FMParserTokenManager(
-                        new SimpleCharStream(
-                                new StringReader(value),
-                                beginLine, beginColumn + 1,
-                                value.length()));
+                        simpleCharacterStream);
                 
-                FMParser parser = new FMParser(parentTemplate, false, tkMan, parentTemplate.getParserConfiguration());
+                FMParser parser = new FMParser(parentTemplate, false, tkMan, pcfg);
                 // We continue from the parent parser's current state:
                 parser.setupStringLiteralMode(parentTkMan, outputFormat);
                 try {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/core/TemplateConfiguration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java
index 50ebc8f..30484dc 100644
--- a/src/main/java/freemarker/core/TemplateConfiguration.java
+++ b/src/main/java/freemarker/core/TemplateConfiguration.java
@@ -83,6 +83,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
     private Boolean recognizeStandardFileExtensions;
     private OutputFormat outputFormat;
     private String encoding;
+    private Integer tabSize;
 
     /**
      * Creates a new instance. The parent will be {@link Configuration#getDefaultConfiguration()} initially, but it will
@@ -238,17 +239,24 @@ public final class TemplateConfiguration extends Configurable implements ParserC
         if (tc.isWhitespaceStrippingSet()) {
             setWhitespaceStripping(tc.getWhitespaceStripping());
         }
+        if (tc.isTabSizeSet()) {
+            setTabSize(tc.getTabSize());
+        }
         
         tc.copyDirectCustomAttributes(this, true);
     }
 
     /**
-     * Sets the settings of the {@link Template} which are not yet set in the {@link Template} and are set in this
+     * Sets those settings of the {@link Template} which aren't yet set in the {@link Template} and are set in this
      * {@link TemplateConfiguration}, leaves the other settings as is. A setting is said to be set in a
      * {@link TemplateConfiguration} or {@link Template} if it was explicitly set via a setter method on that object, as
      * opposed to be inherited from the {@link Configuration}.
      * 
      * <p>
+     * Note that this method doesn't deal with settings that influence the parser, as those are already baked in at this
+     * point via the {@link ParserConfiguration}. 
+     * 
+     * <p>
      * Note that the {@code encoding} setting of the {@link Template} counts as unset if it's {@code null},
      * even if {@code null} was set via {@link Template#setEncoding(String)}.
      *
@@ -522,6 +530,34 @@ public final class TemplateConfiguration extends Configurable implements ParserC
     }
     
     /**
+     * See {@link Configuration#setTabSize(int)}. 
+     * 
+     * @since 2.3.25
+     */
+    public void setTabSize(int tabSize) {
+        this.tabSize = Integer.valueOf(tabSize);
+    }
+
+    /**
+     * Getter pair of {@link #setTabSize(int)}.
+     * 
+     * @since 2.3.25
+     */
+    public int getTabSize() {
+        return tabSize != null ? tabSize.intValue()
+                : getParentConfiguration().getTabSize();
+    }
+    
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     * 
+     * @since 2.3.25
+     */
+    public boolean isTabSizeSet() {
+        return tabSize != null;
+    }    
+    
+    /**
      * Returns {@link Configuration#getIncompatibleImprovements()} from the parent {@link Configuration}. This mostly
      * just exist to satisfy the {@link ParserConfiguration} interface.
      * 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java b/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
index c479f45..9acaa54 100644
--- a/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
+++ b/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java
@@ -71,4 +71,8 @@ public final class _ParserConfigurationWithInheritedFormat implements ParserConf
     public ArithmeticEngine getArithmeticEngine() {
         return wrappedPCfg.getArithmeticEngine();
     }
+
+    public int getTabSize() {
+        return wrappedPCfg.getTabSize();
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/template/Configuration.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index e48d6b5..99045fb 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -263,6 +263,13 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     public static final String NAMING_CONVENTION_KEY_CAMEL_CASE = "namingConvention";
     /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
     public static final String NAMING_CONVENTION_KEY = NAMING_CONVENTION_KEY_SNAKE_CASE;
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.25 */
+    public static final String TAB_SIZE_KEY_SNAKE_CASE = "tab_size";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.25 */
+    public static final String TAB_SIZE_KEY_CAMEL_CASE = "tabSize";
+    /** Alias to the {@code ..._SNAKE_CASE} variation. @since 2.3.25 */
+    public static final String TAB_SIZE_KEY = TAB_SIZE_KEY_SNAKE_CASE;
     
     /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
     public static final String TEMPLATE_LOADER_KEY_SNAKE_CASE = "template_loader";
@@ -322,6 +329,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_SNAKE_CASE,
         REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_SNAKE_CASE,
         STRICT_SYNTAX_KEY_SNAKE_CASE,
+        TAB_SIZE_KEY_SNAKE_CASE,
         TAG_SYNTAX_KEY_SNAKE_CASE,
         TEMPLATE_CONFIGURATIONS_KEY_SNAKE_CASE,
         TEMPLATE_LOADER_KEY_SNAKE_CASE,
@@ -347,6 +355,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE,
         REGISTERED_CUSTOM_OUTPUT_FORMATS_KEY_CAMEL_CASE,
         STRICT_SYNTAX_KEY_CAMEL_CASE,
+        TAB_SIZE_KEY_CAMEL_CASE,
         TAG_SYNTAX_KEY_CAMEL_CASE,
         TEMPLATE_CONFIGURATIONS_KEY_CAMEL_CASE,
         TEMPLATE_LOADER_KEY_CAMEL_CASE,
@@ -496,6 +505,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     private Version incompatibleImprovements;
     private int tagSyntax = ANGLE_BRACKET_TAG_SYNTAX;
     private int namingConvention = AUTO_DETECT_NAMING_CONVENTION;
+    private int tabSize = 8;  // Default from JavaCC 3.x 
 
     private TemplateCache cache;
     
@@ -2237,6 +2247,38 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     }
     
     /**
+     * Sets the assumed display width of the tab character (ASCII 9), which influences the column number shown in error
+     * messages (or the column number you get through other API-s). So for example if the users edit templates in an
+     * editor where the tab width is set to 4, you should set this to 4 so that the column numbers printed by FreeMarker
+     * will match the column number shown in the editor. This setting doesn't affect the output of templates, as a tab
+     * in the template will remain a tab in the output too.
+     * 
+     * @param tabSize
+     *            At least 1, at most 256.
+     * 
+     * @since 2.3.25
+     */
+    public void setTabSize(int tabSize) {
+        if (tabSize < 1) {
+           throw new IllegalArgumentException("\"tabSize\" must be at least 1, but was " + tabSize);
+        }
+        // To avoid integer overflows:
+        if (tabSize > 256) {
+            throw new IllegalArgumentException("\"tabSize\" can be more than 256, but was " + tabSize);
+        }
+        this.tabSize = tabSize;
+    }
+
+    /**
+     * The getter pair of {@link #setTabSize(int)}.
+     * 
+     * @since 2.3.25
+     */
+    public int getTabSize() {
+        return tabSize;
+    }
+    
+    /**
      * Retrieves the template with the given name from the template cache, loading it into the cache first if it's
      * missing/staled.
      * 
@@ -2928,6 +2970,8 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 } else {
                     throw invalidSettingValueException(name, value);
                 }
+            } else if (TAB_SIZE_KEY_SNAKE_CASE.equals(name) || TAB_SIZE_KEY_CAMEL_CASE.equals(name)) {
+                setTabSize(Integer.parseInt(value));
             } else if (INCOMPATIBLE_IMPROVEMENTS_KEY_SNAKE_CASE.equals(name)
                     || INCOMPATIBLE_IMPROVEMENTS_KEY_CAMEL_CASE.equals(name)) {
                 setIncompatibleImprovements(new Version(value));

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/java/freemarker/template/Template.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/Template.java b/src/main/java/freemarker/template/Template.java
index f785768..1da3aaf 100644
--- a/src/main/java/freemarker/template/Template.java
+++ b/src/main/java/freemarker/template/Template.java
@@ -238,14 +238,16 @@ public class Template extends Configurable {
         this.setEncoding(encoding);
         LineTableBuilder ltbReader;
         try {
+            ParserConfiguration actualParserConfiguration = getParserConfiguration();
+            
             if (!(reader instanceof BufferedReader) && !(reader instanceof StringReader)) {
                 reader = new BufferedReader(reader, 0x1000);
             }
-            ltbReader = new LineTableBuilder(reader);
+            ltbReader = new LineTableBuilder(reader, actualParserConfiguration);
             reader = ltbReader;
             
             try {
-                parser = new FMParser(this, reader, getParserConfiguration());
+                parser = new FMParser(this, reader, actualParserConfiguration);
                 try {
                     this.rootElement = parser.Root();
                 } catch (IndexOutOfBoundsException exc) {
@@ -572,8 +574,8 @@ public class Template extends Configurable {
     
     /**
      * Returns the {@link ParserConfiguration} that was used for parsing this template. This is most often the same
-     * object as {@link #getConfiguration()}, but sometimes it's a {@link TemplateConfiguration}, or something else. It's
-     * never {@code null}.
+     * object as {@link #getConfiguration()}, but sometimes it's a {@link TemplateConfiguration}, or something else.
+     * It's never {@code null}.
      * 
      * @since 2.3.24
      */
@@ -735,10 +737,16 @@ public class Template extends Configurable {
 
     /**
      * Returns the template source at the location specified by the coordinates given, or {@code null} if unavailable.
+     * A strange legacy in the behavior of this method is that it replaces tab characters with spaces according the
+     * value of {@link Template#getParserConfiguration()}/{@link ParserConfiguration#getTabSize()} (which usually
+     * comes from {@link Configuration#getTabSize()}), because tab characters move the column number with more than
+     * 1 in error messages. However, if you set the tab size to 1, this method leaves the tab characters as is.
+     * 
      * @param beginColumn the first column of the requested source, 1-based
      * @param beginLine the first line of the requested source, 1-based
      * @param endColumn the last column of the requested source, 1-based
      * @param endLine the last line of the requested source, 1-based
+     * 
      * @see freemarker.core.TemplateObject#getSource()
      */
     public String getSource(int beginColumn,
@@ -771,6 +779,7 @@ public class Template extends Configurable {
      */
     private class LineTableBuilder extends FilterReader {
         
+        private final int tabSize;
         private final StringBuilder lineBuf = new StringBuilder();
         int lastChar;
         boolean closed;
@@ -781,8 +790,9 @@ public class Template extends Configurable {
         /**
          * @param r the character stream to wrap
          */
-        LineTableBuilder(Reader r) {
+        LineTableBuilder(Reader r, ParserConfiguration parserConfiguration) {
             super(r);
+            tabSize = parserConfiguration.getTabSize();
         }
         
         public boolean hasFailure() {
@@ -861,8 +871,8 @@ public class Template extends Configurable {
                     lines.add(lineBuf.toString());
                     lineBuf.setLength(0);
                 }
-            } else if (c == '\t') {
-                int numSpaces = 8 - (lineBuf.length() % 8);
+            } else if (c == '\t' && tabSize != 1) {
+                int numSpaces = tabSize - (lineBuf.length() % tabSize);
                 for (int i = 0; i < numSpaces; i++) {
                     lineBuf.append(' ');
                 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index b800eb4..bb8ac4f 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -154,6 +154,8 @@ public class FMParser {
                                 : null,
                         template != null ? template.getParserConfiguration().getRecognizeStandardFileExtensions()
                                 : null,
+                        template != null ? template.getParserConfiguration().getTabSize()
+                                : null,
                         new Version(incompatibleImprovements),
                         template != null ? template.getArithmeticEngine() : null));
     }
@@ -164,11 +166,13 @@ public class FMParser {
      * @since 2.3.24
      */
     public FMParser(Template template, Reader reader, ParserConfiguration pCfg) {
-        this(template, true, readerToTokenManager(reader), pCfg);
+        this(template, true, readerToTokenManager(reader, pCfg), pCfg);
     }
 
-    private static FMParserTokenManager readerToTokenManager(Reader reader) {
-        return new FMParserTokenManager(new SimpleCharStream(reader, 1, 1));
+    private static FMParserTokenManager readerToTokenManager(Reader reader, ParserConfiguration pCfg) {
+        SimpleCharStream simpleCharStream = new SimpleCharStream(reader, 1, 1);
+        simpleCharStream.setTabSize(pCfg.getTabSize());
+        return new FMParserTokenManager(simpleCharStream);
     }
 
     /**
@@ -193,6 +197,8 @@ public class FMParser {
             lpCfg.setOutputFormatIfNotSet(template.getOutputFormat());
             lpCfg.setRecognizeStandardFileExtensionsIfNotSet(
                     template.getParserConfiguration().getRecognizeStandardFileExtensions());
+            lpCfg.setTabSizeIfNotSet(
+                    template.getParserConfiguration().getTabSize());
         }
 
         int incompatibleImprovements = pCfg.getIncompatibleImprovements().intValue();

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 6f64c7a..454ac56 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -30,7 +30,7 @@
 
     <titleabbrev>Manual</titleabbrev>
 
-    <productname>Freemarker 2.3.24</productname>
+    <productname>Freemarker 2.3.25</productname>
   </info>
 
   <preface role="index.html" xml:id="preface">
@@ -26525,6 +26525,15 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
             </listitem>
 
             <listitem>
+              <para>New <literal>Configuration</literal> (and
+              <literal>TemplateConfiguration</literal>) setting,
+              <literal>tab_size</literal>. This only influences how the column
+              number reported in error messages is calculated (and the column
+              number available with other API-s). It doesn't influence the
+              output of the templates. Defaults to 8.</para>
+            </listitem>
+
+            <listitem>
               <para>Bug fixed (FREEMARKER-18): If you had a JSP custom tag and
               an EL function defined in the same TLD with the same name, the
               EL function has overwritten the custom tag. This is a bug
@@ -26535,6 +26544,21 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
               (<literal>&lt;@my.foo...&gt;</literal>) and as a function
               (<literal>my.f(...)</literal>).</para>
             </listitem>
+
+            <listitem>
+              <para>Bug fixed (<link
+              xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-19">FREEMARKER-19</link>):
+              The column numbers calculated by the parser has assumed tab size
+              1 since 2.3.25 (an unwanted side effect of updating JavaCC),
+              while before it has assume tab size 8. The default was restored
+              to 8. This bug has affected the column numbers in error
+              messages. It also broke the output of some rarely used AIP-s,
+              namely <literal>Template.getSource(beginCol, beginLine, endCol,
+              endLine)</literal>,
+              <literal>TemplateObject.getSource()</literal> and through that
+              <literal>TemplateObject.toString()</literal>, if the first or
+              last line has contain tab characters.</para>
+            </listitem>
           </itemizedlist>
         </section>
       </section>

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/test/java/freemarker/core/TabSizeTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/TabSizeTest.java b/src/test/java/freemarker/core/TabSizeTest.java
new file mode 100644
index 0000000..82c6951
--- /dev/null
+++ b/src/test/java/freemarker/core/TabSizeTest.java
@@ -0,0 +1,79 @@
+package freemarker.core;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.MalformedTemplateNameException;
+import freemarker.template.Template;
+import freemarker.template.TemplateNotFoundException;
+import freemarker.test.TemplateTest;
+
+public class TabSizeTest extends TemplateTest {
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_22);
+        return cfg;
+    }
+
+    @Test
+    public void testBasics() throws Exception {
+        assertErrorColumnNumber(3, "${*}");
+        assertErrorColumnNumber(8 + 3, "\t${*}");
+        assertErrorColumnNumber(16 + 3, "\t\t${*}");
+        assertErrorColumnNumber(16 + 3, "  \t  \t${*}");
+        
+        getConfiguration().setTabSize(1);
+        assertErrorColumnNumber(3, "${*}");
+        assertErrorColumnNumber(1 + 3, "\t${*}");
+        assertErrorColumnNumber(2 + 3, "\t\t${*}");
+        assertErrorColumnNumber(6 + 3, "  \t  \t${*}");
+    }
+    
+    @Test
+    public void testEvalBI() throws Exception {
+        assertErrorContains("${r'\t~'?eval}", "column 9");
+        getConfiguration().setTabSize(4);
+        assertErrorContains("${r'\t~'?eval}", "column 5");
+    }
+
+    @Test
+    public void testInterpretBI() throws Exception {
+        assertErrorContains("<@'\\t$\\{*}'?interpret />", "column 11");
+        getConfiguration().setTabSize(4);
+        assertErrorContains("<@'\\t$\\{*}'?interpret />", "column 7");
+    }
+    
+    @Test
+    public void testStringLiteralInterpolation() throws Exception {
+        assertErrorColumnNumber(6, "${'${*}'}");
+        assertErrorColumnNumber(9, "${'${\t*}'}");
+        getConfiguration().setTabSize(16);
+        assertErrorColumnNumber(17, "${'${\t*}'}");
+    }
+
+    protected void assertErrorColumnNumber(int expectedColumn, String templateSource)
+            throws TemplateNotFoundException, MalformedTemplateNameException, IOException {
+        addTemplate("t", templateSource);
+        try {
+            getConfiguration().getTemplate("t");
+            fail();
+        } catch (ParseException e) {
+            assertEquals(expectedColumn, e.getColumnNumber());
+        }
+        getConfiguration().clearTemplateCache();
+        
+        try {
+            new Template(null, templateSource, getConfiguration());
+            fail();
+        } catch (ParseException e) {
+            assertEquals(expectedColumn, e.getColumnNumber());
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/test/java/freemarker/core/TemplateConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/TemplateConfigurationTest.java b/src/test/java/freemarker/core/TemplateConfigurationTest.java
index 99b1b7a..ad3aaca 100644
--- a/src/test/java/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/freemarker/core/TemplateConfigurationTest.java
@@ -176,6 +176,7 @@ public class TemplateConfigurationTest {
         SETTING_ASSIGNMENTS.put("autoEscapingPolicy", Configuration.DISABLE_AUTO_ESCAPING_POLICY);
         SETTING_ASSIGNMENTS.put("outputFormat", HTMLOutputFormat.INSTANCE);
         SETTING_ASSIGNMENTS.put("recognizeStandardFileExtensions", true);
+        SETTING_ASSIGNMENTS.put("tabSize", 1);
         
         // Special settings:
         SETTING_ASSIGNMENTS.put("encoding", NON_DEFAULT_ENCODING);
@@ -586,6 +587,19 @@ public class TemplateConfigurationTest {
                     UndefinedOutputFormat.INSTANCE.getName(), HTMLOutputFormat.INSTANCE.getName());
             testedProps.add(Configuration.RECOGNIZE_STANDARD_FILE_EXTENSIONS_KEY_CAMEL_CASE);
         }
+
+        {
+            TemplateConfiguration tc = new TemplateConfiguration();
+            tc.setLogTemplateExceptions(false);
+            tc.setParentConfiguration(new Configuration(new Version(2, 3, 22)));
+            tc.setTabSize(3);
+            assertOutputWithoutAndWithTC(tc,
+                    "<#attempt><@'\\t$\\{1+}'?interpret/><#recover>"
+                    + "${.error?replace('(?s).*?column ([0-9]+).*', '$1', 'r')}"
+                    + "</#attempt>",
+                    "13", "8");
+            testedProps.add(Configuration.TAB_SIZE_KEY_CAMEL_CASE);
+        }
         
         assertEquals("Check that you have tested all parser settings; ", PARSER_PROP_NAMES, testedProps);
     }
@@ -749,8 +763,8 @@ public class TemplateConfigurationTest {
     
     private void assertOutputWithoutAndWithTC(TemplateConfiguration tc, String ftl, String expectedDefaultOutput,
             String expectedConfiguredOutput) throws TemplateException, IOException {
-        assertOutput(tc, ftl, expectedConfiguredOutput);
         assertOutput(null, ftl, expectedDefaultOutput);
+        assertOutput(tc, ftl, expectedConfiguredOutput);
     }
 
     private void assertOutput(TemplateConfiguration tc, String ftl, String expectedConfiguredOutput)

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/test/java/freemarker/template/ConfigurationTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index 28b5d9a..523b072 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -67,6 +67,7 @@ import freemarker.core.HTMLOutputFormat;
 import freemarker.core.HexTemplateNumberFormatFactory;
 import freemarker.core.MarkupOutputFormat;
 import freemarker.core.OutputFormat;
+import freemarker.core.ParseException;
 import freemarker.core.RTFOutputFormat;
 import freemarker.core.TemplateDateFormatFactory;
 import freemarker.core.TemplateNumberFormatFactory;
@@ -1376,6 +1377,59 @@ public class ConfigurationTest extends TestCase {
                     containsString(TemplateNumberFormatFactory.class.getName())));
         }
     }
+
+    @Test
+    public void testSetTabSize() throws Exception {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
+        
+        String ftl = "${\t}";
+        
+        try {
+            new Template(null, ftl, cfg);
+            fail();
+        } catch (ParseException e) {
+            assertEquals(9, e.getColumnNumber());
+        }
+        
+        cfg.setTabSize(1);
+        try {
+            new Template(null, ftl, cfg);
+            fail();
+        } catch (ParseException e) {
+            assertEquals(4, e.getColumnNumber());
+        }
+        
+        try {
+            cfg.setTabSize(0);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+        
+        try {
+            cfg.setTabSize(257);
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testTabSizeSetting() throws Exception {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_0);
+        assertEquals(8, cfg.getTabSize());
+        cfg.setSetting(Configuration.TAB_SIZE_KEY_CAMEL_CASE, "4");
+        assertEquals(4, cfg.getTabSize());
+        cfg.setSetting(Configuration.TAB_SIZE_KEY_SNAKE_CASE, "1");
+        assertEquals(1, cfg.getTabSize());
+        
+        try {
+            cfg.setSetting(Configuration.TAB_SIZE_KEY_SNAKE_CASE, "x");
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getCause(), instanceOf(NumberFormatException.class));
+        }
+    }
     
     @SuppressFBWarnings(value="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS", justification="We test failures")
     @Test

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/5141bcd0/src/test/java/freemarker/template/GetSourceTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/GetSourceTest.java b/src/test/java/freemarker/template/GetSourceTest.java
new file mode 100644
index 0000000..1d7f63c
--- /dev/null
+++ b/src/test/java/freemarker/template/GetSourceTest.java
@@ -0,0 +1,35 @@
+package freemarker.template;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class GetSourceTest {
+
+    
+    @Test
+    public void testGetSource() throws Exception {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_23);
+        
+        {
+            // Note: Default tab size is 8.
+            Template t = new Template(null, "a\n\tb\nc", cfg);
+            // A historical quirk we keep for B.C.: it repaces tabs with spaces.
+            assertEquals("a\n        b\nc", t.getSource(1, 1, 1, 3));
+        }
+        
+        {
+            cfg.setTabSize(4);
+            Template t = new Template(null, "a\n\tb\nc", cfg);
+            assertEquals("a\n    b\nc", t.getSource(1, 1, 1, 3));
+        }
+        
+        {
+            cfg.setTabSize(1);
+            Template t = new Template(null, "a\n\tb\nc", cfg);
+            // If tab size is 1, it behaves as it always should have: it keeps the tab.
+            assertEquals("a\n\tb\nc", t.getSource(1, 1, 1, 3));
+        }
+    }
+    
+}