“L’éternité, c’est long … surtout vers la fin.”
Franz Kafka
In the first article on the Include Directive, I showed how this directive can be implemented in FreshMarker. Although additional content can now be inserted into the template, some details are still missing.
An important detail is the avoidance of recursive includes. These usually never end well. The following template recursive.fmt
shows the simplest form of the problem.
recursive <#include 'recursive.fmt'/>
If this include is used in a template, the parsing of the template ends with an error. There are a number of ways to avoid this problem. For the moment, a simple solution is sufficient, so we limit the depth of the includes by giving each include a counter that is incremented in relation to its calling include.
An include in the template therefore has the counter 0 and all includes in this include have the counter 1. If the counter reaches a certain limit, the include is no longer processed.
private boolean handleLimitIncludeLevel(IncludeInstruction ftl) { return includeLevel > 4; } @Override public List<Fragment> visit(IncludeInstruction ftl, List<Fragment> input) { if (featureSet.isDisabled(IncludeDirectiveFeature.ENABLED)) { logger.info("include directive ignored"); return List.of(); } if (handleLimitIncludeLevel(ftl)) { logger.info("include level exceeded"); return List.of(); } // ... }
In the above implementation, a depth of five include directives is permitted, further includes are ignored. Our recursive example can thus be processed and generates the following output.
recursive recursive recursive recursive recursive
Now that FreshMarker has mastered features, we would like to control this implementation via additional features. Two additional flags LIMIT_INCLUDE_LEVEL
and IGNORE_LIMIT_EXCEEDED_ERROR
change the processing.
private boolean handleLimitIncludeLevel(IncludeInstruction ftl) { if (featureSet.isDisabled(IncludeDirectiveFeature.LIMIT_INCLUDE_LEVEL)) { return false; } boolean isLimitExceeded = includeLevel > 4; if (isLimitExceeded && featureSet.isDisabled(IncludeDirectiveFeature.IGNORE_LIMIT_EXCEEDED_ERROR)) { throw new ParsingException("include level exceeded: " + includeLevel, ftl); } return isLimitExceeded; }
The LIMIT_INCLUDE_LEVEL
flag switches the limitation on or off and IGNORE_LIMIT_EXCEEDED_ERROR
can be used to choose between an exception or a message when the limit is exceeded.
Another improvement for the Include Directive is achieved if we do not need to activate a FreshMarkerParser
for the content. Plain texts such as CSS styles, code examples, licences or terms and conditions do not contain FreshMarker directives or interpolations, so their content can be inserted directly into a ConstantFragment
. In order to be able to distinguish whether the content of the include should be parsed or not, we add additional parameters to the grammar.
Include #IncludeInstruction : (<FTL_DIRECTIVE_OPEN1>|<FTL_DIRECTIVE_OPEN2>) <_INCLUDE><BLANK> Expression [<SEMICOLON>] (<IDENTIFIER><EQUALS>Expression)* LooseTagEnd ;
With this addition, we can pass any parameters to the Include Directive. For the time being, however, only the boolean
parameter parse
.
Map<String, Object> parameters = getParameters(ftl); TemplateBoolean parsedIncludeDefault = TemplateBoolean.from(featureSet.isEnabled(IncludeDirectiveFeature.PARSE_BY_DEFAULT)); logger.info("include parsed default: {}", parsedIncludeDefault); if (TemplateBoolean.FALSE.equals(parameters.getOrDefault("parse", parsedIncludeDefault))) { input.add(new ConstantFragment(template.getTemplateLoader().getImport(template.getPath(), path))); logger.info("include unparsed fragment"); return input; }
If the parameter is true
, the content of the directive is parsed. If the parameter is false
, the above code fragment is executed and the content of the include is loaded and inserted into a ConstantFragment
. If the parameter is not set, the PARSE_BY_DEFAULT
feature is used to decide whether to parse or not.
The Include Directive can be tried out in the new FreshMarker version 1.7.0. Have fun!