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 2017/06/23 22:28:12 UTC
incubator-freemarker git commit: Continued work on the FM2 to FM3
converter
Repository: incubator-freemarker
Updated Branches:
refs/heads/3 e97f6ba21 -> 2b5cf6f68
Continued work on the FM2 to FM3 converter
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/2b5cf6f6
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/2b5cf6f6
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/2b5cf6f6
Branch: refs/heads/3
Commit: 2b5cf6f68006d335951218e9ec0225e0e8d3aecc
Parents: e97f6ba
Author: ddekany <dd...@apache.org>
Authored: Sat Jun 24 00:27:57 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Sat Jun 24 00:27:57 2017 +0200
----------------------------------------------------------------------
.../core/FM2ASTToFM3SourceConverter.java | 316 ++++++++++++++++---
.../core/UnexpectedNodeContentException.java | 2 +-
.../converter/FM2ToFM3ConverterTest.java | 56 +++-
3 files changed, 327 insertions(+), 47 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2b5cf6f6/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
index 6afcfa1..0cb2d35 100644
--- a/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
+++ b/freemarker-converter/src/main/java/freemarker/core/FM2ASTToFM3SourceConverter.java
@@ -23,10 +23,12 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
-import org.apache.freemarker.core.NamingConvention;
-import org.apache.freemarker.core.util._NullArgumentException;
import org.apache.freemarker.converter.ConverterException;
import org.apache.freemarker.converter.ConverterUtils;
+import org.apache.freemarker.core.NamingConvention;
+import org.apache.freemarker.core.util.FTLUtil;
+import org.apache.freemarker.core.util._NullArgumentException;
+import org.apache.freemarker.core.util._StringUtil;
import freemarker.template.Configuration;
import freemarker.template.Template;
@@ -50,9 +52,12 @@ import freemarker.template.Template;
* {@link #getParam(TemplateObject, int, ParameterRole, Class)}, etc.) instead of directly calling methods specific
* to the node subclass. Always process all parameters; where you don't use
* {@link #getOnlyParam(TemplateObject, ParameterRole, Class)}, use {@link #assertParamCount(TemplateObject, int)} to
- * ensure that. If you know you don't need some of the paramters, still at least call
+ * ensure that no parameter remains unhandled. If you know you don't need some of the parameters, still at least call
* {@link #assertParamRole(TemplateObject, int, ParameterRole)} for them. These ensure that if new parameters are
* added in FreeMarker 2.x, no information will be silently lost during conversion.
+ * <li>At many places you will see that we meticulously extract parts from the source, piece by piece print it to the
+ * output, but at the end we just end up with the same text that could have been copied from the source. The idea is
+ * that as the FM3 template language evolves, we will have to change the output for some pieces.
* </ul>
*/
@SuppressWarnings("deprecation")
@@ -100,7 +105,7 @@ public class FM2ASTToFM3SourceConverter {
private String getOutput() throws ConverterException {
String s = out.toString();
try {
- new org.apache.freemarker.core.Template(null, s, fm3Config);
+ //!!T new org.apache.freemarker.core.Template(null, s, fm3Config);
} catch (Exception e) {
throw new ConverterException(
"The result of the conversion wasn't valid FreeMarker 3 template; see cause exception", e);
@@ -132,7 +137,7 @@ public class FM2ASTToFM3SourceConverter {
printEndTagSkippedTokens(node);
print(tagEndChar);
} else if (node instanceof ConditionalBlock) {
- assertParamCount(node,2);
+ assertParamCount(node, 2);
TemplateObject conditionExp = getParam(node, 0, ParameterRole.CONDITION, TemplateObject.class);
int nodeSubtype = getParam(node, 1, ParameterRole.AST_NODE_SUBTYPE, Integer.class);
@@ -163,6 +168,92 @@ public class FM2ASTToFM3SourceConverter {
printEndTagSkippedTokens(node);
print(tagEndChar);
}
+ } else if (node instanceof UnifiedCall) {
+ print(tagBeginChar);
+ print('@');
+
+ TemplateObject callee = getParam(node, 0, ParameterRole.CALLEE, TemplateObject.class);
+ printExpressionNode(callee);
+
+ TemplateObject lastPrintedExp = callee;
+ int paramIdx = 1;
+ int paramCount = node.getParameterCount();
+
+ // Print positional arguments:
+ while (paramIdx < paramCount && node.getParameterRole(paramIdx) == ParameterRole.ARGUMENT_VALUE) {
+ TemplateObject argValue = getParam(node, paramIdx, ParameterRole.ARGUMENT_VALUE, TemplateObject.class);
+
+ printParameterSeparatorSource(lastPrintedExp, argValue);
+ printExpressionNode(argValue);
+
+ lastPrintedExp = argValue;
+ paramIdx++;
+ }
+
+ // Print named arguments:
+ while (paramIdx < paramCount
+ && node.getParameterRole(paramIdx) == ParameterRole.ARGUMENT_NAME) {
+ TemplateObject argValue = getParam(node, paramIdx + 1, ParameterRole.ARGUMENT_VALUE,
+ TemplateObject.class);
+
+ printParameterSeparatorSource(lastPrintedExp, argValue); // Prints something like " someArgName="
+ printExpressionNode(argValue);
+
+ lastPrintedExp = argValue;
+ paramIdx += 2;
+ }
+
+ // Print loop variables:
+ int pos = getEndPositionExclusive(lastPrintedExp);
+ boolean beforeFirstLoopVar = true;
+ while (paramIdx < paramCount) {
+ String sep = readExpWSAndSeparator(pos, beforeFirstLoopVar ? ';' : ',', false);
+ assertNodeContent(sep.length() != 0, node,
+ "Can't find loop variable separator", null);
+ print(sep);
+ pos += sep.length();
+
+ String loopVarName = getParam(node, paramIdx, ParameterRole.TARGET_LOOP_VARIABLE, String.class);
+ print(_StringUtil.toFTLTopLevelIdentifierReference(loopVarName));
+ String identifierInSrc = readIdentifier(pos);
+ assertNodeContent(identifierInSrc.length() != 0, node,
+ "Can't find loop variable identifier in source", null);
+ pos += identifierInSrc.length(); // skip loop var name
+
+ beforeFirstLoopVar = false;
+ paramIdx++;
+ }
+
+ int startTagEndPos = printStartTagSkippedTokens(node, pos, false);
+ print(tagEndChar);
+
+ int elementEndPos = getEndPositionInclusive(node);
+ {
+ char c = src.charAt(elementEndPos);
+ assertNodeContent(c == tagEndChar, node,
+ "tagEndChar expected, found '{}'", c);
+ }
+ if (startTagEndPos != elementEndPos) { // We have an end-tag
+ assertNodeContent(src.charAt(startTagEndPos - 1) != '/', node,
+ "Not expected \"/\" at the end of the start tag", null);
+ printChildrenElements(node);
+
+ print(tagBeginChar);
+ print("/@");
+ int nameStartPos = elementEndPos; // Not 1 less; consider the case of </@>
+ while (nameStartPos >= 2 && !src.startsWith("/@", nameStartPos - 2)) {
+ nameStartPos--;
+ }
+ assertNodeContent(nameStartPos >= 2, node,
+ "Couldn't extract name from end-tag.", null);
+ print(src.substring(nameStartPos, elementEndPos)); // Also prints ignored WS after name, for now
+ print(tagEndChar);
+ } else { // We don't have end-tag
+ assertNodeContent(src.charAt(startTagEndPos - 1) == '/', node,
+ "Expected \"/\" at the end of the start tag", null);
+ assertNodeContent(node.getChildCount() == 0, node,
+ "Expected no children", null);
+ }
} else if (node instanceof Comment) {
print(tagBeginChar);
print("#--");
@@ -175,8 +266,68 @@ public class FM2ASTToFM3SourceConverter {
}
private void printExpressionNode(TemplateObject node) throws ConverterException {
- if (node instanceof Identifier || node instanceof NumberLiteral || node instanceof BooleanLiteral) {
+ if (node instanceof Identifier) {
+ print(FTLUtil.escapeIdentifier(((Identifier) node).getName()));
+ } else if (node instanceof NumberLiteral) {
+ print(getSrcSectionInclEnd(
+ node.getBeginColumn(), node.getBeginLine(),
+ node.getEndColumn(), node.getEndLine()));
+ } else if (node instanceof BooleanLiteral) {
print(node.getCanonicalForm());
+ } else if (node instanceof StringLiteral) {
+ boolean rawString = false;
+ char quote;
+ {
+ int pos = getStartPosition(node);
+ quote = src.charAt(pos);
+ while ((quote == '\\' || quote == '{' /* 2.3.26 bug workaround */ || quote == 'r')
+ && pos < src.length()) {
+ pos++;
+ if (quote == 'r') {
+ rawString = true;
+ }
+ quote = src.charAt(pos);
+ }
+ if (quote != '\'' && quote != '"') {
+ throw new UnexpectedNodeContentException(node, "Unexpected string quote character: {}", quote);
+ }
+ }
+ if (rawString) {
+ print('r');
+ }
+ print(quote);
+
+ int parameterCount = node.getParameterCount();
+ if (parameterCount == 0) {
+ if (!rawString) {
+ print(FTLUtil.escapeStringLiteralPart(((StringLiteral) node).getAsString(), quote));
+ } else {
+ print(((StringLiteral) node).getAsString());
+ }
+ } else {
+ // Not really a literal; contains interpolations
+ for (int paramIdx = 0; paramIdx < parameterCount; paramIdx++) {
+ Object param = getParam(node, paramIdx, ParameterRole.VALUE_PART, Object.class);
+ if (param instanceof String) {
+ print(FTLUtil.escapeStringLiteralPart((String) param));
+ } else {
+ assertNodeContent(param instanceof Interpolation, node,
+ "Unexpected parameter type: {}", param.getClass().getName());
+
+ // We print the interpolation, the cut it out from the output, then put it back escaped:
+ int interpStartPos = out.length();
+ printNode((TemplateElement) param);
+ int interpEndPos = out.length();
+ String interp = out.substring(interpStartPos, interpEndPos);
+ out.setLength(interpStartPos + 2); // +2 to keep the "${"
+ String inerpInside = interp.substring(2, interp.length() - 1);
+ print(FTLUtil.escapeStringLiteralPart(inerpInside, quote)); // For now we escape as FTL2
+ print(interp.charAt(interp.length() - 1)); // "}"
+ }
+ }
+ }
+
+ print(quote);
} else if (node instanceof AddConcatExpression) {
assertParamCount(node, 2);
TemplateObject lho = getParam(node, 0, ParameterRole.LEFT_HAND_OPERAND, TemplateObject.class);
@@ -221,8 +372,8 @@ public class FM2ASTToFM3SourceConverter {
printExpressionNode(lho); // [lho]?biName
- int postLHOPos = getPosition(lho.getEndColumn(), lho.getEndLine()) + 1;
- int endPos = getPosition(node.getEndColumn(), node.getEndLine());
+ int postLHOPos = getEndPositionExclusive(lho);
+ int endPos = getEndPositionInclusive(node);
boolean foundQuestionMark = false;
int pos = postLHOPos;
scanForRHO: while (pos < endPos) {
@@ -322,13 +473,16 @@ public class FM2ASTToFM3SourceConverter {
/**
* Prints the part between the last parameter (or the directive name if there are no parameters) and the tag closer
* character, for a start tag. That part may contains whitespace or comments, which aren't visible in the AST.
+ *
+ * @return The position of the last character of the start tag. Note that the printed string never includes this
+ * character.
*/
- private void printStartTagSkippedTokens(TemplateElement node, TemplateObject lastParam, boolean trimSlash)
- throws UnexpectedNodeContentException {
+ private int printStartTagSkippedTokens(TemplateElement node, TemplateObject lastParam, boolean trimSlash)
+ throws ConverterException {
int pos;
if (lastParam == null) {
// No parameter; must skip the tag name
- pos = getPosition(node.getBeginColumn(), node.getBeginLine());
+ pos = getStartPosition(node);
{
char c = src.charAt(pos++);
assertNodeContent(c == tagBeginChar, node,
@@ -349,41 +503,40 @@ public class FM2ASTToFM3SourceConverter {
} else {
pos = getPosition(lastParam.getEndColumn() + 1, lastParam.getEndLine());
}
+ return printStartTagSkippedTokens(node, pos, trimSlash);
+ }
+
+ /**
+ * Similar to {@link #printStartTagSkippedTokens(TemplateElement, TemplateObject, boolean)}, but with explicitly
+ * specified scan start position.
+ *
+ * @param pos The position where the first skipped character can occur (or the tag end character).
+ */
+ private int printStartTagSkippedTokens(TemplateElement node, int pos, boolean trimSlash)
+ throws ConverterException {
final int startPos = pos;
- while (pos < src.length()) {
- char c = src.charAt(pos);
- if ((c == '<' || c == '[')
- && (src.startsWith("!--", pos + 1) || src.startsWith("#--", pos + 1))) {
- pos += 4;
- scanForCommentEnd: while (pos < src.length()) {
- if (src.startsWith("-->", pos) || src.startsWith("--]", pos)) {
- pos += 3;
- break scanForCommentEnd;
- }
- pos++;
- }
- if (pos == src.length()) {
- throw new UnexpectedNodeContentException(node, "Can't find comment end in the start tag", null);
- }
- } else if (c == '/' && pos + 1 < src.length() && src.charAt(pos + 1) == tagEndChar) {
- print(src.substring(startPos, trimSlash ? pos : pos + 1));
- return;
- } else if (c == tagEndChar) {
- print(src.substring(startPos, pos));
- return;
- } else if (Character.isWhitespace(c)) {
- pos++;
- } else {
- throw new UnexpectedNodeContentException(node,
- "Unexpected character when scanning for tag end: '{}'", c);
- }
+ pos = getPositionAfterWSAndExpComments(pos);
+ if (pos == src.length()) {
+ throw new UnexpectedNodeContentException(node,
+ "End of source reached when scanning for tag end", null);
+ }
+
+ char c = src.charAt(pos);
+ if (c == '/' && pos + 1 < src.length() && src.charAt(pos + 1) == tagEndChar) {
+ print(src.substring(startPos, trimSlash ? pos : pos + 1));
+ return pos + 1;
+ } else if (c == tagEndChar) {
+ print(src.substring(startPos, pos));
+ return pos;
+ } else {
+ throw new UnexpectedNodeContentException(node,
+ "Unexpected character when scanning for tag end: '{}'", c);
}
- throw new UnexpectedNodeContentException(node, "Can't find start tag end", null);
}
private void printEndTagSkippedTokens(TemplateElement node) throws UnexpectedNodeContentException {
- int tagEndPos = getPosition(node.getEndColumn(), node.getEndLine());
+ int tagEndPos = getEndPositionInclusive(node);
{
char c = src.charAt(tagEndPos);
assertNodeContent(c == tagEndChar, node,
@@ -467,6 +620,18 @@ public class FM2ASTToFM3SourceConverter {
}
}
+ private int getStartPosition(TemplateObject node) {
+ return getPosition(node.getBeginColumn(), node.getBeginLine());
+ }
+
+ private int getEndPositionInclusive(TemplateObject node) {
+ return getPosition(node.getEndColumn(), node.getEndLine());
+ }
+
+ private int getEndPositionExclusive(TemplateObject node) {
+ return getEndPositionInclusive(node) + 1;
+ }
+
/**
* Returns the position of a character in the {@link #src} string.
*
@@ -490,6 +655,10 @@ public class FM2ASTToFM3SourceConverter {
return rowStartPositions.get(row - 1) + column - 1;
}
+ private String getSrcSectionInclEnd(int startColumn, int startRow, int exclEndColumn, int endRow) {
+ return src.substring(getPosition(startColumn, startRow), getPosition(exclEndColumn, endRow) + 1);
+ }
+
private String getSrcSectionExclEnd(int startColumn, int startRow, int exclEndColumn, int endRow) {
return src.substring(getPosition(startColumn, startRow), getPosition(exclEndColumn, endRow));
}
@@ -498,4 +667,71 @@ public class FM2ASTToFM3SourceConverter {
return Character.isLetterOrDigit(c) || c == '_';
}
+ /**
+ * @return Position after the whitespace and comments, or the argument position if there were node
+ */
+ private int getPositionAfterWSAndExpComments(int pos) throws ConverterException {
+ scanForNoWSNoComment: while (pos < src.length()) {
+ char c = src.charAt(pos);
+ if ((c == '<' || c == '[')
+ && (src.startsWith("!--", pos + 1) || src.startsWith("#--", pos + 1))) {
+ pos += 4;
+ scanForCommentEnd:
+ while (pos < src.length()) {
+ if (src.startsWith("-->", pos) || src.startsWith("--]", pos)) {
+ pos += 3;
+ break scanForCommentEnd;
+ }
+ pos++;
+ }
+ if (pos == src.length()) {
+ throw new ConverterException("Can't find comment end at " + pos, null);
+ }
+ } else if (Character.isWhitespace(c)) {
+ pos++;
+ } else {
+ break scanForNoWSNoComment;
+ }
+ }
+ return pos;
+ }
+
+
+ private String readExpWSAndSeparator(int startPos, char separator, boolean separatorOptional)
+ throws ConverterException {
+ int pos = getPositionAfterWSAndExpComments(startPos);
+
+ if (pos == src.length() || src.charAt(pos) != separator) {
+ // No separator
+ return separatorOptional ? src.substring(startPos, pos) : "";
+ }
+ pos++; // Skip separator
+
+ pos = getPositionAfterWSAndExpComments(pos);
+
+ return src.substring(startPos, pos);
+ }
+
+ private String readIdentifier(int startPos) throws ConverterException {
+ int pos = startPos;
+ scanUntilIdentifierEnd: while (pos < src.length()) {
+ char c = src.charAt(pos);
+ if (c == '\\') {
+ if (pos + 1 == src.length()) {
+ throw new ConverterException("Misplaced \"\\\" at position " + pos);
+ }
+ if (!FTLUtil.isEscapedIdentifierCharacter(src.charAt(pos + 1))) {
+ throw new ConverterException("Invalid escape at position " + pos);
+ }
+ pos += 2; // to skip escaped character
+ } else if (pos == startPos && FTLUtil.isNonEscapedIdentifierStart(c)
+ || FTLUtil.isNonEscapedIdentifierPart(c)) {
+ pos++;
+ } else {
+ break scanUntilIdentifierEnd;
+ }
+ }
+ return src.substring(startPos, pos);
+ }
+
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2b5cf6f6/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java b/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
index 81d9761..8a4b474 100644
--- a/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
+++ b/freemarker-converter/src/main/java/freemarker/core/UnexpectedNodeContentException.java
@@ -27,7 +27,7 @@ import org.apache.freemarker.converter.ConverterException;
public class UnexpectedNodeContentException extends ConverterException {
public UnexpectedNodeContentException(TemplateObject node, String errorMessage, Object msgParam) {
super("Unexpected AST content for " + _StringUtil.jQuote(node.getNodeTypeSymbol()) + " node (class: "
- + node.getClass().getName() + ") at " + node.getStartLocation() + ":\n"
+ + node.getClass().getName() + ") " + node.getStartLocation() + ":\n"
+ renderMessage(errorMessage, msgParam));
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/2b5cf6f6/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
----------------------------------------------------------------------
diff --git a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
index 8dc89ec..31367b2 100644
--- a/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
+++ b/freemarker-converter/src/test/java/org/freemarker/converter/FM2ToFM3ConverterTest.java
@@ -27,8 +27,8 @@ import java.io.IOException;
import java.util.Properties;
import org.apache.commons.io.FileUtils;
-import org.apache.freemarker.converter.FM2ToFM3Converter;
import org.apache.freemarker.converter.ConverterException;
+import org.apache.freemarker.converter.FM2ToFM3Converter;
import org.freemarker.converter.test.ConverterTest;
import org.junit.Test;
@@ -46,13 +46,29 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
}
@Test
- public void testInterpolations() throws IOException, ConverterException {
- assertConvertedSame("${var}");
- assertConvertedSame("${ var\n}");
+ public void testLiterals() throws IOException, ConverterException {
+ assertConvertedSame("${''}");
+ assertConvertedSame("${'s'}");
+ assertConvertedSame("${\"\"}");
+ assertConvertedSame("${\"s\"}");
+ assertConvertedSame("${\"\\\"'\"}");
+ assertConvertedSame("${'\"\\''}");
+ assertConvertedSame("${'1${x + 1 + \\'s\\'}2'}");
+ assertConvertedSame("${\"s ${'x $\\{\\\"y\\\"}'}\"}");
+ assertConvertedSame("${'${1}${2}'}");
+
+ assertConvertedSame("${r'${1}'}");
+
+ assertConvertedSame("${1}");
+ assertConvertedSame("${0.5}");
+ assertConvertedSame("${-1.5}");
+
+ assertConvertedSame("${true}");
+ assertConvertedSame("${false}");
}
@Test
- public void testExpressions() throws IOException, ConverterException {
+ public void testOtherExpressions() throws IOException, ConverterException {
assertConvertedSame("${x + 1\r\n\t- y % 2 / 2 * +z / -1}");
assertConvertedSame("${x * (y + z) * (\ty+z\n)}");
@@ -63,7 +79,13 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
}
@Test
- public void testDirectives() throws IOException, ConverterException {
+ public void testInterpolations() throws IOException, ConverterException {
+ assertConvertedSame("${var}");
+ assertConvertedSame("${ var\n}");
+ }
+
+ @Test
+ public void testCoreDirectives() throws IOException, ConverterException {
assertConvertedSame("<#if foo>1</#if>");
assertConvertedSame("<#if\n foo\n>\n123\n</#if\n>");
@@ -72,6 +94,28 @@ public class FM2ToFM3ConverterTest extends ConverterTest {
}
@Test
+ public void testUserDirectives() throws IOException, ConverterException {
+ assertConvertedSame("<@foo/>");
+ assertConvertedSame("<@foo />");
+ assertConvertedSame("<@foo\\-bar />");
+ assertConvertedSame("<@f...@foo>");
+ assertConvertedSame("<@foo\\-bar >t</@foo\\-bar>");
+ assertConvertedSame("<@foo\\-bar >t</@>");
+ assertConvertedSame("<@foo x=1 y=2 />");
+ assertConvertedSame("<@foo x\\-y=1 />");
+ assertConvertedSame("<@foo\n\tx = 1\n\ty = 2\n/>");
+ assertConvertedSame("<@foo 1 2 />");
+ assertConvertedSame("<@foo <#-- C1 --> 1 <#-- C2 --> 2 <#-- C3 --> />");
+ assertConvertedSame("<@foo 1, 2 />");
+ assertConvertedSame("<@foo <#-- C1 --> 1 <#-- C2 -->, <#-- C3 --> 2 <#-- C4 --> />");
+ assertConvertedSame("<@foo x=1; i, j></@>");
+ assertConvertedSame("<@foo 1; i, j></@>");
+ assertConvertedSame("<@foo 1 2; i\\-2, j></@>");
+ assertConvertedSame("<@foo x=1 y=2; i></@>");
+ assertConvertedSame("<@foo x=1 ;\n i <#-- C0 --> , <#-- C1 -->\n\t<!-- C2 --> j <#-- C3 -->\n></@>");
+ }
+
+ @Test
public void testBuiltInExpressions() throws IOException, ConverterException {
assertConverted("${s?upperCase} ${s?leftPad(123)}", "${s?upper_case} ${s?left_pad(123)}");
assertConverted("${s?html}", "${s?web_safe}");