diff --git a/freemarker-core/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java b/freemarker-core/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java index e2f51b5a6..5b4bde671 100644 --- a/freemarker-core/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java +++ b/freemarker-core/src/main/java/freemarker/core/LegacyConstructorParserConfiguration.java @@ -37,6 +37,8 @@ class LegacyConstructorParserConfiguration implements ParserConfiguration { private OutputFormat outputFormat; private Boolean recognizeStandardFileExtensions; private Integer tabSize; + private String normalizedEol; + private boolean normalizedEolSet; private final Version incompatibleImprovements; LegacyConstructorParserConfiguration(boolean strictSyntaxMode, boolean whitespaceStripping, @@ -150,6 +152,21 @@ public int getTabSize() { } return tabSize.intValue(); } + + @Override + public String getNormalizedEol() { + if (!normalizedEolSet) { + throw new IllegalStateException(); + } + return normalizedEol; + } + + void setNormalizedEolIfNotSet(String normalizedEol) { + if (!normalizedEolSet) { + this.normalizedEol = normalizedEol; + this.normalizedEolSet = true; + } + } void setTabSizeIfNotSet(int tabSize) { if (this.tabSize == null) { diff --git a/freemarker-core/src/main/java/freemarker/core/ParserConfiguration.java b/freemarker-core/src/main/java/freemarker/core/ParserConfiguration.java index 8c125dadb..e44793f21 100644 --- a/freemarker-core/src/main/java/freemarker/core/ParserConfiguration.java +++ b/freemarker-core/src/main/java/freemarker/core/ParserConfiguration.java @@ -89,4 +89,12 @@ public interface ParserConfiguration { */ int getTabSize(); + /** + * See {@link Configuration#setNormalizedEol(String)}; {@code null} if the output shouldn't be made to use any + * particular line break. + * + * @since 2.3.36 + */ + String getNormalizedEol(); + } diff --git a/freemarker-core/src/main/java/freemarker/core/TemplateConfiguration.java b/freemarker-core/src/main/java/freemarker/core/TemplateConfiguration.java index 121c18b6b..3fa54b78a 100644 --- a/freemarker-core/src/main/java/freemarker/core/TemplateConfiguration.java +++ b/freemarker-core/src/main/java/freemarker/core/TemplateConfiguration.java @@ -88,6 +88,8 @@ public final class TemplateConfiguration extends Configurable implements ParserC private OutputFormat outputFormat; private String encoding; private Integer tabSize; + private String normalizedEol; + private boolean normalizedEolSet; /** * Creates a new instance. The parent will be {@link Configuration#getDefaultConfiguration()} initially, but it will @@ -224,6 +226,9 @@ public void merge(TemplateConfiguration tc) { if (tc.isOutputEncodingSet()) { setOutputEncoding(tc.getOutputEncoding()); } + if (tc.isNormalizedEolSet()) { + setNormalizedEol(tc.getNormalizedEol()); + } if (tc.isOutputFormatSet()) { setOutputFormat(tc.getOutputFormat()); } @@ -266,6 +271,9 @@ public void merge(TemplateConfiguration tc) { if (tc.isTabSizeSet()) { setTabSize(tc.getTabSize()); } + if (tc.isNormalizedEolSet()) { + setNormalizedEol(tc.getNormalizedEol()); + } if (tc.isLazyImportsSet()) { setLazyImports(tc.getLazyImports()); } @@ -645,6 +653,35 @@ public int getTabSize() { return tabSize != null ? tabSize.intValue() : getNonNullParentConfiguration().getTabSize(); } + + /** + * See {@link Configuration#setNormalizedEol(String)}. + * + * @since 2.3.36 + */ + public void setNormalizedEol(String normalizedEol) { + this.normalizedEol = normalizedEol; + this.normalizedEolSet = true; + } + + /** + * Getter pair of {@link #setNormalizedEol(String)}. + * + * @since 2.3.36 + */ + @Override + public String getNormalizedEol() { + return normalizedEolSet ? normalizedEol : getNonNullParentConfiguration().getNormalizedEol(); + } + + /** + * Tells if this setting is set in this object, as opposed to being inherited. + * + * @since 2.3.36 + */ + public boolean isNormalizedEolSet() { + return normalizedEolSet; + } /** * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}. diff --git a/freemarker-core/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java b/freemarker-core/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java index 82d20a030..f43715c5d 100644 --- a/freemarker-core/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java +++ b/freemarker-core/src/main/java/freemarker/core/_ParserConfigurationWithInheritedFormat.java @@ -90,5 +90,10 @@ public ArithmeticEngine getArithmeticEngine() { public int getTabSize() { return wrappedPCfg.getTabSize(); } - + + @Override + public String getNormalizedEol() { + return wrappedPCfg.getNormalizedEol(); + } + } \ No newline at end of file diff --git a/freemarker-core/src/main/java/freemarker/template/Configuration.java b/freemarker-core/src/main/java/freemarker/template/Configuration.java index 4cf6ce613..3b9796d4d 100644 --- a/freemarker-core/src/main/java/freemarker/template/Configuration.java +++ b/freemarker-core/src/main/java/freemarker/template/Configuration.java @@ -573,6 +573,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf private int interpolationSyntax = LEGACY_INTERPOLATION_SYNTAX; private int namingConvention = AUTO_DETECT_NAMING_CONVENTION; private int tabSize = 8; // Default from JavaCC 3.x + private String normalizedEol; private boolean fallbackOnNullLoopVariable = true; // Default for backward compatibility private boolean preventStrippings; @@ -2808,6 +2809,52 @@ public int getTabSize() { return tabSize; } + /** + * Sets the end-of-line (line break) string that the output of the templates should use, or {@code null} (the + * default) to not prescribe any. This is a parser-level setting, as it's applied when the template is parsed; + * it therefore can't be changed with the {@code setting} directive from inside a template, but it can differ + * per template, via {@link TemplateConfiguration}. + * + *
It affects two things: + * + *
${...} and FTL tags).
+ * If this setting is non-{@code null} they are all replaced with this value, and so the line breaks that
+ * the template file happens to use — which depends on the editor and operating system of whoever last
+ * saved it — don't leak into the output. If it's {@code null}, they are output as they are in the file,
+ * which is what FreeMarker did before 2.3.36.
+ * \R escape of string literals, which is replaced with this value, or with a line feed
+ * (U+000A) if this setting is {@code null}. Unlike \n, which always gives a line feed,
+ * \R gives whatever the output is supposed to use.
+ * Note that the values inserted by ${...} are never affected; this setting is about the
+ * template, not about the data.
+ *
+ *
Usually you set this to {@code "\n"} or {@code "\r\n"}. Prefer specifying the line break explicitly
+ * rather than deriving it from the machine the template is executed on, as then the output doesn't depend on
+ * that, which matters if it's stored in a version control system, for example.
+ *
+ * @param normalizedEol
+ * The line break to use, or {@code null} to not prescribe any. Should be {@code "\n"},
+ * {@code "\r\n"} or {@code "\r"}, though other values aren't rejected.
+ *
+ * @since 2.3.36
+ */
+ public void setNormalizedEol(String normalizedEol) {
+ this.normalizedEol = normalizedEol;
+ }
+
+ /**
+ * The getter pair of {@link #setNormalizedEol(String)}.
+ *
+ * @since 2.3.36
+ */
+ @Override
+ public String getNormalizedEol() {
+ return normalizedEol;
+ }
+
/**
* The getter pair of {@link #setFallbackOnNullLoopVariable(boolean)}.
*
diff --git a/freemarker-core/src/main/java/freemarker/template/utility/StringUtil.java b/freemarker-core/src/main/java/freemarker/template/utility/StringUtil.java
index c4664d48c..3ed076797 100644
--- a/freemarker-core/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/freemarker-core/src/main/java/freemarker/template/utility/StringUtil.java
@@ -39,7 +39,7 @@
* Some text related utilities.
*/
public class StringUtil {
-
+
/**
* Used to look up if the chars with low code needs to be escaped, but note that it gives bad result for '=', as
* there the it matters if it's after '['.
@@ -557,6 +557,18 @@ private static boolean isInterpolationStart(char c) {
* @throws ParseException if there string contains illegal escapes
*/
public static String FTLStringLiteralDec(String s) throws ParseException {
+ return FTLStringLiteralDec(s, null);
+ }
+
+ /**
+ * Same as {@link #FTLStringLiteralDec(String)}, but you can say what the \R escape stands for.
+ *
+ * @param normalizedEol
+ * The value of the {@code normalized_eol} setting, or {@code null} for a line feed.
+ *
+ * @since 2.3.36
+ */
+ public static String FTLStringLiteralDec(String s, String normalizedEol) throws ParseException {
int idx = s.indexOf('\\');
if (idx == -1) {
@@ -589,6 +601,10 @@ public static String FTLStringLiteralDec(String s) throws ParseException {
buf.append('\n');
bidx = idx + 2;
break;
+ case 'R':
+ buf.append(normalizedEol != null ? normalizedEol : "\n");
+ bidx = idx + 2;
+ break;
case 'r':
buf.append('\r');
bidx = idx + 2;
diff --git a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
index 871956a2b..259b0d38a 100644
--- a/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
+++ b/freemarker-core/src/main/javacc/freemarker/core/FTL.jj
@@ -220,6 +220,8 @@ public class FMParser {
template.getParserConfiguration().getRecognizeStandardFileExtensions());
lpCfg.setTabSizeIfNotSet(
template.getParserConfiguration().getTabSize());
+ lpCfg.setNormalizedEolIfNotSet(
+ template.getConfiguration().getNormalizedEol());
}
int incompatibleImprovements = pCfg.getIncompatibleImprovements().intValue();
@@ -394,6 +396,48 @@ public class FMParser {
return token_source.namingConvention;
}
+ /**
+ * Replaces the line breaks of static template text with the {@code normalized_eol} setting value, if that
+ * setting is set. Done here, when the template is parsed, so that executing it costs nothing.
+ *
+ * @return {@code s} itself if there's nothing to replace.
+ */
+ private String normalizeEol(String s) {
+ String normalizedEol = pCfg.getNormalizedEol();
+ if (normalizedEol == null) {
+ return s;
+ }
+
+ int len = s.length();
+ boolean needsReplacement = false;
+ for (int i = 0; i < len; i++) {
+ char c = s.charAt(i);
+ if (c == '\r' || (c == '\n' && !normalizedEol.equals("\n"))) {
+ needsReplacement = true;
+ break;
+ }
+ }
+ if (!needsReplacement) {
+ return s;
+ }
+
+ StringBuilder sb = new StringBuilder(len + 16);
+ for (int i = 0; i < len; i++) {
+ char c = s.charAt(i);
+ if (c == '\r') {
+ sb.append(normalizedEol);
+ if (i + 1 < len && s.charAt(i + 1) == '\n') {
+ i++; // CRLF is one line break
+ }
+ } else if (c == '\n') {
+ sb.append(normalizedEol);
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
/**
* Throw an exception if the expression passed in is a String Literal
*/
@@ -1224,7 +1268,7 @@ TOKEN:
<#ESCAPED_CHAR :
"\\"
(
- ("n" | "t" | "r" | "f" | "b" | "g" | "l" | "a" | "\\" | "'" | "\"" | "{" | "=")
+ ("n" | "t" | "r" | "f" | "b" | "g" | "l" | "a" | "R" | "\\" | "'" | "\"" | "{" | "=")
|
("x" ["0"-"9", "A"-"F", "a"-"f"])
)
@@ -2510,7 +2554,7 @@ StringLiteral StringLiteral(boolean interpolate) :
s = t.image.substring(2, t.image.length() -1);
} else {
try {
- s = StringUtil.FTLStringLiteralDec(t.image.substring(1, t.image.length() -1));
+ s = StringUtil.FTLStringLiteralDec(t.image.substring(1, t.image.length() -1), pCfg.getNormalizedEol());
} catch (ParseException pe) {
pe.lineNumber = t.beginLine;
pe.columnNumber = t.beginColumn;
@@ -3847,7 +3891,7 @@ TextBlock NoParse() :
start =