Merge pull request #7274 from mkouba/issue-6621

Qute - ignore expressions/tags starting with invalid identifiers
This commit is contained in:
Guillaume Smet
2020-02-19 17:20:41 +01:00
committed by GitHub
3 changed files with 85 additions and 17 deletions

View File

@@ -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]
----
<html>
<body>
{_foo} <1>
{ foo} <2>
{{foo}} <3>
{"foo":true} <4>
</body>
</html>
----
<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:

View File

@@ -33,6 +33,9 @@ class Parser implements Function<String, Expression> {
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<String, Expression> {
case TEXT:
text(character);
break;
case ESCAPE:
escape(character);
break;
case TAG_INSIDE:
tag(character);
break;
@@ -150,9 +156,20 @@ class Parser implements Function<String, Expression> {
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<String, Expression> {
}
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, Expression> {
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<String, Expression> {
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<String, Expression> {
// 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<String, String> typeInfos = typeInfoStack.peek();
@@ -512,18 +534,22 @@ class Parser implements Function<String, Expression> {
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<String, Expression> {
TAG_INSIDE,
TAG_CANDIDATE,
COMMENT,
ESCAPE,
}

View File

@@ -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()