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() {