Template Engine
Template Engine Sub Project
The aim of the project is to examine the Freemarker library with a view to using it to replace our export engine. In particular it would cover functionality logic functions like looping and if tests. This would leave our code to deal with the important things like TO HIT values etc.
We would do this in three phases:
- Add support for Freemarker templates for output
- Once sufficient sheets have been converted/rewritten, deprecate support for old sheet types
- Finally after a few releases, remove support for the old sheet types.
There are a few reasons for exploring this change
- Add a richer set of logic commands to enable more customisable output
- Simpler more consistent logic commands and more widely understood syntax
- Remove one of the more complex and difficult to maintain sections of the PCGen code base.
Status
Currently working on phase 1.
FreeMarker support has been added to PCGen and is available in the trunk autobuilds. It will first be available in the 6.03.00 alpha release scheduled for February 2014.
Progress is being tracked in CODE-2418 FreeMarker Output Generation - Phase 1
FreeMarker Template How-To
Introduction to FreeMarker
FreeMarker is a template engine that has now been incorporated into PCGen's export system as an alternative to our venerable export syntax. The FreeMarker engine brings some important benefits:
- Industry standard syntax
- Support for macros to output parameterised blocks
- Improved flow-control and logic instructions
- Support for including other files, e.g. macro libraries or common blocks
- Calculations and state within the output template
- Built-in syntax support in many text editors
Here's a simple example FreeMarker PCGen template
<!-- Produced on ${.now?date} at ${.now?time} using template ${.template_name} --> <h1>Freemarker Sheet for <@pcstring tag="NAME"/> - ${.now?date}</h1> <p> Character Name: <@pcstring tag="NAME"/> </p> <p> Player Name: <@pcstring tag="PLAYERNAME"/> </p> <p> Fighter: ${pcvar("CL=Fighter")} </p> <p> Rogue: ${pcvar("CL=Rogue")} </p> <p><@loop from=0 to=pcvar('countdistinct("CLASSES")')-1 ; class , class_has_next > <@pcstring tag="CLASSABB.${class}"/> <#t> <@pcstring tag="CLASS.${class}.LEVEL"/><#if class_has_next>,</#if><#t> </@loop></p>
which produces the output
<!-- Produced on 29/10/2013 at 9:44:47 PM using template csheet_test-html.ftl --> <h1>Freemarker Sheet for Ronald 'Surefingers' Millbridge - 29/10/2013</h1> <p> Character Name: Ronald 'Surefingers' Millbridge </p> <p> Player Name: James </p> <p> Fighter: 0 </p> <p> Rogue: 11 </p> <p>Rog 11,Wiz 2</p>
FreeMarker Documentation
FreeMarker has quite thorough documentation. The main references output sheet authors will use are:
In addition, see testsuite/base-xml.ftl for a fully worked example of xml output.
Creating a Sheet
FreeMarker sheets are detected by PCGen by their file name suffix. A template ending in .ftl will be processed by FreeMarker rather than by PCGen's legacy output engine. The extension of the file to be produced should be immediately before that and preceded by a . or -. So a template to produce html output might be named csheet-first.html.ftl or chseet-compact-html.ftl and PCGen would pick up that it was a template to be processed by FreeMarker template that would produce output of a html file.
PCGen's Custom Functions
To provide output of PCGen characters we need to be able to access the character data and the output tags. The following tags are provided for that purpose.
pcstring
This tag evaluates a PCGen export token for the current character and returns the value as a string. It may be called as a directive (with a single parameter named 'tag') or as a function (with a single unnamed parameter).
Examples
<@pcstring tag="PLAYERNAME"/> <@pcstring tag="CLASSABB.${class}"/> ${pcstring('CLASSABB.${class}')} <#if (pcstring('SPELLLISTCLASS.${class}')?length > 0)>present</#if>
pcvar
This tag allows character variable values to be exported to a FreeMarker template. It evaluates a variable for the current character and returns the value as a number.
Examples
<#assign numClasses=pcvar('countdistinct("CLASSES")')/> ${pcvar('count("ABILITIES","CATEGORY=FEAT","TYPE=Metamagic","VISIBILITY=DEFAULT[or]VISIBILITY=OUTPUT_ONLY")')} ${pcvar("CL=Fighter")}
pcboolean
pcboolean allows character boolean values to be exported to a FreeMarker template. It evaluates an export token for the current character and returns the value as a boolean.
Examples
<#if pcboolean('WEAPON.${weap}.ISTYPE.Double')> <#if (pcboolean('HASVAR:Manifester.OR.HASVAR:PsychicWarriorManifester')) >
loop
The loop tag provides a way to repeat content a certain number of times. Unlike the inbuilt list directive it can loop zero times.
Parameters
- from (optional) - The starting value, defaults to 0.
- to - The ending value (inclusive). If this is less than from then the contents will not be output.
- step (optional) - The amount to increment b each loop, defaults to 1.
In addition up to two loopvars may be specified. The first will be populated with the current index value of the loop and the second will be a boolean indicating if there are more iterations of the loop to go. We recommend that the second variable, if present, is named after the first variable with _has_next added. e.g. class, class_has_next . This is in order to match the list directive, which provides a similarly named variable automatically.
Example
<@loop from=0 to=pcvar('countdistinct("CLASSES")')-1 ; class , class_has_next > <@pcstring tag="CLASSABB.${class}"/> <@pcstring tag="CLASS.${class}.LEVEL"/><#if class_has_next>,</#if> </@loop>
might produce
Rog 11, Wiz 5
equipsetloop
The equipsetloop tag provides a way to repeat content for each of a character's equipment sets.
Parameters
None
Example
<@equipsetloop> <b>Equipment Set : </b> ${pcstring("EQSET.NAME")}. Equipment: <@loop from=0 to=pcvar('COUNT[EQUIPMENT.MERGELOC]')-1 ; equip , equip_has_next > ${pcstring("EQ.MERGELOC.${equip}.NAME")}<#if equip_has_next>, </#if> </@loop><#lt><#-- Equipment --> </equipmentset><br/> </@equipsetloop>
might produce
<b>Equipment Set : </b> Travelling Gear. Equipment: Backpack, Longsword, Traveller's Outfit<br/> <b>Equipment Set : </b> Council. Equipment: Coin purse, Noble's Outfit, Rapier<br/>
Phase 1
The aim of phase 1 is to utilise the already built output system and add the ability to
- Detect Freemarker templates and use the Freemarker engine to process them
- Add custom directives that can be called from a template (output sheet) to allow the existing output tags to be used.
This gives us a simple change-over and use of our existing well tested output tags.
We need to ensure that freemarker templates can be used in html, and PDF output styles.
See User Defined Directives and Shared Variables for information on creating custom directives/tags.
1b Assisted Looping
Freemarker's inbuilt looping mechanism is object oriented. It expects to receive a list and process through it. In contrast, PCGen currently only outputs strings and numbers. The technique described at For loop can be used, which results in a structure like the following to loop through a list:
<#assign max=pcvar('countdistinct("CLASSES")')-1> <#if (max>=0)> <#list 0..max as class> <@pcstring tag="CLASSABB.${class}"/> <@pcstring tag="CLASS.${class}.LEVEL"/><#if class_has_next>,</#if> </#list> </#if>
Note The if test is to allow the loop to be skipped if the list is empty.
Now this is a bit verbose so I have instead created a custom 'loop' directive to allow looping a number of times. This also deals with the empty loop scenario. See loop
1c Object Output
To more closely align with FreeMarker's object oriented approach, we should examine the potential for exporting objects for output. So for example, exporting a list of spell objects that can then be looped over and each object queried as needed. A base for this output interface could be the Facade work done for the user interface.
<#list pc.knownspells as spell> <li>${spell.name} School: ${spell.school} <#if spell.subschool??> [${spell.school}] </#if></li> </#list>