diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java
index e2bd413a21a50..1dd41b1ed927d 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/LiteralSupport.java
@@ -74,7 +74,15 @@ static Object getLiteralValue(String literal) {
* false
otherwise
*/
static boolean isStringLiteralSeparator(char character) {
- return character == '"' || character == '\'';
+ return isStringLiteralSeparatorSingle(character) || isStringLiteralSeparatorDouble(character);
+ }
+
+ static boolean isStringLiteralSeparatorSingle(char character) {
+ return character == '\'';
+ }
+
+ static boolean isStringLiteralSeparatorDouble(char character) {
+ return character == '"';
}
static boolean isStringLiteral(String value) {
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
index 5b23843db8a4d..ee14ccf58c764 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java
@@ -23,6 +23,7 @@
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
+import java.util.regex.Pattern;
import org.jboss.logging.Logger;
@@ -165,7 +166,7 @@ Template parse() {
} else {
String reason = null;
ErrorCode code = null;
- if (state == State.TAG_INSIDE_STRING_LITERAL) {
+ if (state == State.TAG_INSIDE_STRING_LITERAL_SINGLE || state == State.TAG_INSIDE_STRING_LITERAL_DOUBLE) {
reason = "unterminated string literal";
code = ParserError.UNTERMINATED_STRING_LITERAL;
} else if (state == State.TAG_INSIDE) {
@@ -249,8 +250,11 @@ private void processCharacter(char character) {
case TAG_INSIDE:
tag(character);
break;
- case TAG_INSIDE_STRING_LITERAL:
- tagStringLiteral(character);
+ case TAG_INSIDE_STRING_LITERAL_SINGLE:
+ tagStringLiteralSingle(character);
+ break;
+ case TAG_INSIDE_STRING_LITERAL_DOUBLE:
+ tagStringLiteralDouble(character);
break;
case COMMENT:
comment(character);
@@ -339,7 +343,8 @@ private boolean isCdataEnd(char character) {
private void tag(char character) {
if (LiteralSupport.isStringLiteralSeparator(character)) {
- state = State.TAG_INSIDE_STRING_LITERAL;
+ state = LiteralSupport.isStringLiteralSeparatorSingle(character) ? State.TAG_INSIDE_STRING_LITERAL_SINGLE
+ : State.TAG_INSIDE_STRING_LITERAL_DOUBLE;
buffer.append(character);
} else if (character == END_DELIMITER) {
flushTag();
@@ -348,8 +353,15 @@ private void tag(char character) {
}
}
- private void tagStringLiteral(char character) {
- if (LiteralSupport.isStringLiteralSeparator(character)) {
+ private void tagStringLiteralSingle(char character) {
+ if (LiteralSupport.isStringLiteralSeparatorSingle(character)) {
+ state = State.TAG_INSIDE;
+ }
+ buffer.append(character);
+ }
+
+ private void tagStringLiteralDouble(char character) {
+ if (LiteralSupport.isStringLiteralSeparatorDouble(character)) {
state = State.TAG_INSIDE;
}
buffer.append(character);
@@ -819,7 +831,8 @@ static int getFirstDeterminingEqualsCharPosition(String part) {
static Iterator splitSectionParams(String content, B block) {
- boolean stringLiteral = false;
+ boolean stringLiteralSingle = false;
+ boolean stringLiteralDouble = false;
short composite = 0;
byte brackets = 0;
boolean space = false;
@@ -830,7 +843,10 @@ static Iterator splitSectionPa
char c = content.charAt(i);
if (c == ' ') {
if (!space) {
- if (!stringLiteral && composite == 0 && brackets == 0) {
+ if (!stringLiteralSingle
+ && !stringLiteralDouble
+ && composite == 0
+ && brackets == 0) {
if (buffer.length() > 0) {
parts.add(buffer.toString());
buffer = new StringBuilder();
@@ -842,19 +858,30 @@ static Iterator splitSectionPa
}
} else {
if (composite == 0
- && LiteralSupport.isStringLiteralSeparator(c)) {
- stringLiteral = !stringLiteral;
- } else if (!stringLiteral
- && isCompositeStart(c) && (i == 0 || space || composite > 0
+ && !stringLiteralDouble
+ && LiteralSupport.isStringLiteralSeparatorSingle(c)) {
+ stringLiteralSingle = !stringLiteralSingle;
+ } else if (composite == 0
+ && !stringLiteralSingle
+ && LiteralSupport.isStringLiteralSeparatorDouble(c)) {
+ stringLiteralDouble = !stringLiteralDouble;
+ } else if (!stringLiteralSingle
+ && !stringLiteralDouble
+ && isCompositeStart(c)
+ && (i == 0 || space || composite > 0
|| (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == '!'))) {
composite++;
- } else if (!stringLiteral
- && isCompositeEnd(c) && composite > 0) {
+ } else if (!stringLiteralSingle
+ && !stringLiteralDouble
+ && isCompositeEnd(c)
+ && composite > 0) {
composite--;
- } else if (!stringLiteral
+ } else if (!stringLiteralSingle
+ && !stringLiteralDouble
&& Parser.isLeftBracket(c)) {
brackets++;
- } else if (!stringLiteral
+ } else if (!stringLiteralSingle
+ && !stringLiteralDouble
&& Parser.isRightBracket(c) && brackets > 0) {
brackets--;
}
@@ -864,7 +891,7 @@ && isCompositeEnd(c) && composite > 0) {
}
if (buffer.length() > 0) {
- if (stringLiteral || composite > 0) {
+ if (stringLiteralSingle || stringLiteralDouble || composite > 0) {
throw block.error("unterminated string literal or composite parameter detected for [{content}]")
.argument("content", content)
.code(ParserError.UNTERMINATED_STRING_LITERAL_OR_COMPOSITE_PARAMETER)
@@ -874,10 +901,16 @@ && isCompositeEnd(c) && composite > 0) {
parts.add(buffer.toString());
}
- // Try to find/replace "standalone" equals signs used as param names separators
- // This allows for more lenient parsing of named section parameters, e.g. item.name = 'foo' instead of item.name='foo'
+ // Try to find/replace/merge:
+ // 1. "standalone" equals signs used as param names separators
+ // 2. parts that start/end with an equal sign followed/preceded by a valid Java identifier
+ // This allows for more lenient parsing of named section parameters
+ // e.g. `item = 'foo'` or `item= 'foo'` instead of `item='foo'`
for (ListIterator it = parts.listIterator(); it.hasNext();) {
- if (it.next().equals("=") && it.previousIndex() != 0 && it.hasNext()) {
+ String next = it.next();
+ if (next.equals("=")
+ && it.previousIndex() != 0
+ && it.hasNext()) {
// move cursor back
it.previous();
String merged = parts.get(it.previousIndex()) + it.next() + it.next();
@@ -889,12 +922,33 @@ && isCompositeEnd(c) && composite > 0) {
it.remove();
it.previous();
it.remove();
+ } else if (next.endsWith("=")
+ && it.hasNext()
+ && EQUAL_ENDS_PATTERN.matcher(next).matches()) {
+ String merged = next + it.next();
+ // replace the element with the merged value
+ it.set(merged);
+ // move cursor back and remove the element that ended with equals
+ it.previous();
+ it.previous();
+ it.remove();
+ } else if (next.startsWith("=")
+ && it.hasPrevious()
+ && EQUAL_STARTS_PATTERN.matcher(next).matches()) {
+ String merged = next + it.previous();
+ // replace the element with the merged value
+ it.set(merged);
+ // move cursor back and remove the element that started with equals
+ it.next();
+ it.remove();
}
}
-
return parts.iterator();
}
+ static final Pattern EQUAL_ENDS_PATTERN = Pattern.compile(".*[a-zA-Z0-9_$]=$");
+ static final Pattern EQUAL_STARTS_PATTERN = Pattern.compile("^=[a-zA-Z0-9_$].*");
+
static boolean isCompositeStart(char character) {
return character == START_COMPOSITE_PARAM;
}
@@ -931,7 +985,8 @@ enum State {
TEXT,
TAG_INSIDE,
- TAG_INSIDE_STRING_LITERAL,
+ TAG_INSIDE_STRING_LITERAL_SINGLE,
+ TAG_INSIDE_STRING_LITERAL_DOUBLE,
TAG_CANDIDATE,
COMMENT,
ESCAPE,
diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
index 9d6b663f675e0..c7375dc68763c 100644
--- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
+++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java
@@ -452,6 +452,30 @@ public void testMandatorySectionParas() {
"Parser error: mandatory section parameters not declared for {#include /}: [template]", 1);
}
+ @Test
+ public void testSectionParameterWithNestedSingleQuotationMark() {
+ Engine engine = Engine.builder().addDefaults().build();
+ assertSectionParams(engine, "{#let id=\"'Foo'\"}", Map.of("id", "\"'Foo'\""));
+ assertSectionParams(engine, "{#let id=\"'Foo \"}", Map.of("id", "\"'Foo \""));
+ assertSectionParams(engine, "{#let id=\"'Foo ' \"}", Map.of("id", "\"'Foo ' \""));
+ assertSectionParams(engine, "{#let id=\"'Foo ' \" bar='baz'}", Map.of("id", "\"'Foo ' \"", "bar", "'baz'"));
+ assertSectionParams(engine, "{#let my=bad id=(\"'Foo ' \" + 1) bar='baz'}",
+ Map.of("my", "bad", "id", "(\"'Foo ' \" + 1)", "bar", "'baz'"));
+ assertSectionParams(engine, "{#let id = 'Foo'}", Map.of("id", "'Foo'"));
+ assertSectionParams(engine, "{#let id= 'Foo'}", Map.of("id", "'Foo'"));
+ assertSectionParams(engine, "{#let my = (bad or not) id=1}", Map.of("my", "(bad or not)", "id", "1"));
+ assertSectionParams(engine, "{#let my= (bad or not) id=1}", Map.of("my", "(bad or not)", "id", "1"));
+
+ }
+
+ private void assertSectionParams(Engine engine, String content, Map expectedParams) {
+ Template template = engine.parse(content);
+ SectionNode node = template.findNodes(n -> n.isSection() && n.asSection().name.equals("let")).iterator().next()
+ .asSection();
+ Map params = node.getBlocks().get(0).parameters;
+ assertEquals(expectedParams, params);
+ }
+
public static class Foo {
public List- getItems() {