“You see
You feel
You know
React
You’re waiting
You’re waitingChange“
Change – Killing Joke
The blog posts on the FreshMarker template engine pursue several goals. On the one hand, the posts should of course entertain their readers. On the other hand, they also serve me to focus and document my ideas on various aspects of the implementation. One very interesting topic in this post is the management of variables within the Template Engine.
The template engine cannot deny the source of inspiration, various concepts are too similar to another template engine. There are also some similarities in the variables, but there are also various differences. In FreshMarker, variables can be defined with the Var Directive and their values can be changed with the Set Directive. In FreshMarker there are no differences between global and local variables and there are no namespaces. A variable exists in its scope, which extends from the position of its definition to the end of its variable context. A variable context spans within Macro, List and Conditional Directives and of course there is an outer variable context, across the entire document.
Dhe following template represents the FizzBuzz function. This function outputs Fizz
or Buzz
for numbers that are divisible by 5 or 7. FizzBuzz
is output for numbers that are divisible by 5 and 7 and the value is output for all other numbers.
FizzBuzz Test <#var result=''> <#list [1, 5, 7, 13, 15, 21, 35] as number> <#if number % 7 == 0 && number % 5 == 0> <#set result='FizzBuzz'> <#elseif number % 5 == 0> <#set result='Fizz'> <#elseif number % 7 == 0> <#set result='Buzz'> <#else> <#set result=number> </#if> ${result} </#list>
FizzBuzz Test 1 Fizz Buzz 13 Fizz Buzz FizzBuzz
The template only checks the numbers 1
,5,7,13,15,21 and 35 so that the output on the left-hand side is not unnecessarily long. The template itself is unnecessarily long for didactic reasons. The use of variables is only required for this article.
In the second line, the result
variable is defined and then set to a new value, depending on the numerical value in lines 5, 7, 9 or 11. The current value is output in line 13. The previous Freshmarker implementation used various variable contexts for this.
The variable contexts are color-coded on the left-hand side. Red is the variable context for the entire template, green is the variable context in the List directive and blue is the four variable contexts in the blocks of the If directive. In total, there are 6 variable contexts that are created for accessing the result variable.
The nested variable contexts are necessary if, for example, a result variable is created at the beginning of the List Directive or at the beginning of a block of the If Directive. In this case, setting a value to the result
variable may only change the value of the innermost variable with the name result
. The outer variable with the same name result
may not be modified.
In this example, however, only a single variable context is required, as shown on the right. In fact, 6 variable contexts are also created in the previous FreshMarker version if the FizzBuzz template does not use any variables at all.
To change the way variable contexts are created, two things need to be implemented. Firstly, the previously responsible directives may no longer create variable contexts. This must now be done by the VariableFragment
when defining a variable. Secondly, the variable context must be removed when the processing of the surrounding directive is completed.
The second part of the implementation is done quite simply. So far, directives that span a variable context have a variant of the following process method.
@Override public void process(ProcessContext context) { Environment environment = context.getEnvironment(); try { context.setEnvironment(new VariableEnvironment(environment)); content.process(context); } finally { context.setEnvironment(environment); } }
The current Environment
is replaced by a VariableEnvironment
that takes care of the variables in this directive. Requests for unknown variables are forwarded to the previous environment and processed there. At the end, the VariableEnvironment
is replaced by the original Environment
in the finally block.
If no VariableEnvironment
is to be created at this point, we leave this line out.
@Override public void process(ProcessContext context) { Environment environment = context.getEnvironment(); try { content.process(context); } finally { context.setEnvironment(environment); } }
The finally block remains, as this removes Environment
instances created in the meantime.
The first part of the implementation is not so easy. When defining a variable in the VariableFragment
, the system has so far checked whether a variable with the name
does not yet exist and then creates the variable.
private void handleVar(ProcessContext context) { Environment environment = context.getEnvironment(); if (environment.checkVariable(name)) { throw new ProcessException("variable " + name + " must not exist", node); } environment.createVariable(name, expression.evaluateToObject(context)); }
A VariableEnvironment
cannot simply be created here because a VariableEnvironment
may already have been created by a previous Var Directive. In this case, the second variable must not create a new scope, but must use the variable context of the first. In the other case, the template snippet <#var result=1><#var result=1>
would not generate an error.
The problem can be solved with an init
field in the VariableFragment
, which is set for the the first instance that occurs in a block.
private void handleVar(ProcessContext context) { Environment environment = context.getEnvironment(); if (init.get()) { environment = new VariableEnvironment(environment); context.setEnvironment(environment); } if (environment.checkVariable(name)) { throw new ProcessException("variable " + name + " must not exist", node); } environment.createVariable(name, expression.evaluateToObject(context)); }
The field can then be used in the handleVar
method to create a VariableEnvironment
. Now only the init
field needs to be set in the VariableFragment
and the implementation of the feature is complete.
In fact, the implementation was finished at this point. But something struck me when I was formulating the previous paragraph.
The optimize
method is used in the directives to optimize blocks. Where the list of Fragment
instances is empty, an empty Fragment
was inserted with ConstantFragment.EMPTY
; if there is a single Fragment
in the list, this element was returned directly. If there are several fragments in the list, these are inserted into a BlockFragment
and this is returned.
public static Fragment optimizeWithVariableContext(List<Fragment> fragments) { return switch (fragments.size()) { case 0 -> ConstantFragment.EMPTY; case 1 -> activateVariableContext(fragments).getFirst(); default -> new BlockFragment(activateVariableContext(fragments)); }; } public static List<Fragment> activateVariableContext(List<Fragment> input) { input.stream().filter(VarVariableFragment.class::isInstance).map(VarVariableFragment.class::cast) .findFirst().ifPresent(fragment -> fragment.init().set(true)); return input; }
The modified optimize
method optimizeWithVariableContext
shown here should be used at the points where the first VariableFragment
in the list should have the init
field set.
However, a simpler solution can be chosen here. If the list of Fragment
instances contains a variable definition, then a new VariableBlockFragment
can be used instead of a BlockFragment
. This creates a VariableEnvironment
at the start of a block and removes it again at the end. This eliminates the logic for handling the VariableEnvironment
in all other fragments.
public static Fragment optimizeWithVariableContext(List<Fragment> fragments) { boolean containsVar = isContainsVar(fragments); return switch (fragments.size()) { case 0 -> ConstantFragment.EMPTY; case 1 -> containsVar ? ConstantFragment.EMPTY : fragments.getFirst(); default -> containsVar ? new VariableBlockFragment(fragments) : new BlockFragment(fragments); }; }
The modified optimizeWithVariableContext
contains a further optimization. If the list contains only one element and this is a variable definition, it is discarded as the variable can never be used in the block. If there is more than one element, either a VariableBlockFragment
or a BlockFragment
is returned, depending on the containsVar
flag.
This means that the use of variables in the new FreshMarker version 1.9.0 has been revised not once but twice.