Allowed escaping # with backlash in identifier names (not in string), as it used to occur in database column names.
diff --git a/src/main/java/freemarker/core/_CoreStringUtils.java b/src/main/java/freemarker/core/_CoreStringUtils.java
index e545d7c..dc87b02 100644
--- a/src/main/java/freemarker/core/_CoreStringUtils.java
+++ b/src/main/java/freemarker/core/_CoreStringUtils.java
@@ -46,7 +46,8 @@
scanForQuotationType: for (int i = 0; i < name.length(); i++) {
final char c = name.charAt(i);
if (!(i == 0 ? StringUtil.isFTLIdentifierStart(c) : StringUtil.isFTLIdentifierPart(c)) && c != '@') {
- if ((quotationType == 0 || quotationType == '\\') && (c == '-' || c == '.' || c == ':')) {
+ if ((quotationType == 0 || quotationType == '\\')
+ && StringUtil.isBackslashEscapedFTLIdentifierCharacter(c)) {
quotationType = '\\';
} else {
quotationType = '"';
@@ -66,8 +67,27 @@
}
}
- private static String backslashEscapeIdentifier(String name) {
- return StringUtil.replace(StringUtil.replace(StringUtil.replace(name, "-", "\\-"), ".", "\\."), ":", "\\:");
+ /*
+ * Escapes an identifier. This assumes that the identifier was once accepted by the parser, thus it is properly
+ * escapeable. Invalid characters that can't be escaped will be left as is. (This is actually feature because of
+ * historically weirdness, like that a sole {@code *} is a valid subvariable name, which must not be escaped.)
+ */
+ public static String backslashEscapeIdentifier(String name) {
+ StringBuilder sb = null;
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (StringUtil.isBackslashEscapedFTLIdentifierCharacter(c)) {
+ if (sb == null) {
+ sb = new StringBuilder(name.length() + 8);
+ sb.append(name, 0, i);
+ }
+ sb.append('\\');
+ }
+ if (sb != null) {
+ sb.append(c);
+ }
+ }
+ return sb == null ? name : sb.toString();
}
/**
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 1238dd9..3317955 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -1270,6 +1270,17 @@
public static boolean isFTLIdentifierPart(final char c) {
return isFTLIdentifierStart(c) || (c >= '0' && c <= '9');
}
+
+ /**
+ * Tells if a character can occur in an FTL identifier if it's preceded with a backslash. For example, {@code "-"}
+ * is a such character (as you can have an identifier like {@code foo\-bar} in FTL), but {@code "f"} is not, as
+ * it needn't be, and can't be escaped.
+ *
+ * @since 2.3.31
+ */
+ public static boolean isBackslashEscapedFTLIdentifierCharacter(final char c) {
+ return c == '-' || c == '.' || c == ':' || c == '#';
+ }
/**
* Escapes the <code>String</code> with the escaping rules of Java language
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index cb80218..a6ee15b 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -1544,7 +1544,8 @@
]
>
|
- <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":")>
+ // Keep this in sync with StringUtil.isBackslashEscapedFTLIdentifierCharacter
+ <#ESCAPED_ID_CHAR: "\\" ("-" | "." | ":" | "#")>
|
<#ID_START_CHAR: <NON_ESCAPED_ID_START_CHAR>|<ESCAPED_ID_CHAR>>
|
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 454979a..5e9a44f 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -2681,8 +2681,8 @@
non-Latin digits), underline (<literal>_</literal>), dollar
(<literal>$</literal>), at sign (<literal>@</literal>).
Furthermore, the first character can't be an ASCII digit
- (<literal>0</literal>-<literal>9</literal>). Starting from
- FreeMarker 2.3.22, the variable name can also contain minus
+ (<literal>0</literal>-<literal>9</literal>). Since FreeMarker
+ 2.3.22 the variable name can also contain minus
(<literal>-</literal>), dot (<literal>.</literal>), and colon
(<literal>:</literal>) at any position, but these must be escaped
with a preceding backslash (<literal>\</literal>), otherwise they
@@ -2690,7 +2690,10 @@
whose name is <quote>data-id</quote>, the expression is
<literal>data\-id</literal>, as <literal>data-id</literal> would
be interpreted as <quote>data minus id</quote>. (Note that these
- escapes only work in identifiers, not in string literals.)</para>
+ escapes only work in identifiers, not in string literals.)
+ Furthermore, since FreeMarker 2.3.31, hash mark
+ (<literal>#</literal>) can also be used, but must be escaped with
+ a preceding backslash (<literal>\</literal>).</para>
</section>
<section xml:id="dgui_template_exp_var_hash">
@@ -29413,6 +29416,16 @@
</listitem>
<listitem>
+ <para>Allowed escaping <literal>#</literal> with backlash in
+ identifier names (not in string), as it used to occur in
+ database column names. Like if you have a column name like
+ <literal>#users</literal>, you can refer to it as
+ <literal>row.\#users</literal>. (Alternatively,
+ <literal>row['#users']</literal> always worked, but is often
+ less convenient.)</para>
+ </listitem>
+
+ <listitem>
<para><link
xlink:href="https://issues.apache.org/jira/projects/FREEMARKER/issues/FREEMARKER-169">FREEMARKER-169</link>:
Fixed bug that made <literal>?c</literal> and
diff --git a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
index 75d52f1..11b1172 100644
--- a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
+++ b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl
@@ -30,9 +30,9 @@
</#function>
${f\-a("f-a")}
-<#assign \-\-\-\.\: = 'dash-dash-dash etc.'>
-${\-\-\-\.\:}
-${.vars['---.:']}
+<#assign \-\-\-\.\:\# = 'dash-dash-dash etc.'>
+${\-\-\-\.\:\#}
+${.vars['---.:#']}
<#assign hash = { '--moz-prop': 'propVal' }>
${hash.\-\-moz\-prop}
${hash['--moz-prop']}
diff --git a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
index 17e2b4e..96eff6b 100644
--- a/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
+++ b/src/test/resources/freemarker/core/cano-identifier-escaping.ftl.out
@@ -21,8 +21,8 @@
<#function f\-a(p\-a)><#return p\-a + " works"/></#function>${f\-a("f-a")}
-<#assign \-\-\-\.\: = "dash-dash-dash etc.">${\-\-\-\.\:}
-${.vars["---.:"]}
+<#assign \-\-\-\.\:\# = "dash-dash-dash etc.">${\-\-\-\.\:\#}
+${.vars["---.:#"]}
<#assign hash = {"--moz-prop": "propVal"}>${hash.\-\-moz\-prop}
${hash["--moz-prop"]}
diff --git a/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt b/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
index 1c62bd5..5afbaec 100644
--- a/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
+++ b/src/test/resources/freemarker/test/templatesuite/expected/identifier-escaping.txt
@@ -40,7 +40,7 @@
<catchAll x=1 y=2 a:b.c=5 data-foo=4 z=3 />
----.: = dash-dash-dash etc.
+---.:# = dash-dash-dash etc.
@as@_a = as1
as/b = as3
as'c = as4
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl b/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
index 9b39235..e29d997 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/identifier-escaping.ftl
@@ -30,9 +30,9 @@
</#function>
${f\-a("f-a")}
-<#assign \-\-\-\.\: = 'dash-dash-dash etc.'>
-${\-\-\-\.\:}
-${.vars['---.:']}
+<#assign \-\-\-\.\:\# = 'dash-dash-dash etc.'>
+${\-\-\-\.\:\#}
+${.vars['---.:#']}
<#assign hash = { '--moz-prop': 'propVal' }>
${hash.\-\-moz\-prop}
${hash['--moz-prop']}