“There is no compression algorithm for experience.”
Andy Jassy
Whitespace in a template engine like FreshMarker‘s is fundamentally different from the rest of the characters outside of it’s language constructs. A considerable amount of whitespace and line breaks are only used to ensure that the structure of the template remains comprehensible to the author.
For this reason, a whitespace handling was built into Freshmarker quite early on, which removes leading and trailing spaces in pure command lines.
␣␣<#list ancestors as ancestor>␣␣⏎
␣␣${ancestor.firstname}␣${ancestor.lastname}⏎
␣␣</#loop>␣␣␣␣⏎
␣␣Johann␣Seemann⏎
␣␣Friedrich Magnus␣Kayser⏎
The spaces and line breaks colored blue here on the left are discarded when the template instance is created. In the generated content on the right, only the whitespaces of the middle line are written and create an indented list of ancestors.
However, it has been shown that there are other situations in which a modified whitespace handling is desirable.
Let us assume that these names of the ancestors are to be listed in an HTML attribute. With the existing possibilities, only the following solution is conceivable.
<span onclick="javascript:show(<#list ancestors as ancestor>${ancestor.firstname}␣${ancestor.lastname},</#list>">
Anchestors
</#span>
To avoid line breaks, the list directive must be written in a single line. This is very confusing. However, there is already a solution to make it more readable.
The user directive oneliner
, which has been included in the FreshMarker library for some time as an example of a user directive, is included in the output and replaces each line break with a space.
<span onclick="javascript:show(<@oneliner>
<#list ancestors as ancestor>
${ancestor.firstname}␣${ancestor.lastname},
</#list>"
</@oneliner>>
Anchestors
</#span>
As the breaks are removed during processing, they can be inserted into the template before processing to improve readability.
Freemarker shows another way of dealing with whitespaces in content with its compress
directive. Like some other Freemarker constructs, this has not made it into the FreshMarker feature set. A special language construct for removing whitespaces does not seem to be appropriate.
However, there is of course no reason not to implement this as another user directive. Like the OneLinerDirective
, the CompressDirective
implements the UserDirective
interface.
public class CompressDirective implements UserDirective { @Override public void execute(ProcessContext context, Map<String, TemplateObject> args, Fragment body) { Writer oldWriter = context.getWriter(); try (CompressWriter writer = new CompressWriter(context.getWriter())) { context.setWriter(writer); body.process(context); } catch (IOException e) { throw new ProcessException("flatten file writer: " + e.getMessage(), e); } finally { context.setWriter(oldWriter); } } }
This CompressDirective
inserts the existing Writer
in another decorator for its content. This CompressWriter
decorator does the detailed work and is removed again at the end of the directive processing.
<@compress>
␣␣␣␣⏎
␣␣Dies␣␣ist␣␣ein␣␣Test␣␣⏎
␣␣␣␣⏎
␣␣␣␣⏎
</@compress>
⏎Dies␣ist␣ein␣Test⏎
The task of the CompressWriter
is to cut off spaces at the beginning and end and to convert sequences consisting only of spaces into a single space character. The green spaces on the left-hand side appear as a single space on the right-hand side. The blue whitespaces consist of spaces and line breaks and are converted into a single line break.
@Override public void write(String str) throws IOException { if (str.indexOf(' ') == -1 && str.indexOf('\n') == -1 && str.indexOf('\t') == -1) { if (replacement != 0) { out.write(replacement); replacement = 0; } prefix = false; out.write(str); return; } if (prefix) { str = str.replaceAll("^ +", ""); prefix = false; } out.write(handleWhitespaces(str)); }
To keep the implementation simple, we first check whether there is any whitespace in the output. If this is the case, the output is passed to the decorated writer. Before this, however, any stored replacement
character is output and the prefix
flag is switched off. The prefix
flag is used for the special handling of leading spaces. They are removed from the output once in the second If-block. In the last line, the output is sent to the decorated writer with corrected whitespaces.
The correction is made in the handleWhitespaces
method.
private String handleWhitespaces(String str) { StringBuilder builder = new StringBuilder(); replacement = 0; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); switch (c) { case ' ' -> replacement = replacement == 0 ? ' ' : replacement; case '\n', '\r' -> replacement = '\n'; default -> { if (replacement != 0) { builder.append(replacement); replacement = 0; } builder.append(c); } } } return builder.toString(); } @Override public void close() throws IOException { if (replacement == '\n') { out.write(replacement); replacement = 0; } out.close(); }
Within the method, a loop is run through all characters, if it is a space, then the space is set in replacement
. But only if replacement
does not contain a line break. In this case, the line break beats the space character. In the case of a line break, replacement
is set to the line break character. In this case, it does not matter what was previously in the variable. The line break character has the highest value. The only case that remains to be considered is when any other character is processed. This is appended directly to the builder
. If there is a whitespace character in replacement
, this is appended to builder
beforehand and replacement
is reset.
Now only the last character is missing if replacement
is not 0
at the end. In this case, the character only needs to be written if it is a line break. A space in replacement would mean that there are only spaces at the end of the output, all of which must be removed after the compress
contract.
This means that the implementation of the user directive compress
as a replacement for the compress
directive has almost come to an end. However, there is still a small improvement for FreshMarker users.
Until now, the user directives provided had to be registered by the users in the Configuration
. The compress
user directive would therefore have required the following lines of code.
Configuration configuration = new Configuration(); configuration.registerUserDirective("compress", new CompressDirective());
So that users no longer have to worry about this, the user directives compress
and oneliner
are automatically registered via the SystemPluginProvider
. The following method is required for this.
@Override public void registerUserDirective(Map<String, UserDirective> directives) { directives.put("oneliner", new OneLinerDirective()); directives.put("compress", new CompressDirective()); }
The implementation is now complete and the corrected version of the compress
user directive can soon be used with FreshMarker 1.7.6.