Repository: quarkusio/quarkus
Issue: Qute: bug in expression parser with spaces

### Describe the bug

I can call a tag with no spaces between parameters of the following method call:

```html
            {#form uri:PersonOrGroups.accept(personOrGroup.id,global:requestUrl)}
            {/form}
```

But if I add a space between the parameters:

```html
            {#form uri:PersonOrGroups.accept(personOrGroup.id, global:requestUrl)}
            {/form}
```

I'm getting this error:

```
Caused by: io.quarkus.qute.TemplateException: Parser error in template [tags/votePersonOrGroup.html] on line 7: invalid virtual method in {uri:PersonOrGroups.accept(personOrGroup.id,}
	at io.quarkus.qute.Parser.parserError(Parser.java:505)
	at io.quarkus.qute.Expressions.parseVirtualMethodParams(Expressions.java:40)
	at io.quarkus.qute.Parser.createPart(Parser.java:784)
	at io.quarkus.qute.Parser.parseExpression(Parser.java:767)
	at io.quarkus.qute.Parser.apply(Parser.java:840)
	at io.quarkus.qute.Parser.apply(Parser.java:34)
	at io.quarkus.qute.SectionBlock$Builder.addExpression(SectionBlock.java:168)
	at io.quarkus.qute.UserTagSectionHelper$Factory.initializeBlock(UserTagSectionHelper.java:92)
	at io.quarkus.qute.Parser.flushTag(Parser.java:434)
	at io.quarkus.qute.Parser.tag(Parser.java:295)
	at io.quarkus.qute.Parser.processCharacter(Parser.java:202)
	at io.quarkus.qute.Parser.parse(Parser.java:134)
	at io.quarkus.qute.EngineImpl.load(EngineImpl.java:138)
```

### Expected behavior

_No response_

### Actual behavior

_No response_

### How to Reproduce?

_No response_

### Output of `uname -a` or `ver`

_No response_

### Output of `java -version`

_No response_

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

_No response_

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)

_No response_

### Additional information

_No response_

Changed files:
independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java, independent-projects/qute/core/src/main/java/io/quarkus/qute/Parser.java, independent-projects/qute/core/src/test/java/io/quarkus/qute/ParserTest.java

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 fa4461a393a..f3a873c62c5 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
@@ -640,6 +640,7 @@ static Iterator<String> splitSectionParams(String content, Function<String, Runt
 
         boolean stringLiteral = false;
         short composite = 0;
+        byte brackets = 0;
         boolean space = false;
         List<String> parts = new ArrayList<>();
         StringBuilder buffer = new StringBuilder();
@@ -648,7 +649,7 @@ static Iterator<String> splitSectionParams(String content, Function<String, Runt
             char c = content.charAt(i);
             if (c == ' ') {
                 if (!space) {
-                    if (!stringLiteral && composite == 0) {
+                    if (!stringLiteral && composite == 0 && brackets == 0) {
                         if (buffer.length() > 0) {
                             parts.add(buffer.toString());
                             buffer = new StringBuilder();
@@ -669,6 +670,12 @@ && isCompositeStart(c) && (i == 0 || space || composite > 0
                 } else if (!stringLiteral
                         && isCompositeEnd(c) && composite > 0) {
                     composite--;
+                } else if (!stringLiteral
+                        && Parser.isLeftBracket(c)) {
+                    brackets++;
+                } else if (!stringLiteral
+                        && Parser.isRightBracket(c) && brackets > 0) {
+                    brackets--;
                 }
                 space = false;
                 buffer.append(c);
diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java
index 1aa43522987..19e919b3172 100644
--- a/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java
+++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/ExpressionTest.java
@@ -70,7 +70,11 @@ public void testExpressions() throws InterruptedException, ExecutionException {
 
     @Test
     public void testNestedVirtualMethods() {
-        Expression exp = ExpressionImpl.from("movie.findServices(movie.name,movie.toNumber(movie.getName))");
+        assertNestedVirtualMethod(ExpressionImpl.from("movie.findServices(movie.name,movie.toNumber(movie.getName))"));
+        assertNestedVirtualMethod(ExpressionImpl.from("movie.findServices(movie.name, movie.toNumber(movie.getName) )"));
+    }
+
+    private void assertNestedVirtualMethod(Expression exp) {
         assertNull(exp.getNamespace());
         List<Expression.Part> parts = exp.getParts();
         assertEquals(2, parts.size());
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 1a12c2952f0..8b1b2c7b406 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
@@ -188,6 +188,8 @@ public void testSectionParameters() {
                 "false");
         assertParams("(item.name == 'foo') and (item.name is false)", "(item.name == 'foo')", "and", "(item.name is false)");
         assertParams("(item.name != 'foo') || (item.name == false)", "(item.name != 'foo')", "||", "(item.name == false)");
+        assertParams("foo.codePointCount(0, foo.length) baz=bar", "foo.codePointCount(0, foo.length)", "baz=bar");
+        assertParams("foo.codePointCount( 0 , foo.length( 1)) baz=bar", "foo.codePointCount( 0 , foo.length( 1))", "baz=bar");
     }
 
     @Test
@@ -372,6 +374,15 @@ public void testInvalidParamDeclaration() {
                 "Parser error on line 1: invalid parameter declaration {@\\n}", 1);
     }
 
+    @Test
+    public void testUserTagVirtualMethodParam() {
+        Engine engine = Engine.builder().addDefaults().addValueResolver(new ReflectionValueResolver())
+                .addSectionHelper(new UserTagSectionHelper.Factory("form", "form-template")).build();
+        engine.putTemplate("form-template", engine.parse("{it}"));
+        Template foo = engine.parse("{#form foo.codePointCount(0, foo.length) /}");
+        assertEquals("3", foo.data("foo", "foo").render());
+    }
+
     public static class Foo {
 
         public List<Item> getItems() {