Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FreeMarker: keep identation when using macros

i'm using the FreeMarker template engine to generate some php classes from an abstract description of a webservice. My problem is, when i call a macro in a FreeMarker template, the macro inserts the text without the lefthand whitespace before the macro call.

exampleTemplate.ftl:

<?php
    class ${class.name} {
        <@docAsComment class.doc/>

        <#list class.fields as field>
        $${field.name};
        </#list>
        <#-- ... -->
    }
?>

<#macro docAsComment doc>
/*
<#if doc.title != "">
* ${doc.title}
</#if>
<#list doc.content as content>
<#if content != ""> * ${content}</#if>
</#list>
*/
</#macro>

This will generate something like this:

<?php
    class foo {
/*
 * foo
 * bar foo, bla
 */          

    $a;
    $b;
    }
?>

One solution would be, to submit the leading whitespace as an argument to the macro, but that makes the template only more unreadable. Is there a better solution?

like image 758
klingt.net Avatar asked Mar 27 '13 12:03

klingt.net


People also ask

What is macro in FreeMarker?

Macro variable stores a template fragment (called macro definition body) that can be used as user-defined directive. The variable also stores the name of allowed parameters to the user-defined directive.

How do you escape characters in FreeMarker?

esc creates a markup output value out of a string value by escaping all special characters in it.

What is eval in FreeMarker?

eval. This built-in evaluates a string as an FTL expression. For example "1+2"? eval returns the number 3. (To render a template that's stored in a string, use the interpret built-in instead.)

What is a FreeMarker error?

If FreeMarker is formatted incorrectly within an email template, you may experience a render error when you attempt to send the email. If your email has a render error, it cannot be processed or sent out.


4 Answers

It would seem that docAsComment is always invoked at the same level of indentation in the code generate. You could bake that indentation into the macro.

If the indentation of the comment is variable, you'd have to pass in the indentation level. I don't understand your comment about that making the template harder to read. It does make the macro a little more complicated.

The invocation would look like this:

<@docAsComment class.doc 1/>

Macro would change to something like this:

<#macro docAsComment doc indent=1>
   <#local spc>${""?left_pad(indent * 4)}</#local>
${spc}/*
<#if doc.title != "">
${spc}* ${doc.title}
</#if>
<#list doc.content as content>
<#if content != "">${spc} * ${content}</#if>
</#list>
${spc}*/
</#macro>

Not too bad, really. You can make the macro a little easier to read by indenting it:

<#macro docAsComment doc indent=1>
    <#local spc>${""?left_pad(indent * 4)}</#local>
    ${spc}/*<#lt>
    <#if doc.title != "">
        ${spc}* ${doc.title}<#lt>
    </#if>
    <#list doc.content as content>
        <#if content != "">${spc} * ${content}</#if><#lt>
    </#list>
    ${spc}*/<#lt>
</#macro>
like image 145
Charles Forsythe Avatar answered Oct 14 '22 13:10

Charles Forsythe


Today, it is possible to use <#nt>. The whitespace documentation says the following about it:

White-space stripping can be disabled for a single line with the nt directive (for No Trim).

According to the V2.3 changelog, in previous versions, lines containing only FTL tags get trimmed, with the exception of <#include> and custom directives (like <@macroname>). But in V2.3 they changed this behavior to ALWAYS trim such lines. So, when using your macro, you may put <#nt> on the line to prevent trimming, and so, keeping indentation.

<#macro test>
...<#t>
</#macro>

Example:
   - <@test /><#nt>

gives the result:

Example:
   - ...

You can see, in the macro, I defined <#t>, this is because the new line from inside the macro, will not be trimmed, and would always give a new line where you <@macro> it, so in one part, we trim the white-space, and in the other part, we keep it!

Edit:

It should be worth mentioning that, for some reason, this only works for one line. If you have multiple lines in your macro, it only keeps the indentation for the first line. Thus far I have found no fix for this but I created an issue in the Freemarker JIRA for this.

Example:

<#macro test>
...
wow
</#macro>

Example:
   - <@test><#nt>

will result in:

Example:
   - ...
wow
like image 42
Limnic Avatar answered Oct 14 '22 13:10

Limnic


The generic solution of these kind of problems (dynamic indentation) is a filter, that (rudimentary) understands the language that you generate (PHP) and re-indents the code. That filter you can implement as a Writer that wraps the real output Writer. Maybe it's good enough if it watches where the {, }, /* and */ tokens are (I'm not sure).

Another solution, which is easier to implement, is to create a custom FreeMarker directive via implementing TemplateDirectiveModel that filters the output generated in its nested content by simply adding or removing the amount of spaces given as parameter to it, at the beginning of each line. Then you could do something like:

<@indent spaces=4>
   ...
</@indent>

Using this will make the template more complicated, but it's still less noisy like inserting the indentation in each line.

like image 34
ddekany Avatar answered Oct 14 '22 15:10

ddekany


For those who wish to prefix an imported macro with some space indentations, here is a class which performs the work:

public final static class IndentDirective
    implements TemplateDirectiveModel
{

  private static final String COUNT = "count";

  public void execute(Environment environment, Map parameters, TemplateModel[] templateModels,
      TemplateDirectiveBody body)
      throws TemplateException, IOException
  {
    Integer count = null;
    final Iterator iterator = parameters.entrySet().iterator();
    while (iterator.hasNext())
    {
      final Map.Entry entry = (Map.Entry) iterator.next();
      final String name = (String) entry.getKey();
      final TemplateModel value = (TemplateModel) entry.getValue();

      if (name.equals(COUNT) == true)
      {
        if (value instanceof TemplateNumberModel == false)
        {
          throw new TemplateModelException("The \"" + COUNT + "\" parameter " + "must be a number");
        }
        count = ((TemplateNumberModel) value).getAsNumber().intValue();
        if (count < 0)
        {
          throw new TemplateModelException("The \"" + COUNT + "\" parameter " + "cannot be negative");
        }
      }
      else
      {
        throw new TemplateModelException("Unsupported parameter '" + name + "'");
      }
    }
    if (count == null)
    {
      throw new TemplateModelException("The required \"" + COUNT + "\" parameter" + "is missing");
    }

    final String indentation = StringUtils.repeat(' ', count);
    final StringWriter writer = new StringWriter();
    body.render(writer);
    final String string = writer.toString();
    final String lineFeed = "\n";
    final boolean containsLineFeed = string.contains(lineFeed) == true;
    final String[] tokens = string.split(lineFeed);
    for (String token : tokens)
    {
      environment.getOut().write(indentation + token + (containsLineFeed == true ? lineFeed : ""));
    }
  }

}

You may integrate it by adding configuration.setSharedVariable("indent", new IndentDirective()); to your FreeMarker configuration and then use it in your template by inserting

<@indent count=4>
[whathever template code, including macro usage]
</@indent>
like image 22
Édouard Mercier Avatar answered Oct 14 '22 13:10

Édouard Mercier