Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,10 @@ public ArithmeticEngine getArithmeticEngine() {
public int getTabSize() {
return wrappedPCfg.getTabSize();
}


@Override
public String getNormalizedEol() {
return wrappedPCfg.getNormalizedEol();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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}.
*
* <p>It affects two things:
*
* <ul>
* <li>The line breaks of the static text of the template (the text outside <code>${...}</code> 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.
* <li>The <code>\R</code> escape of string literals, which is replaced with this value, or with a line feed
* (U+000A) if this setting is {@code null}. Unlike <code>\n</code>, which always gives a line feed,
* <code>\R</code> gives whatever the output is supposed to use.
* </ul>
*
* <p>Note that the values inserted by <code>${...}</code> are never affected; this setting is about the
* template, not about the data.
*
* <p>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)}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 '['.
Expand Down Expand Up @@ -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 <code>\R</code> 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) {
Expand Down Expand Up @@ -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;
Expand Down
54 changes: 49 additions & 5 deletions freemarker-core/src/main/javacc/freemarker/core/FTL.jj
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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"])
)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -3847,7 +3891,7 @@ TextBlock NoParse() :
start = <NOPARSE>
end = UnparsedContent(start, buf)
{
TextBlock result = new TextBlock(buf.toString(), true);
TextBlock result = new TextBlock(normalizeEol(buf.toString()), true);
result.setLocation(template, start, end);
return result;
}
Expand Down Expand Up @@ -4374,7 +4418,7 @@ TextBlock PCData() :
{
if (stripText && mixedContentNesting == 1 && !preventStrippings) return null;

TextBlock result = new TextBlock(buf.toString(), false);
TextBlock result = new TextBlock(normalizeEol(buf.toString()), false);
result.setLocation(template, start, t);
return result;
}
Expand All @@ -4389,7 +4433,7 @@ TextBlock WhitespaceText() :
{
if (stripText && mixedContentNesting == 1 && !preventStrippings) return null;

TextBlock result = new TextBlock(t.image, false);
TextBlock result = new TextBlock(normalizeEol(t.image), false);
result.setLocation(template, t, t);
return result;
}
Expand Down
Loading