From 6df596a92aa7120d212b280d6d1f297ec0e09e77 Mon Sep 17 00:00:00 2001
From: Martin Kouba <mkouba@redhat.com>
Date: Tue, 16 Jun 2020 10:20:17 +0200
Subject: [PATCH] Qute - add unparsed character data

- similar to freemarker's <#noparse> and velocity's #[[
---
 docs/src/main/asciidoc/qute-reference.adoc    |  6 +++-
 .../src/main/java/io/quarkus/qute/Parser.java | 33 ++++++++++++++++---
 .../test/java/io/quarkus/qute/ParserTest.java | 15 +++++++++
 3 files changed, 49 insertions(+), 5 deletions(-)

diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc
index 445731178971e..ae59583784fb4 100644
--- a/docs/src/main/asciidoc/qute-reference.adoc
+++ b/docs/src/main/asciidoc/qute-reference.adoc
@@ -80,7 +80,7 @@ NOTE: In Quarkus, the caching is done automatically.
 The dynamic parts of a template include:
 
 * *Comment* 
-** `{! This is a comment !}`,
+** Starts with `{!` and ends with `!}`:  `{! This is a comment !}`,
 ** Could be multi-line,
 ** May contain expressions and sections: `{! {#if true} !}`.
 * *Expression*
@@ -93,6 +93,10 @@ The dynamic parts of a template include:
 ** 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.
+* *Unparsed Character Data*
+** Starts with `{[` and ends with `]}`: `{[ <script>if(true){alert('Qute is cute!')};</script> ]}`,
+** Could be multi-line,
+** Used to mark the content that should be rendered but not parsed.
 
 === Identifiers
 
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 7b7324a3d189f..b96899989989d 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
@@ -37,6 +37,8 @@ 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 CDATA_START_DELIMITER = '[';
+    private static final char CDATA_END_DELIMITER = ']';
     private static final char UNDERSCORE = '_';
     private static final char ESCAPE_CHAR = '\\';
 
@@ -151,6 +153,9 @@ private void processCharacter(char character) {
             case COMMENT:
                 comment(character);
                 break;
+            case CDATA:
+                cdata(character);
+                break;
             case TAG_CANDIDATE:
                 tagCandidate(character);
                 break;
@@ -193,6 +198,17 @@ private void comment(char character) {
         }
     }
 
+    private void cdata(char character) {
+        if (character == END_DELIMITER && buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == CDATA_END_DELIMITER) {
+            // End of cdata
+            state = State.TEXT;
+            buffer.deleteCharAt(buffer.length() - 1);
+            flushText();
+        } else {
+            buffer.append(character);
+        }
+    }
+
     private void tag(char character) {
         if (character == END_DELIMITER) {
             flushTag();
@@ -205,8 +221,15 @@ private void tagCandidate(char character) {
         if (isValidIdentifierStart(character)) {
             // Real tag start, flush text if any
             flushText();
-            state = character == COMMENT_DELIMITER ? State.COMMENT : State.TAG_INSIDE;
-            buffer.append(character);
+            if (character == COMMENT_DELIMITER) {
+                buffer.append(character);
+                state = State.COMMENT;
+            } else if (character == CDATA_START_DELIMITER) {
+                state = State.CDATA;
+            } else {
+                buffer.append(character);
+                state = State.TAG_INSIDE;
+            }
         } else {
             // Ignore expressions/tags starting with an invalid identifier
             buffer.append(START_DELIMITER).append(character);
@@ -219,8 +242,9 @@ private void tagCandidate(char 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
+        // A valid identifier must start with a digit, alphabet, underscore, comment delimiter, cdata start delimiter or a tag command (e.g. # for sections)
+        return Tag.isCommand(character) || character == COMMENT_DELIMITER || character == CDATA_START_DELIMITER
+                || character == UNDERSCORE
                 || Character.isDigit(character)
                 || Character.isAlphabetic(character);
     }
@@ -570,6 +594,7 @@ enum State {
         TAG_CANDIDATE,
         COMMENT,
         ESCAPE,
+        CDATA,
 
     }
 
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 a20a52f1fbcf7..d22372ac0caf7 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
@@ -165,6 +165,21 @@ public void testWhitespace() {
         assertEquals("Hello world", engine.parse("Hello {name ?: 'world'  }").render());
     }
 
+    @Test
+    public void testCdata() {
+        Engine engine = Engine.builder().addDefaults().build();
+        String jsSnippet = "<script>const foo = function(){alert('bar');};</script>";
+        try {
+            engine.parse("Hello {name} " + jsSnippet);
+            fail();
+        } catch (Exception expected) {
+        }
+        assertEquals("Hello world <script>const foo = function(){alert('bar');};</script>", engine.parse("Hello {name} {["
+                + jsSnippet
+                + "]}").data("name", "world").render());
+        assertEquals("Hello world <strong>", engine.parse("Hello {name} {[<strong>]}").data("name", "world").render());
+    }
+
     private void assertParserError(String template, String message, int line) {
         Engine engine = Engine.builder().addDefaultSectionHelpers().build();
         try {