Skip to content

Commit

Permalink
Qute: clarify valid identifiers
Browse files Browse the repository at this point in the history
- resolves quarkusio#11249
  • Loading branch information
mkouba committed Aug 7, 2020
1 parent 9d2d55f commit 340b5f2
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 41 deletions.
57 changes: 36 additions & 21 deletions docs/src/main/asciidoc/qute-reference.adoc
Expand Up @@ -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.
Expand All @@ -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]
----
<html>
<body>
{_foo} <1>
{ foo} <2>
{{foo}} <3>
{"foo":true} <4>
{_foo.bar} <1>
{! comment !}<2>
{ foo} <3>
{{foo}} <4>
{"foo":true} <5>
</body>
</html>
----
<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

Expand Down Expand Up @@ -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 <<literals, literal>> value.
In the `object.property` (dot notation) syntax, the `property` must be a <<identifiers,valid identifier>.
In the `object[property_name]` (bracket notation) syntax, the `property_name` has to be a non-null <<literals, literal>> value.
An expression could start with an optional namespace followed by a colon (`:`).

.Expressions Example
Expand All @@ -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 <<literals, literal>> value.

.Virtual Method Example
.Virtual Methods Example
[source]
----
{item.getLabels(1)} <1>
Expand All @@ -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 <<literals, literal>> value.

[[literals]]
===== Supported Literals

Expand Down
Expand Up @@ -68,7 +68,7 @@ public static List<String> 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<String> builder = ImmutableList.builder();
Expand All @@ -77,7 +77,7 @@ public static List<String> 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);
Expand All @@ -90,7 +90,7 @@ public static List<String> splitParts(String value, SplitConfig splitConfig) {
if (splitConfig.shouldAppendSeparator(c)) {
buffer.append(c);
}
separator = true;
separator = c;
} else {
buffer.append(c);
}
Expand Down Expand Up @@ -128,10 +128,10 @@ public static List<String> splitParts(String value, SplitConfig splitConfig) {
}
buffer.append(c);
}
separator = false;
separator = 0;
} else {
buffer.append(c);
separator = false;
separator = 0;
}
}
}
Expand Down Expand Up @@ -180,11 +180,6 @@ public boolean shouldAppendSeparator(char candidate) {
return candidate == '[';
}

@Override
public boolean ignoreAdjacentSeparator(char candidate) {
return candidate == '.';
}

}

interface SplitConfig {
Expand All @@ -203,10 +198,6 @@ default boolean shouldAppendSeparator(char candidate) {
return false;
}

default boolean ignoreAdjacentSeparator(char candidate) {
return isSeparator(candidate);
}

}

}
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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());
}
}

Expand Down
Expand Up @@ -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"));
Expand All @@ -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"));
Expand Down
Expand Up @@ -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 {
Expand Down

0 comments on commit 340b5f2

Please sign in to comment.