FreeMarker: Keep Identity When Using Macros

I use the FreeMarker template engine to generate some php classes from an abstract description of a web service. My problem is that when I call the macro in the FreeMarker template, the macro inserts the text without space or space before calling the macro.

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 create something like this:

 <?php class foo { /* * foo * bar foo, bla */ $a; $b; } ?> 

One solution would be to introduce leading spaces as an argument to the macro, but this makes the template even more unreadable. Is there a better solution?

+8
source share
4 answers

It would seem that docAsComment always called at the same level of indentation in code generation. You can bake this indentation in a macro.

If the indentation of the comment is variable, you will need to go at the indentation level. I do not understand your comment about making the template more difficult to read. This makes the macro a little more complicated.

The call will look like this:

 <@docAsComment class.doc 1/> 

The 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 so bad, really. You can make a macro a little easier to read by renaming 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> 
+5
source

A common solution to these problems (dynamic indentation) is a filter that (rudimentary) understands the language you are generating (PHP) and reinstalls the code. This filter can be implemented as Writer , which wraps the actual output of Writer . Maybe this is good enough if it looks where the tokens are { , } , /* and */ (I'm not sure).

Another solution that is easier to implement is to create a custom FreeMarker directive using the TemplateDirectiveModel implementation, which filters the output generated in the embedded content by simply adding or removing the number of spaces specified as an argument to it at the beginning of each line. Then you can do something like:

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

Using this will make the template more complex, but it is even less noisy, like inserting indentation in each line.

+1
source

Today you can use <#nt> . The whitespace documentation reports the following about it:

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

According to V2.3 changelog in previous versions, lines containing only FTL tags are truncated, with the exception of <#include> and user directives (for example, <@macroname> ). But in V2.3, they changed this behavior to ALWAYS cut off such lines. So, when using a macro, you can put <#nt> in a line to prevent cropping, and thus keep the indentation.

 <#macro test> ...<#t> </#macro> Example: - <@test /><#nt> 

gives the result:

 Example: - ... 

You can see that in the macro I defined <#t> , this is because the new line from the inside of the macro will not be trimmed and will always give a new line, where you are <@macro> , so in one part, we crop the white space, and in another part we save it!

Edit:

It should be noted that for some reason this only works for one line . If your macro has multiple lines, it only saves the indent for the first line. So far I have not found any fixes for this, but I created a problem in Freemarker JIRA for this.

Example:

 <#macro test> ... wow </#macro> Example: - <@test><#nt> 

will result in:

 Example: - ... wow 
+1
source

For those who want to add a few spaces to the imported macro, here is a class that does the job:

 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 can integrate it by adding configuration.setSharedVariable("indent", new IndentDirective()); in your FreeMarker configuration and then use it in your template by inserting

 <@indent count=4> [whathever template code, including macro usage] </@indent> 
0
source

All Articles