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?
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.
esc creates a markup output value out of a string value by escaping all special characters in it.
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.)
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.
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>
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
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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With