FreshMarker Import Directive

“Conceptual integrity is the most important consideration in system design.”

Frederick P. Brooks

FreshMarker is an embedded Java template engine that focusses on extensibility and simplicity. For this reason, every implementation of an extension is associated with weighing up its pros and cons. For a long time, the import directive therefore remained an unrealised feature.

In the meantime, the decision has been made to implement a simple version of an import for FreshMarker. Initially, it will only be possible to import macros from separate files.

The following example shows the file fragments.fmt, which contains two macros. The first macro writes a salutation and the second a signature. Both are each provided with a parameter to vary the content.

<#macro salutation name>
Dear ${name},
</#macro>
            
<#macro signature host>
Best regards,
${host.name}
${host.organisation}
${host.contact}
</#macro>

Until now, these macros had to be specified in the actual template file. This leads to confusion and duplicated template code if the macros are to be used in many templates.

The two templates are inserted into the actual template with the import directive. As an example, the template instance for the invitation to a Genealogy Conference is generated here. The content of the example is generated by an AI, so please don’t book tickets for the conference.

Template template = configuration.getTemplate("template", """
<#import 'fragments.fmt' as f>
Subject: Invitation to the Genealogy Conference 2023
        
<@f.salutation name=name/>
       
We are delighted to invite you to the Genealogy Conference 2023, taking place on August 24 in Bielefeld.
        
Join us for engaging lectures, interactive workshops, and insightful panel discussions with experts from around the world. 
This is a wonderful opportunity to deepen your knowledge and connect with fellow genealogy enthusiasts.
        
Please find details about registration and the event program attached to this email. 
We look forward to your participation and an enriching exchange!
        
<@f.signature host=host/>
""");
String email = template.process(Map.of(
  "host", new Host("Jens Kaiser", "Schegge.De", "+49 176 04069069"),
  "name", "Henry Walton Jones")))

The import directive in the first line contains the name of the file to be loaded and a namespace specification. The specifications from the file name according to which the file is actually found will be discussed in a subsequent article. The namespace is used to be able to use macros with the same name from different imports in parallel.

To call a macro from a special namespace, it is sufficient to precede the name with the namespace followed by a dot. In the example above, <@f.salutation name=name/> instead of <@salutation name=name/> as before.

If a text is generated from the template and a sufficient model, the following text is found in the variable email at the end.

Subject: Invitation to the Genealogy Conference 2023
        
Dear Henry Walton Jones,
        
We are delighted to invite you to the Genealogy Conference 2023, taking place on August 24 in Bielefeld.
        
Join us for engaging lectures, interactive workshops, and insightful panel discussions with experts from around the world.
This is a wonderful opportunity to deepen your knowledge and connect with fellow genealogy enthusiasts.
        
Please find details about registration and the event program attached to this email. 
We look forward to your participation and an enriching exchange!
        
Best regards,
Jens Kaiser
Schegge.De
+49 176 04069069

The macro call salutation has been correctly replaced by Dear Henry Walton Jones, and the signature call by the organiser.

At the core of FreshMarker’s import mechanism is the visit method for ImportInstruction in the FragmentBuilder. The FragmentBuilder creates the template from the parse tree generated by CongoCC.

@Override
public BlockFragment visit(ImportInstruction ftl, BlockFragment input) {
  String path = ftl.get(3).accept(InterpolationBuilder.INSTANCE, null).toString();
  String namespace = ftl.get(5).toString();
  try {
    FreshMarkerParser parser = new FreshMarkerParser(Files.readString(configuration.getFileSystem().getPath(path)));
    parser.setInputSource(namespace);
    parser.Root();
    Root root = (Root) parser.rootNode();
    new TokenLineNormalizer().normalize(root);
    root.accept(new ImportBuilder(template, configuration, namespace), template.getRootFragment());
    return input;
  } catch (FileNotFoundException e) {
    throw new ParsingException("cannot find import: " + path, ftl);
  } catch (IOException e) {
    throw new ParsingException("cannot read import: " + path, ftl);
  }
}

The ImportInstruction contains the name of the import file and the namespace name at positions 4 and 6. A new parser is created with this information and the parse tree generated with it is processed with an ImportBuilder. Like the FragmentBuilder, the ImportBuilder is a visitor for a CongoCC parse tree. The task of the ImportBuilder is to react only to macro definitions from the parse tree and to delegate their processing to the FragmentBuilder.

Unlike FreeMarker, UserDirectives defined by macros are not stored in the data model but in a separate data structure. Therefore, the namespace is actually a name extension and not an expression that refers to a data structure at runtime. The changes for the use of a namespace are therefore easy.

@Override
public UserDirective getDirective(String nameSpace, String name) {
  return Optional.ofNullable(userDirectives.get(new NameSpaced(nameSpace, name)))
      .orElseThrow(() -> new ProcessException("unknown directive: " + name));
}

To call a UserDirective, the namespace is passed in addition to the name. Where previously the name was the key in the userDirectives map, a NameSpaced record is now used. For macros from an import, the corresponding namespace is part of the key and for macros from the template null is inserted as namespace to the key.

The implementation of the Import Directive for FreshMarker is now complete and will soon be available with version 1.0.0 on Maven Central.

Leave a Comment