From 7c4fd04f58976e37d769dbdedbe4f8a886aff4d8 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 18 Feb 2020 14:11:13 +0100 Subject: [PATCH] Qute - ignore expressions/tags starting with invalid identifiers - make it possible to use escape sequences to insert delimiters - update docs - resolves #6621 --- docs/src/main/asciidoc/qute-reference.adoc | 26 ++++++++ .../src/main/java/io/quarkus/qute/Parser.java | 61 +++++++++++++------ .../test/java/io/quarkus/qute/ParserTest.java | 15 +++++ 3 files changed, 85 insertions(+), 17 deletions(-) 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()