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 {