diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc
index a627e5d6d4b1e..cfbaf92102c28 100644
--- a/docs/src/main/asciidoc/qute-reference.adoc
+++ b/docs/src/main/asciidoc/qute-reference.adoc
@@ -89,7 +89,7 @@ The dynamic parts of a template include:
** Virtual methods: `{item.get(name)}`, `{name ?: 'John'}`,
** With namespace: `{inject:colors}`.
* *Section*
-** May contain expressions and sections: `{#if foo}{foo.name}{/if}`,
+** May contain expressions and nested sections: `{#if foo}{foo.name}{/if}`,
** The name in the closing tag is optional: `{#if active}ACTIVE!{/}`,
** Can be empty: `{#myTag image=true /}`,
** May declare nested section blocks: `{#if item.valid} Valid. {#else} Invalid. {/if}` and decide which block to render.
@@ -98,31 +98,46 @@ The dynamic parts of a template include:
** Could be multi-line,
** Used to mark the content that should be rendered but not parsed.
-=== Identifiers
+[[identifiers]]
+=== Identifiers and Tags
-Expressions/tags must start with a curly bracket (`{`) followed by a valid identifier.
-A valid identifier is a digit, an alphabet character, underscore (`_`), or a section command (`#`).
-Expressions/tags starting with an invalid identifier are ignored.
-A closing curly bracket (`}`) is ignored if not inside an expression/tag.
+Identifiers are used in expressions and section tags.
+A valid identifier is a sequence of non-whitespace characters.
+However, users are encouraged to only use valid Java identifiers in expressions.
+
+TIP: You can use bracket notation if you need to specify an identifier that contains a dot, e.g. `{map['my.key']}`.
+
+When parsing a template document the parser identifies all _tags_.
+A tag starts and ends with a curly bracket, e.g. `{foo}`.
+The content of a tag must start with:
+
+* a digit, or
+* an alphabet character, or
+* underscore, or
+* a built-in command: `#`, `!`, `@`, `/`.
+
+If it does not start with any of the above it is ignored by the parser.
-.hello.html
+.Tag Examples
[source,html]
----
- {_foo} <1>
- { foo} <2>
- {{foo}} <3>
- {"foo":true} <4>
+ {_foo.bar} <1>
+ {! comment !}<2>
+ { foo} <3>
+ {{foo}} <4>
+ {"foo":true} <5>
----
-<1> Evaluated: expression starts with underscore.
-<2> Ignored: expression starts with whitespace.
-<3> Ignored: expression starts with `{`.
-<4> Ignored: expression starts with `"`.
+<1> Parsed: an expression that starts with underscore.
+<2> Parsed: a comment
+<3> Ignored: starts with whitespace.
+<4> Ignored: starts with `{`.
+<5> Ignored: starts with `"`.
-TIP: It is also possible to use escape sequences `\{` and `\}` to insert delimiters in the text. In fact, an escape sequence is usually only needed for the start delimiter, ie. `\\{foo}` will be rendered as `{foo}` (no evaluation will happen).
+TIP: It is also possible to use escape sequences `\{` and `\}` to insert delimiters in the text. In fact, an escape sequence is usually only needed for the start delimiter, ie. `\\{foo}` will be rendered as `{foo}` (no parsing/evaluation will happen).
=== Removing Standalone Lines From the Template
@@ -186,7 +201,8 @@ In this case, all whitespace characters from a standalone line will be printed t
An expression outputs a value.
It consists of one or more parts separated by dot (dot notation) or square brackets (bracket notation).
-In the `object[property_name]` syntax, the `property_name` has to be a non-null <> value.
+In the `object.property` (dot notation) syntax, the `property` must be a <.
+In the `object[property_name]` (bracket notation) syntax, the `property_name` has to be a non-null <> value.
An expression could start with an optional namespace followed by a colon (`:`).
.Expressions Example
@@ -202,9 +218,10 @@ An expression could start with an optional namespace followed by a colon (`:`).
<3> equivalent to `{item.name}` but using the bracket notation
<4> namespace `global`, one part: `colors`
-A part of an expression could be a _virtual method_ in which case the name can be followed by a list of comma-separated parameters in parentheses:
+A part of an expression could be a _virtual method_ in which case the name can be followed by a list of comma-separated parameters in parentheses.
+A parameter of a virtual method can be either a nested expression or a <> value.
-.Virtual Method Example
+.Virtual Methods Example
[source]
----
{item.getLabels(1)} <1>
@@ -213,8 +230,6 @@ A part of an expression could be a _virtual method_ in which case the name can b
<1> no namespace, two parts - `item`, `getLabels(1)`, the second part is a virtual method with name `getLabels` and params `1`
<2> infix notation, translated to `name.or('John')`; no namespace, two parts - `name`, `or('John')`
-NOTE: A parameter of a virtual method can be a <> value.
-
[[literals]]
===== Supported Literals
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java
index 989d8b0f56792..d6579cec58ea9 100644
--- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java
+++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Expressions.java
@@ -68,7 +68,7 @@ public static List splitParts(String value, SplitConfig splitConfig) {
return Collections.emptyList();
}
boolean literal = false;
- boolean separator = false;
+ char separator = 0;
byte infix = 0;
byte brackets = 0;
ImmutableList.Builder builder = ImmutableList.builder();
@@ -77,7 +77,7 @@ public static List splitParts(String value, SplitConfig splitConfig) {
char c = value.charAt(i);
if (splitConfig.isSeparator(c)) {
// Adjacent separators may be ignored
- if (!separator || !splitConfig.ignoreAdjacentSeparator(c)) {
+ if (separator == 0 || separator != c) {
if (!literal && brackets == 0) {
if (splitConfig.shouldPrependSeparator(c)) {
buffer.append(c);
@@ -90,7 +90,7 @@ public static List splitParts(String value, SplitConfig splitConfig) {
if (splitConfig.shouldAppendSeparator(c)) {
buffer.append(c);
}
- separator = true;
+ separator = c;
} else {
buffer.append(c);
}
@@ -128,10 +128,10 @@ public static List splitParts(String value, SplitConfig splitConfig) {
}
buffer.append(c);
}
- separator = false;
+ separator = 0;
} else {
buffer.append(c);
- separator = false;
+ separator = 0;
}
}
}
@@ -180,11 +180,6 @@ public boolean shouldAppendSeparator(char candidate) {
return candidate == '[';
}
- @Override
- public boolean ignoreAdjacentSeparator(char candidate) {
- return candidate == '.';
- }
-
}
interface SplitConfig {
@@ -203,10 +198,6 @@ default boolean shouldAppendSeparator(char candidate) {
return false;
}
- default boolean ignoreAdjacentSeparator(char candidate) {
- return isSeparator(candidate);
- }
-
}
}
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 8ffb3effd7ee4..3a66f57b892b8 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
@@ -289,6 +289,20 @@ private boolean isValidIdentifierStart(char character) {
|| Character.isAlphabetic(character);
}
+ static boolean isValidIdentifier(String value) {
+ int offset = 0;
+ int length = value.length();
+ while (offset < length) {
+ int c = value.codePointAt(offset);
+ if (!Character.isWhitespace(c)) {
+ offset += Character.charCount(c);
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
private boolean isLineSeparatorStart(char character) {
return character == LINE_SEPARATOR_CR || character == LINE_SEPARATOR_LF;
}
@@ -670,6 +684,15 @@ static ExpressionImpl parseExpression(String value, Scope scope, Origin origin)
Part first = null;
for (String strPart : strParts) {
Part part = createPart(namespace, first, strPart, scope, origin);
+ if (!isValidIdentifier(part.getName())) {
+ StringBuilder builder = new StringBuilder("Invalid identifier found [");
+ builder.append(value).append("]");
+ if (!origin.getTemplateId().equals(origin.getTemplateGeneratedId())) {
+ builder.append(" in template [").append(origin.getTemplateId()).append("]");
+ }
+ builder.append(" on line ").append(origin.getLine());
+ throw new TemplateException(builder.toString());
+ }
if (first == null) {
first = part;
}
@@ -701,7 +724,7 @@ private static Part createPart(String namespace, Part first, String value, Scope
builder.append(" in template [").append(origin.getTemplateId()).append("]");
}
builder.append(" on line ").append(origin.getLine());
- throw new IllegalArgumentException(builder.toString());
+ throw new TemplateException(builder.toString());
}
}
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 3e0e1eb6e16bb..3d24dff4f9dca 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
@@ -18,7 +18,8 @@ public class ExpressionTest {
@Test
public void testExpressions() throws InterruptedException, ExecutionException {
verify("data:name.value", "data", null, name("name", "name"), name("value", "value"));
- verify("name.value", null, null, name("name"), name("value"));
+ // ignore adjacent separators
+ verify("name..value", null, null, name("name"), name("value"));
verify("0", null, CompletableFuture.completedFuture(Integer.valueOf(0)), name("0", "|java.lang.Integer|"));
verify("false", null, CompletableFuture.completedFuture(Boolean.FALSE), name("false", "|java.lang.Boolean|"));
verify("null", null, CompletableFuture.completedFuture(null), name("null"));
@@ -41,19 +42,20 @@ public void testExpressions() throws InterruptedException, ExecutionException {
virtualMethod("call", ExpressionImpl.from("bar.alpha(1)"), ExpressionImpl.from("bar.alpha('ping')")));
verify("'foo:bar'", null, CompletableFuture.completedFuture("foo:bar"), name("'foo:bar'", "|java.lang.String|"));
// bracket notation
- verify("name['value']", null, null, name("name"), name("value"));
- verify("name[false]", null, null, name("name"), name("false"));
+ // ignore adjacent separators
+ verify("name[['value']", null, null, name("name"), name("value"));
+ verify("name[false]]", null, null, name("name"), name("false"));
verify("name[1l]", null, null, name("name"), name("1"));
try {
verify("name['value'][1][null]", null, null);
fail();
- } catch (IllegalArgumentException expected) {
+ } catch (TemplateException expected) {
assertTrue(expected.getMessage().contains("Null value"));
}
try {
verify("name[value]", null, null);
fail();
- } catch (IllegalArgumentException expected) {
+ } catch (TemplateException expected) {
assertTrue(expected.getMessage().contains("Non-literal value"));
}
//verify("name[1l]['foo']", null, null, name("name"), name("1"), name("foo"));
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 5ad5ed961cb79..d6e5a9c12a6ef 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
@@ -201,6 +201,27 @@ public void testRemoveStandaloneLines() {
assertEquals("bar\n", engine.parse("{foo}\n").data("foo", "bar").render());
}
+ @Test
+ public void testValidIdentifiers() {
+ assertTrue(Parser.isValidIdentifier("foo"));
+ assertTrue(Parser.isValidIdentifier("_foo"));
+ assertTrue(Parser.isValidIdentifier("foo$$bar"));
+ assertTrue(Parser.isValidIdentifier("1Foo_$"));
+ assertTrue(Parser.isValidIdentifier("1"));
+ assertTrue(Parser.isValidIdentifier("1?"));
+ assertTrue(Parser.isValidIdentifier("1:"));
+ assertTrue(Parser.isValidIdentifier("-foo"));
+ assertTrue(Parser.isValidIdentifier("foo["));
+ assertTrue(Parser.isValidIdentifier("foo^"));
+ Engine engine = Engine.builder().addDefaults().build();
+ try {
+ engine.parse("{foo\nfoo}");
+ fail();
+ } catch (Exception expected) {
+ assertTrue(expected.getMessage().contains("Invalid identifier found"), expected.toString());
+ }
+ }
+
private void assertParserError(String template, String message, int line) {
Engine engine = Engine.builder().addDefaultSectionHelpers().build();
try {