18

Introduction

I'm currently creating a templatebuilder where users can build a template for an app. The user can drag and drop multiple blocks, such as text blocks and 'custom code' blocks. The template will be parsed within an app. Right now, a template could look like this:

<section>
    <div class="row">
        <div class="col-sm-12">
            <section data-type="code">
                <#code></#code>
            </section>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12" data-type="container-content">
            <section data-type="text">
                <u>Lorem</u> ipsum
            </section>
        </div>
    </div>
</section>

So, this template contains two elements (see the data-type attribute): one part is custom written code. Here, the user wrote custom code, including Apache Freemarker code. The second part is custom written text.

Situation

The code above will be used in two different ways.

  • Exactly this code will be used inside an app that's using the template (so that's why they should be able to write Freemarker code, because this will be parsed).
  • On my website, the user should be able to edit this template. Because the code is stored in the database as written above, there is a problem:

Problem

When I directly render the template in the web-interface, the text part will render correctly with the <u></u> tags, but the code part will be rendered as html as well which might cause weird behaviour (such as the freemarker notation </#list> being auto-converted to <!--#list-->).

But, if I render the full template as text only, the text part with the <u></u> tags will not be rendered as well.

Expected outcome

I want to read the template variable with JavaScript / jQuery and then parse each data-type with text as html, and with code as text.

How can I loop through the template and do this?

Jordy
  • 4,719
  • 11
  • 47
  • 81
  • This is interesting... Is this whole thing compiles and runs on the client side? – Soolie Dec 20 '17 at 15:46
  • This should probably be done *before* the html is rendered on the browsers side – Jonas Wilms Dec 20 '17 at 15:49
  • @Soolie yes, it is! – Jordy Dec 20 '17 at 15:54
  • Should not `$('[data-type="code"]').each(function () {$(this).text($(this).html())});` do the trick? – Philipp Maurer Dec 20 '17 at 16:21
  • No, because `$('[data-type="code"]')` only works on templates that are already parsed as html. And when I do that, the code part will be parsed and that's what I want to prevent. – Jordy Dec 20 '17 at 16:29
  • damn.. i really interest with this. but can you provide `<#code>codeExample#code>` and give us the result and expected result. (in SO code block not in text explanation). because i never use apache freermaker before – plonknimbuzz Dec 24 '17 at 20:50
  • i forgot: how do you convert all of those? `
    restYourExampleAbove
    ` then throw `$(".containerThemeBuilder").html()` to ajax?
    – plonknimbuzz Dec 24 '17 at 20:54
  • @plonknimbuzz It's not about the code within `<#code>#code>` or the fact that it's using Freemarker; it's that the combination of some characters are not supported. See this: https://jsfiddle.net/akzL6cee/2/. So, it converts the `#code>` to `` (see source code). Is this the info you need? – Jordy Dec 25 '17 at 22:07
  • is there any possibility of loading this using AJAX and pre-processing before rendering? – Eduardo Poço Dec 25 '17 at 23:01
  • Yes, that would be possible. But still: how to run part of the code as html and part of the code as text? – Jordy Dec 25 '17 at 23:48
  • i was thinking to send whole data as text then parse it with dom or regex. then parse `data-type=code` to code and `data-type=text` to text. btw, what happen if we throw/send this: `<#code>test#code>` to apache freemaker? – plonknimbuzz Dec 26 '17 at 16:43
  • I think the solution might be something like that, yes. But not exactly sure how to accomplish it... `<#code>test#code>` doesn't make sense. The `#code>` is still rendered as ``. – Jordy Dec 26 '17 at 23:33
  • What about just replacing the offending combinations with html escaped versions?` like this: `$("#output1").html( newHTML.replace('<#','<#').replace('#','</#') ) ` https://jsfiddle.net/wgou4Lsc/ – Tiago Coelho Dec 27 '17 at 10:28
  • @Jordy here what i mean: http://jsbin.com/madonigipi/edit?html i hope you can solve your prob using eduardo answer. – plonknimbuzz Dec 27 '17 at 18:40
  • Thank you for the help! I checked it. Unfortunately, the `<#code>test#code>` is not executed correctly (the `#code>` statement is missing, and transformed to ``. Indeed, I can solve this particular problem with Eduardo's answer, but I still cant prevent the code from being executed at all.. – Jordy Dec 27 '17 at 22:13

5 Answers5

8

There is an alternative syntax that uses square brackets instead of angle brackets.

Check if it solves your tag identifying problem without messing any other feature.

https://freemarker.apache.org/docs/dgui_misc_alternativesyntax.html

EDIT 1

To show the source code inside the <#code> tags when the HTML is parsed, you could escape it in your database (escape html special chars like <, > and & to &lt; &gt; and &amp;). So, when it is rendered, no html tags will be created in the code content and the document won't be messed up.

Then, you can render all the content of the database directly as HTML: text will keep markup and code will be text.

To do that modification, you can use regular expressions to find what is enclosed by <#code> tags and replace with the HTML-escaped equivalent. The exact way to do it depends on the language you will be using for the job, as there are some differences in RegExes and in the available escape funcions.

EDIT 2

If you are loading the content using AJAX, you have the chance of applying the replace in javascript, AFTER the content was obtained from the server, keeping your database as it is.

Eduardo Poço
  • 2,819
  • 1
  • 19
  • 27
  • Yes, great point! This solves the problem for the angle brackets for sure. Any ideas how to prevent execution of the code between `
    ` at all?
    – Jordy Dec 26 '17 at 23:36
  • Is it an option to escape the code that is already in your database? I find it difficult to recover the html code that may already been rendered, eventually messing the following tags. – Eduardo Poço Dec 28 '17 at 01:05
  • I think it might be the only option I have. So I have to do it that way, or find another workaround. – Jordy Dec 28 '17 at 23:54
2

Recap of the problem

For parsing HTML in javascript, you generally use a DOMParser object (supported by IE10+).

Like you said, parsing fails inside the data-type="code" section, because it does not know how to handle the </#...> tags...

const templ = `<section><div class="row"><div class="col-sm-12"><section data-type="code"><#code></#code></section></div></div><div class="row"><div class="col-sm-12" data-type="container-content"><section data-type="text"><u>Lorem</u> ipsum</section></div></div></section>`;
const parser = new DOMParser();
const doc = parser.parseFromString(templ, "text/html");

console.log(
  "Wrongly parsed </#code> tag:\n",
  doc.querySelector("[data-type='code']").innerHTML
);

Finding a way around

Now, it might sound like a good idea to try and do a quick regex find-and-replace on the characters that need to be escaped, but I wouldn't recommend it...

As far as I know, there's no way to "break in" to the parsing process or pass a strategy for certain types of elements...

I'd say this leaves you with two options. Either:

  1. Don't use the unparsable syntax inside the code section, as suggested by user Eduardo Poço in their answer

or, (my prefered direction), try to

  1. Modify the template itself to stop parsing the contents of the code sections all together

Using a modified template

There's a tag for "script" like content in HTML! It's, unsurprisingly, the <script> tag. Let's inject it in our code section:

<section data-type="code">
    <script type="text">
        <#code></#code>
    </script>
</section>

The DOMParser won't touch this tag, leaving it exactly as is:

const templ = '<section><div class="row"><div class="col-sm-12"><section data-type="code"><script type="text"><#code></#code></' + 'script></section></div></div><div class="row"><div class="col-sm-12" data-type="container-content"><section data-type="text"><u>Lorem</u> ipsum</section></div></div></section>';

const parser = new DOMParser();
const doc = parser.parseFromString(templ, "text/html");

console.log(
  "Now, there's a <script> tag:\n",
  doc.querySelector("[data-type='code']").innerHTML
);

Note that I had to join the template string from two parts to make sure stackoverflow's snippet doesn't break. Are they experiencing a similar issue? :-o


Now, all we have to do is use the general DOM methods, including innerText (not innerHTML) to get the script's inner content back in to the visible part of the DOM:

var templ = '<section><div class="row"><div class="col-sm-12"><section data-type="code"><script type="text"><#code></#code></' + 'script></section></div></div><div class="row"><div class="col-sm-12" data-type="container-content"><section data-type="text"><u>Lorem</u> ipsum</section></div></div></section>`;'

var parser = new DOMParser();
var doc = parser.parseFromString(templ, "text/html");

Array
  .from(doc.querySelectorAll(
    "[data-type='code'] > script")
  )
  .forEach(script => {
      const codeTag = document.createElement("code");
      codeTag.innerText = script.innerHTML;
      script.replaceWith(codeTag);
  });

document.getElementById("wrapper").appendChild(doc.body.firstChild);
code { background: #efefef; }
<div id="wrapper"></div>
user3297291
  • 22,592
  • 4
  • 29
  • 45
1

You could use charset codes, so it does not execute before outputting. HTML charset reference

They can edit it, since it appears normal and send it back to you or server. Make sure you include your charset reference in the head.

<meta charset="UTF-8"> // HTML5 
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"> // HTML4

<!-- @CHARSET / No execute -->
&#60;section&#62;
    &#60;div class="row"&#62;
        &#60;div class="col-sm-12"&#62;
            &#60;section data-type="code"&#62;
                <#code> <!-- Run this --> </#code>
            &#60;/section&#62;
        &#60;/div&#62;
    &#60;/div&#62;
    &#60;div class="row"&#62;
        &#60;div class="col-sm-12" data-type="container-content"&#62;
            &#60;section data-type="text"&#62;
                &#60;u&#62; <!-- Don't run --> &#60;/u&#62; 
            &#60;/section&#62;
        &#60;/div&#62;
    &#60;/div&#62;
&#60;/section&#62;
Jordy
  • 4,719
  • 11
  • 47
  • 81
ABC
  • 2,068
  • 1
  • 10
  • 21
  • But still, as soon as I parse it, all the code will be executed so it doesn't make any sense. What I want is to execute all code, except the code between the `
    ` tags.
    – Jordy Dec 26 '17 at 23:35
1

If i didn't misunderstand it, you can use <plaintext> tag for rendering block as text on the page.

<plaintext>
    <#code></#code>
</plaintext>
Tugay İlik
  • 3,688
  • 1
  • 14
  • 21
  • It's obsolete unfortunately. And it doesn't have a closing tag so I guess it would render all code after the tag as text. – Jordy Dec 28 '17 at 23:53
1

What about something like this?

// https://stackoverflow.com/questions/7477/autosizing-textarea-using-prototype
function FitToContent(id, maxHeight)
{
   var text = id && id.style ? id : document.getElementById(id);
   if (!text)
      return;

   /* Accounts for rows being deleted, pixel value may need adjusting */
   if (text.clientHeight == text.scrollHeight) {
      text.style.height = "30px";
   }

   var adjustedHeight = text.clientHeight;
   if (!maxHeight || maxHeight > adjustedHeight)
   {
      adjustedHeight = Math.max(text.scrollHeight, adjustedHeight);
      if (maxHeight)
         adjustedHeight = Math.min(maxHeight, adjustedHeight);
      if (adjustedHeight > text.clientHeight)
         text.style.height = adjustedHeight + "px";
   }
}

$('textarea').each(function(i,v){
  FitToContent($(v)[0], document.documentElement.clientHeight)
});
textarea {
  background: transparent;
  border: 0;
  font-family: 'Times New Roman';
  font-size: 1em;
  width: 100%;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<section>
    <div class="row">
        <div class="col-sm-12">
            <h4>First code block</h4>
            <section data-type="code">
                <textarea class='code'><#code>
    <h2>FreeMarker Spring MVC Hello World</h2>
    <table class="datatable">
        <tr>
            <th>Make</th><th>Model</th>
        </tr>
        <#list model["carList"] as car>
        <tr>
            <td>${car.make}</td>
            <td>${car.model}</td>
        </tr>
        </#list>
    </table>
</#code></textarea>
            </section>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-12" data-type="container-content">
            <h4>Regular HTML section</h4>
            <section data-type="text">
                <u>Lorem</u> ipsum
            </section>
        </div>
    </div>
      <div class="row">
        <div class="col-sm-12">
            <h4>Second code block</h4>
            <section data-type="code">
                <textarea class='code'><#code>
    <table class="datatable">
        <tr>
            <th>Name</th><th>Email</th>
        </tr>
        <#list model["personList"] as person>
        <tr>
            <td>${person.name}</td>
            <td>${person.email}</td>
        </tr>
        </#list>
    </table>
</#code></textarea>
            </section>
        </div>
    </div>
</section>
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61