diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc
index ef3dc8586..829bc6acb 100644
--- a/docs/src/main/asciidoc/qute-reference.adoc
+++ b/docs/src/main/asciidoc/qute-reference.adoc
@@ -94,6 +94,32 @@ The dynamic parts of a template include:
** can be empty: `{#myTag image=true /}`,
** may declare nested section blocks: `{#if item.valid} Valid. {#else} Invalid. {/if}` and decide which block to render.
+=== Identifiers
+
+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.
+
+.hello.html
+[source,html]
+----
+
+
+ {_foo} <1>
+ { foo} <2>
+ {{foo}} <3>
+ {"foo":true} <4>
+
+
+----
+<1> Evaluated: expression starts with underscore.
+<2> Ignored: expression starts with whitespace.
+<3> Ignored: expression starts with `{`.
+<4> Ignored: expression 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).
+
==== Expressions
An expression consists of:
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 3db90dbc7..23eb47d83 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
@@ -33,6 +33,9 @@ class Parser implements Function {
private static final char START_DELIMITER = '{';
private static final char END_DELIMITER = '}';
private static final char COMMENT_DELIMITER = '!';
+ private static final char UNDERSCORE = '_';
+ private static final char ESCAPE_CHAR = '\\';
+
// Linux, BDS, etc.
private static final char LINE_SEPARATOR_LF = '\n';
// Mac OS 9, ZX Spectrum :-), etc.
@@ -135,6 +138,9 @@ class Parser implements Function {
case TEXT:
text(character);
break;
+ case ESCAPE:
+ escape(character);
+ break;
case TAG_INSIDE:
tag(character);
break;
@@ -150,9 +156,20 @@ class Parser implements Function {
lineCharacter++;
}
+ private void escape(char character) {
+ if (character != START_DELIMITER && character != END_DELIMITER) {
+ // Invalid escape sequence is just ignored
+ buffer.append(ESCAPE_CHAR);
+ }
+ buffer.append(character);
+ state = State.TEXT;
+ }
+
private void text(char character) {
if (character == START_DELIMITER) {
state = State.TAG_CANDIDATE;
+ } else if (character == ESCAPE_CHAR) {
+ state = State.ESCAPE;
} else {
if (isLineSeparator(character)) {
line++;
@@ -181,24 +198,29 @@ class Parser implements Function {
}
private void tagCandidate(char character) {
- if (Character.isWhitespace(character)) {
+ if (isValidIdentifierStart(character)) {
+ // Real tag start, flush text if any
+ flushText();
+ state = character == COMMENT_DELIMITER ? State.COMMENT : State.TAG_INSIDE;
+ buffer.append(character);
+ } else {
+ // Ignore expressions/tags starting with an invalid identifier
buffer.append(START_DELIMITER).append(character);
if (isLineSeparator(character)) {
line++;
lineCharacter = 1;
}
state = State.TEXT;
- } else if (character == START_DELIMITER) {
- buffer.append(START_DELIMITER).append(START_DELIMITER);
- state = State.TEXT;
- } else {
- // Real tag start, flush text if any
- flushText();
- state = character == COMMENT_DELIMITER ? State.COMMENT : State.TAG_INSIDE;
- buffer.append(character);
}
}
+ private boolean isValidIdentifierStart(char character) {
+ // A valid identifier must start with a digit, alphabet, underscore, comment delimiter or a tag command (e.g. # for sections)
+ return Tag.isCommand(character) || character == COMMENT_DELIMITER || character == UNDERSCORE
+ || Character.isDigit(character)
+ || Character.isAlphabetic(character);
+ }
+
private boolean isLineSeparator(char character) {
return character == LINE_SEPARATOR_CR
|| (character == LINE_SEPARATOR_LF
@@ -218,7 +240,7 @@ class Parser implements Function {
String content = buffer.toString();
String tag = START_DELIMITER + content + END_DELIMITER;
- if (content.charAt(0) == Tag.SECTION.getCommand()) {
+ if (content.charAt(0) == Tag.SECTION.command) {
boolean isEmptySection = false;
if (content.charAt(content.length() - 1) == Tag.SECTION_END.command) {
@@ -307,7 +329,7 @@ class Parser implements Function {
sectionStack.addFirst(sectionNode);
}
}
- } else if (content.charAt(0) == Tag.SECTION_END.getCommand()) {
+ } else if (content.charAt(0) == Tag.SECTION_END.command) {
SectionBlock.Builder block = sectionBlockStack.peek();
SectionNode.Builder section = sectionStack.peek();
String name = content.substring(1, content.length());
@@ -336,7 +358,7 @@ class Parser implements Function {
// Remove the last type info map from the stack
typeInfoStack.pop();
- } else if (content.charAt(0) == Tag.PARAM.getCommand()) {
+ } else if (content.charAt(0) == Tag.PARAM.command) {
// {@org.acme.Foo foo}
Map typeInfos = typeInfoStack.peek();
@@ -512,18 +534,22 @@ class Parser implements Function {
EXPRESSION(null),
SECTION('#'),
SECTION_END('/'),
- SECTION_BLOCK(':'),
PARAM('@'),
;
- private final Character command;
+ final Character command;
- private Tag(Character command) {
+ Tag(Character command) {
this.command = command;
}
- public Character getCommand() {
- return command;
+ static boolean isCommand(char command) {
+ for (Tag tag : Tag.values()) {
+ if (tag.command != null && tag.command == command) {
+ return true;
+ }
+ }
+ return false;
}
}
@@ -534,6 +560,7 @@ class Parser implements Function {
TAG_INSIDE,
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 b2fc918e4..25ab3564a 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
@@ -37,6 +37,21 @@ public class ParserTest {
"Parser error on line 2: no section helper found for {#foo test/}", 2);
}
+ @Test
+ public void testIgnoreInvalidIdentifier() {
+ Engine engine = Engine.builder().addDefaults().build();
+ assertEquals("{\"foo\":\"bar\"} bar {'} baz ZX80",
+ engine.parse("{\"foo\":\"bar\"} {_foo} {'} {1foo} {čip}").data("_foo", "bar").data("1foo", "baz")
+ .data("čip", "ZX80").render());
+ }
+
+ @Test
+ public void testEscapingDelimiters() {
+ Engine engine = Engine.builder().addDefaults().build();
+ assertEquals("{foo} bar \\ignored {čip}",
+ engine.parse("\\{foo\\} {foo} \\ignored \\{čip}").data("foo", "bar").render());
+ }
+
@Test
public void testTypeCheckInfos() {
Engine engine = Engine.builder().addDefaultSectionHelpers()