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