FreshMarker Includes (2)

“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!

Leave a Comment